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