1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.launcher3.touch;
17 
18 import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
19 import static com.android.launcher3.LauncherAnimUtils.newCancelListener;
20 import static com.android.launcher3.LauncherState.ALL_APPS;
21 import static com.android.launcher3.LauncherState.NORMAL;
22 import static com.android.launcher3.LauncherState.OVERVIEW;
23 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
24 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_ALLAPPS;
25 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
26 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW;
27 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEDOWN;
28 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEUP;
29 import static com.android.launcher3.util.DisplayController.getSingleFrameMs;
30 
31 import android.animation.Animator.AnimatorListener;
32 import android.animation.ValueAnimator;
33 import android.view.MotionEvent;
34 
35 import com.android.launcher3.Launcher;
36 import com.android.launcher3.LauncherAnimUtils;
37 import com.android.launcher3.LauncherState;
38 import com.android.launcher3.Utilities;
39 import com.android.launcher3.anim.AnimatorPlaybackController;
40 import com.android.launcher3.logger.LauncherAtom;
41 import com.android.launcher3.logging.StatsLogManager;
42 import com.android.launcher3.states.StateAnimationConfig;
43 import com.android.launcher3.util.FlingBlockCheck;
44 import com.android.launcher3.util.TouchController;
45 
46 /**
47  * TouchController for handling state changes
48  */
49 public abstract class AbstractStateChangeTouchController
50         implements TouchController, SingleAxisSwipeDetector.Listener {
51 
52     protected final Launcher mLauncher;
53     protected final SingleAxisSwipeDetector mDetector;
54     protected final SingleAxisSwipeDetector.Direction mSwipeDirection;
55 
56     protected final AnimatorListener mClearStateOnCancelListener =
57             newCancelListener(this::clearState);
58     private final FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck();
59 
60     protected int mStartContainerType;
61 
62     protected LauncherState mStartState;
63     protected LauncherState mFromState;
64     protected LauncherState mToState;
65     protected AnimatorPlaybackController mCurrentAnimation;
66     protected boolean mGoingBetweenStates = true;
67     // Ratio of transition process [0, 1] to drag displacement (px)
68     protected float mProgressMultiplier;
69 
70     private boolean mNoIntercept;
71     private boolean mIsLogContainerSet;
72     private float mStartProgress;
73     private float mDisplacementShift;
74     private boolean mCanBlockFling;
75     private boolean mAllAppsOvershootStarted;
76 
AbstractStateChangeTouchController(Launcher l, SingleAxisSwipeDetector.Direction dir)77     public AbstractStateChangeTouchController(Launcher l, SingleAxisSwipeDetector.Direction dir) {
78         mLauncher = l;
79         mDetector = new SingleAxisSwipeDetector(l, this, dir);
80         mSwipeDirection = dir;
81     }
82 
canInterceptTouch(MotionEvent ev)83     protected abstract boolean canInterceptTouch(MotionEvent ev);
84 
85     @Override
onControllerInterceptTouchEvent(MotionEvent ev)86     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
87         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
88             mNoIntercept = !canInterceptTouch(ev);
89             if (mNoIntercept) {
90                 return false;
91             }
92 
93             // Now figure out which direction scroll events the controller will start
94             // calling the callbacks.
95             final int directionsToDetectScroll;
96             boolean ignoreSlopWhenSettling = false;
97 
98             if (mCurrentAnimation != null) {
99                 directionsToDetectScroll = SingleAxisSwipeDetector.DIRECTION_BOTH;
100                 ignoreSlopWhenSettling = true;
101             } else {
102                 directionsToDetectScroll = getSwipeDirection();
103                 if (directionsToDetectScroll == 0) {
104                     mNoIntercept = true;
105                     return false;
106                 }
107             }
108             mDetector.setDetectableScrollConditions(
109                     directionsToDetectScroll, ignoreSlopWhenSettling);
110         }
111 
112         if (mNoIntercept) {
113             return false;
114         }
115 
116         onControllerTouchEvent(ev);
117         return mDetector.isDraggingOrSettling();
118     }
119 
getSwipeDirection()120     private int getSwipeDirection() {
121         LauncherState fromState = mLauncher.getStateManager().getState();
122         int swipeDirection = 0;
123         if (getTargetState(fromState, true /* isDragTowardPositive */) != fromState) {
124             swipeDirection |= SingleAxisSwipeDetector.DIRECTION_POSITIVE;
125         }
126         if (getTargetState(fromState, false /* isDragTowardPositive */) != fromState) {
127             swipeDirection |= SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
128         }
129         return swipeDirection;
130     }
131 
132     @Override
onControllerTouchEvent(MotionEvent ev)133     public final boolean onControllerTouchEvent(MotionEvent ev) {
134         return mDetector.onTouchEvent(ev);
135     }
136 
getShiftRange()137     protected float getShiftRange() {
138         return mLauncher.getAllAppsController().getShiftRange();
139     }
140 
141     /**
142      * Returns the state to go to from fromState given the drag direction. If there is no state in
143      * that direction, returns fromState.
144      */
getTargetState(LauncherState fromState, boolean isDragTowardPositive)145     protected abstract LauncherState getTargetState(LauncherState fromState,
146             boolean isDragTowardPositive);
147 
initCurrentAnimation()148     protected abstract float initCurrentAnimation();
149 
reinitCurrentAnimation(boolean reachedToState, boolean isDragTowardPositive)150     private boolean reinitCurrentAnimation(boolean reachedToState, boolean isDragTowardPositive) {
151         LauncherState newFromState = mFromState == null ? mLauncher.getStateManager().getState()
152                 : reachedToState ? mToState : mFromState;
153         LauncherState newToState = getTargetState(newFromState, isDragTowardPositive);
154 
155         onReinitToState(newToState);
156 
157         if (newFromState == mFromState && newToState == mToState || (newFromState == newToState)) {
158             return false;
159         }
160 
161         mFromState = newFromState;
162         mToState = newToState;
163 
164         mStartProgress = 0;
165         if (mCurrentAnimation != null) {
166             mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener);
167         }
168         mProgressMultiplier = initCurrentAnimation();
169         mCurrentAnimation.dispatchOnStart();
170         return true;
171     }
172 
onReinitToState(LauncherState newToState)173     protected void onReinitToState(LauncherState newToState) {
174     }
175 
onReachedFinalState(LauncherState newToState)176     protected void onReachedFinalState(LauncherState newToState) {
177     }
178 
179     @Override
onDragStart(boolean start, float startDisplacement)180     public void onDragStart(boolean start, float startDisplacement) {
181         mStartState = mLauncher.getStateManager().getState();
182         mIsLogContainerSet = false;
183 
184         if (mCurrentAnimation == null) {
185             mFromState = mStartState;
186             mToState = null;
187             cancelAnimationControllers();
188             reinitCurrentAnimation(false, mDetector.wasInitialTouchPositive());
189             mDisplacementShift = 0;
190         } else {
191             mCurrentAnimation.pause();
192             mStartProgress = mCurrentAnimation.getProgressFraction();
193         }
194         mCanBlockFling = mFromState == NORMAL;
195         mFlingBlockCheck.unblockFling();
196     }
197 
198     @Override
onDrag(float displacement)199     public boolean onDrag(float displacement) {
200         float deltaProgress = mProgressMultiplier * (displacement - mDisplacementShift);
201         float progress = deltaProgress + mStartProgress;
202         updateProgress(progress);
203         boolean isDragTowardPositive = mSwipeDirection.isPositive(
204                 displacement - mDisplacementShift);
205         if (progress <= 0) {
206             if (reinitCurrentAnimation(false, isDragTowardPositive)) {
207                 mDisplacementShift = displacement;
208                 if (mCanBlockFling) {
209                     mFlingBlockCheck.blockFling();
210                 }
211             }
212         } else if (progress >= 1) {
213             if (reinitCurrentAnimation(true, isDragTowardPositive)) {
214                 mDisplacementShift = displacement;
215                 if (mCanBlockFling) {
216                     mFlingBlockCheck.blockFling();
217                 }
218             }
219             if (mToState == LauncherState.ALL_APPS) {
220                 mAllAppsOvershootStarted = true;
221                 // 1f, value when all apps container hit the top
222                 mLauncher.getAppsView().onPull(progress - 1f, progress - 1f);
223             }
224 
225         } else {
226             mFlingBlockCheck.onEvent();
227 
228         }
229 
230         return true;
231     }
232 
233     @Override
onDrag(float displacement, MotionEvent ev)234     public boolean onDrag(float displacement, MotionEvent ev) {
235         if (!mIsLogContainerSet) {
236             if (mStartState == ALL_APPS) {
237                 mStartContainerType = LAUNCHER_STATE_ALLAPPS;
238             } else if (mStartState == NORMAL) {
239                 mStartContainerType = LAUNCHER_STATE_HOME;
240             } else if (mStartState == OVERVIEW) {
241                 mStartContainerType = LAUNCHER_STATE_OVERVIEW;
242             }
243             mIsLogContainerSet = true;
244         }
245         return onDrag(displacement);
246     }
247 
updateProgress(float fraction)248     protected void updateProgress(float fraction) {
249         if (mCurrentAnimation == null) {
250             return;
251         }
252         mCurrentAnimation.setPlayFraction(fraction);
253     }
254 
255     /**
256      * Returns animation config for state transition between provided states
257      */
getConfigForStates( LauncherState fromState, LauncherState toState)258     protected StateAnimationConfig getConfigForStates(
259             LauncherState fromState, LauncherState toState) {
260         return new StateAnimationConfig();
261     }
262 
263     @Override
onDragEnd(float velocity)264     public void onDragEnd(float velocity) {
265         if (mCurrentAnimation == null) {
266             // Unlikely, but we may have been canceled just before onDragEnd(). We assume whoever
267             // canceled us will handle a new state transition to clean up.
268             return;
269         }
270 
271         boolean fling = mDetector.isFling(velocity);
272 
273         boolean blockedFling = fling && mFlingBlockCheck.isBlocked();
274         if (blockedFling) {
275             fling = false;
276         }
277 
278         final LauncherState targetState;
279         final float progress = mCurrentAnimation.getProgressFraction();
280         final float progressVelocity = velocity * mProgressMultiplier;
281         final float interpolatedProgress = mCurrentAnimation.getInterpolatedProgress();
282         if (fling) {
283             targetState =
284                     Float.compare(Math.signum(velocity), Math.signum(mProgressMultiplier)) == 0
285                             ? mToState : mFromState;
286             // snap to top or bottom using the release velocity
287         } else {
288             targetState =
289                     (interpolatedProgress > SUCCESS_TRANSITION_PROGRESS) ? mToState : mFromState;
290         }
291 
292         final float endProgress;
293         final float startProgress;
294         final long duration;
295         // Increase the duration if we prevented the fling, as we are going against a high velocity.
296         final int durationMultiplier = blockedFling && targetState == mFromState
297                 ? LauncherAnimUtils.blockedFlingDurationFactor(velocity) : 1;
298 
299         if (targetState == mToState) {
300             endProgress = 1;
301             if (progress >= 1) {
302                 duration = 0;
303                 startProgress = 1;
304             } else {
305                 startProgress = Utilities.boundToRange(progress
306                         + progressVelocity * getSingleFrameMs(mLauncher), 0f, 1f);
307                 duration = BaseSwipeDetector.calculateDuration(velocity,
308                         endProgress - Math.max(progress, 0)) * durationMultiplier;
309             }
310         } else {
311             // Let the state manager know that the animation didn't go to the target state,
312             // but don't cancel ourselves (we already clean up when the animation completes).
313             mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener);
314             mCurrentAnimation.dispatchOnCancel();
315 
316             endProgress = 0;
317             if (progress <= 0) {
318                 duration = 0;
319                 startProgress = 0;
320             } else {
321                 startProgress = Utilities.boundToRange(progress
322                         + progressVelocity * getSingleFrameMs(mLauncher), 0f, 1f);
323                 duration = BaseSwipeDetector.calculateDuration(velocity,
324                         Math.min(progress, 1) - endProgress) * durationMultiplier;
325             }
326         }
327         if (targetState != mStartState) {
328             logReachedState(targetState);
329         }
330         mCurrentAnimation.setEndAction(() -> onSwipeInteractionCompleted(targetState));
331         ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
332         anim.setFloatValues(startProgress, endProgress);
333         updateSwipeCompleteAnimation(anim, duration, targetState, velocity, fling);
334         mCurrentAnimation.dispatchOnStart();
335         if (targetState == LauncherState.ALL_APPS) {
336             if (mAllAppsOvershootStarted) {
337                 mLauncher.getAppsView().onRelease();
338                 mAllAppsOvershootStarted = false;
339             } else {
340                 mLauncher.getAppsView().addSpringFromFlingUpdateListener(anim, velocity, progress);
341             }
342         }
343         anim.start();
344     }
345 
updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration, LauncherState targetState, float velocity, boolean isFling)346     protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration,
347             LauncherState targetState, float velocity, boolean isFling) {
348         animator.setDuration(expectedDuration)
349                 .setInterpolator(scrollInterpolatorForVelocity(velocity));
350     }
351 
onSwipeInteractionCompleted(LauncherState targetState)352     protected void onSwipeInteractionCompleted(LauncherState targetState) {
353         onReachedFinalState(mToState);
354         clearState();
355         boolean shouldGoToTargetState = mGoingBetweenStates || (mToState != targetState);
356         if (shouldGoToTargetState) {
357             goToTargetState(targetState);
358         }
359     }
360 
goToTargetState(LauncherState targetState)361     protected void goToTargetState(LauncherState targetState) {
362         if (!mLauncher.isInState(targetState)) {
363             // If we're already in the target state, don't jump to it at the end of the animation in
364             // case the user started interacting with it before the animation finished.
365             mLauncher.getStateManager().goToState(targetState, false /* animated */);
366         }
367         mLauncher.getRootView().getSysUiScrim().createSysuiMultiplierAnim(
368                 1f).setDuration(0).start();
369     }
370 
logReachedState(LauncherState targetState)371     private void logReachedState(LauncherState targetState) {
372         // Transition complete. log the action
373         mLauncher.getStatsLogManager().logger()
374                 .withSrcState(mStartState.statsLogOrdinal)
375                 .withDstState(targetState.statsLogOrdinal)
376                 .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
377                         .setWorkspace(
378                                 LauncherAtom.WorkspaceContainer.newBuilder()
379                                         .setPageIndex(mLauncher.getWorkspace().getCurrentPage()))
380                         .build())
381                 .log(StatsLogManager.getLauncherAtomEvent(mStartState.statsLogOrdinal,
382                             targetState.statsLogOrdinal, mToState.ordinal > mFromState.ordinal
383                                     ? LAUNCHER_UNKNOWN_SWIPEUP
384                                     : LAUNCHER_UNKNOWN_SWIPEDOWN));
385     }
386 
clearState()387     protected void clearState() {
388         cancelAnimationControllers();
389         mGoingBetweenStates = true;
390         mDetector.finishedScrolling();
391         mDetector.setDetectableScrollConditions(0, false);
392     }
393 
cancelAnimationControllers()394     private void cancelAnimationControllers() {
395         mCurrentAnimation = null;
396     }
397 }
398