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