• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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.MIN_PROGRESS_TO_ALL_APPS;
19 import static com.android.launcher3.LauncherState.ALL_APPS;
20 import static com.android.launcher3.LauncherState.NORMAL;
21 import static com.android.launcher3.LauncherState.OVERVIEW;
22 import static com.android.launcher3.LauncherStateManager.ANIM_ALL;
23 import static com.android.launcher3.LauncherStateManager.ATOMIC_COMPONENT;
24 import static com.android.launcher3.LauncherStateManager.NON_ATOMIC_COMPONENT;
25 import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
26 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
27 
28 import android.animation.Animator;
29 import android.animation.AnimatorListenerAdapter;
30 import android.animation.AnimatorSet;
31 import android.animation.ValueAnimator;
32 import android.view.HapticFeedbackConstants;
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.LauncherStateManager.AnimationComponents;
39 import com.android.launcher3.LauncherStateManager.AnimationConfig;
40 import com.android.launcher3.LauncherStateManager.StateHandler;
41 import com.android.launcher3.Utilities;
42 import com.android.launcher3.anim.AnimationSuccessListener;
43 import com.android.launcher3.anim.AnimatorPlaybackController;
44 import com.android.launcher3.anim.AnimatorSetBuilder;
45 import com.android.launcher3.userevent.nano.LauncherLogProto;
46 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
47 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
48 import com.android.launcher3.util.FlingBlockCheck;
49 import com.android.launcher3.util.PendingAnimation;
50 import com.android.launcher3.util.TouchController;
51 
52 /**
53  * TouchController for handling state changes
54  */
55 public abstract class AbstractStateChangeTouchController
56         implements TouchController, SwipeDetector.Listener {
57 
58     private static final String TAG = "ASCTouchController";
59 
60     // Progress after which the transition is assumed to be a success in case user does not fling
61     public static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;
62 
63     /**
64      * Play an atomic recents animation when the progress from NORMAL to OVERVIEW reaches this.
65      */
66     public static final float ATOMIC_OVERVIEW_ANIM_THRESHOLD = 0.5f;
67     protected static final long ATOMIC_DURATION = 200;
68 
69     protected final Launcher mLauncher;
70     protected final SwipeDetector mDetector;
71 
72     private boolean mNoIntercept;
73     protected int mStartContainerType;
74 
75     protected LauncherState mStartState;
76     protected LauncherState mFromState;
77     protected LauncherState mToState;
78     protected AnimatorPlaybackController mCurrentAnimation;
79     protected PendingAnimation mPendingAnimation;
80 
81     private float mStartProgress;
82     // Ratio of transition process [0, 1] to drag displacement (px)
83     private float mProgressMultiplier;
84     private float mDisplacementShift;
85     private boolean mCanBlockFling;
86     private FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck();
87 
88     private AnimatorSet mAtomicAnim;
89     private boolean mPassedOverviewAtomicThreshold;
90     // mAtomicAnim plays the atomic components of the state animations when we pass the threshold.
91     // However, if we reinit to transition to a new state (e.g. OVERVIEW -> ALL_APPS) before the
92     // atomic animation finishes, we only control the non-atomic components so that we don't
93     // interfere with the atomic animation. When the atomic animation ends, we start controlling
94     // the atomic components as well, using this controller.
95     private AnimatorPlaybackController mAtomicComponentsController;
96     private float mAtomicComponentsStartProgress;
97 
AbstractStateChangeTouchController(Launcher l, SwipeDetector.Direction dir)98     public AbstractStateChangeTouchController(Launcher l, SwipeDetector.Direction dir) {
99         mLauncher = l;
100         mDetector = new SwipeDetector(l, this, dir);
101     }
102 
canInterceptTouch(MotionEvent ev)103     protected abstract boolean canInterceptTouch(MotionEvent ev);
104 
105     @Override
onControllerInterceptTouchEvent(MotionEvent ev)106     public final boolean onControllerInterceptTouchEvent(MotionEvent ev) {
107         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
108             mNoIntercept = !canInterceptTouch(ev);
109             if (mNoIntercept) {
110                 return false;
111             }
112 
113             // Now figure out which direction scroll events the controller will start
114             // calling the callbacks.
115             final int directionsToDetectScroll;
116             boolean ignoreSlopWhenSettling = false;
117 
118             if (mCurrentAnimation != null) {
119                 directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
120                 ignoreSlopWhenSettling = true;
121             } else {
122                 directionsToDetectScroll = getSwipeDirection();
123                 if (directionsToDetectScroll == 0) {
124                     mNoIntercept = true;
125                     return false;
126                 }
127             }
128             mDetector.setDetectableScrollConditions(
129                     directionsToDetectScroll, ignoreSlopWhenSettling);
130         }
131 
132         if (mNoIntercept) {
133             return false;
134         }
135 
136         onControllerTouchEvent(ev);
137         return mDetector.isDraggingOrSettling();
138     }
139 
getSwipeDirection()140     private int getSwipeDirection() {
141         LauncherState fromState = mLauncher.getStateManager().getState();
142         int swipeDirection = 0;
143         if (getTargetState(fromState, true /* isDragTowardPositive */) != fromState) {
144             swipeDirection |= SwipeDetector.DIRECTION_POSITIVE;
145         }
146         if (getTargetState(fromState, false /* isDragTowardPositive */) != fromState) {
147             swipeDirection |= SwipeDetector.DIRECTION_NEGATIVE;
148         }
149         return swipeDirection;
150     }
151 
152     @Override
onControllerTouchEvent(MotionEvent ev)153     public final boolean onControllerTouchEvent(MotionEvent ev) {
154         return mDetector.onTouchEvent(ev);
155     }
156 
getShiftRange()157     protected float getShiftRange() {
158         return mLauncher.getAllAppsController().getShiftRange();
159     }
160 
161     /**
162      * Returns the state to go to from fromState given the drag direction. If there is no state in
163      * that direction, returns fromState.
164      */
getTargetState(LauncherState fromState, boolean isDragTowardPositive)165     protected abstract LauncherState getTargetState(LauncherState fromState,
166             boolean isDragTowardPositive);
167 
initCurrentAnimation(@nimationComponents int animComponents)168     protected abstract float initCurrentAnimation(@AnimationComponents int animComponents);
169 
170     /**
171      * Returns the container that the touch started from when leaving NORMAL state.
172      */
getLogContainerTypeForNormalState()173     protected abstract int getLogContainerTypeForNormalState();
174 
reinitCurrentAnimation(boolean reachedToState, boolean isDragTowardPositive)175     private boolean reinitCurrentAnimation(boolean reachedToState, boolean isDragTowardPositive) {
176         LauncherState newFromState = mFromState == null ? mLauncher.getStateManager().getState()
177                 : reachedToState ? mToState : mFromState;
178         LauncherState newToState = getTargetState(newFromState, isDragTowardPositive);
179 
180         if (newFromState == mFromState && newToState == mToState || (newFromState == newToState)) {
181             return false;
182         }
183 
184         mFromState = newFromState;
185         mToState = newToState;
186 
187         mStartProgress = 0;
188         mPassedOverviewAtomicThreshold = false;
189         if (mCurrentAnimation != null) {
190             mCurrentAnimation.setOnCancelRunnable(null);
191         }
192         int animComponents = goingBetweenNormalAndOverview(mFromState, mToState)
193                 ? NON_ATOMIC_COMPONENT : ANIM_ALL;
194         if (mAtomicAnim != null) {
195             // Control the non-atomic components until the atomic animation finishes, then control
196             // the atomic components as well.
197             animComponents = NON_ATOMIC_COMPONENT;
198             mAtomicAnim.addListener(new AnimationSuccessListener() {
199                 @Override
200                 public void onAnimationSuccess(Animator animation) {
201                     cancelAtomicComponentsController();
202                     if (mCurrentAnimation != null) {
203                         mAtomicComponentsStartProgress = mCurrentAnimation.getProgressFraction();
204                         long duration = (long) (getShiftRange() * 2);
205                         mAtomicComponentsController = AnimatorPlaybackController.wrap(
206                                 createAtomicAnimForState(mFromState, mToState, duration), duration);
207                         mAtomicComponentsController.dispatchOnStart();
208                     }
209                 }
210             });
211         }
212         if (goingBetweenNormalAndOverview(mFromState, mToState)) {
213             cancelAtomicComponentsController();
214         }
215         mProgressMultiplier = initCurrentAnimation(animComponents);
216         mCurrentAnimation.dispatchOnStart();
217         return true;
218     }
219 
goingBetweenNormalAndOverview(LauncherState fromState, LauncherState toState)220     private boolean goingBetweenNormalAndOverview(LauncherState fromState, LauncherState toState) {
221         return (fromState == NORMAL || fromState == OVERVIEW)
222                 && (toState == NORMAL || toState == OVERVIEW)
223                 && mPendingAnimation == null;
224     }
225 
226     @Override
onDragStart(boolean start)227     public void onDragStart(boolean start) {
228         mStartState = mLauncher.getStateManager().getState();
229         if (mStartState == ALL_APPS) {
230             mStartContainerType = LauncherLogProto.ContainerType.ALLAPPS;
231         } else if (mStartState == NORMAL) {
232             mStartContainerType = getLogContainerTypeForNormalState();
233         } else if (mStartState   == OVERVIEW){
234             mStartContainerType = LauncherLogProto.ContainerType.TASKSWITCHER;
235         }
236         if (mCurrentAnimation == null) {
237             mFromState = mStartState;
238             mToState = null;
239             mAtomicComponentsController = null;
240             reinitCurrentAnimation(false, mDetector.wasInitialTouchPositive());
241             mDisplacementShift = 0;
242         } else {
243             mCurrentAnimation.pause();
244             mStartProgress = mCurrentAnimation.getProgressFraction();
245         }
246         mCanBlockFling = mFromState == NORMAL;
247         mFlingBlockCheck.unblockFling();
248     }
249 
250     @Override
onDrag(float displacement, float velocity)251     public boolean onDrag(float displacement, float velocity) {
252         float deltaProgress = mProgressMultiplier * (displacement - mDisplacementShift);
253         float progress = deltaProgress + mStartProgress;
254         updateProgress(progress);
255         boolean isDragTowardPositive = (displacement - mDisplacementShift) < 0;
256         if (progress <= 0) {
257             if (reinitCurrentAnimation(false, isDragTowardPositive)) {
258                 mDisplacementShift = displacement;
259                 if (mCanBlockFling) {
260                     mFlingBlockCheck.blockFling();
261                 }
262             }
263         } else if (progress >= 1) {
264             if (reinitCurrentAnimation(true, isDragTowardPositive)) {
265                 mDisplacementShift = displacement;
266                 if (mCanBlockFling) {
267                     mFlingBlockCheck.blockFling();
268                 }
269             }
270         } else {
271             mFlingBlockCheck.onEvent();
272         }
273 
274         return true;
275     }
276 
277     protected void updateProgress(float fraction) {
278         mCurrentAnimation.setPlayFraction(fraction);
279         if (mAtomicComponentsController != null) {
280             mAtomicComponentsController.setPlayFraction(fraction - mAtomicComponentsStartProgress);
281         }
282         maybeUpdateAtomicAnim(mFromState, mToState, fraction);
283     }
284 
285     /**
286      * When going between normal and overview states, see if we passed the overview threshold and
287      * play the appropriate atomic animation if so.
288      */
289     private void maybeUpdateAtomicAnim(LauncherState fromState, LauncherState toState,
290             float progress) {
291         if (!goingBetweenNormalAndOverview(fromState, toState)) {
292             return;
293         }
294         float threshold = toState == OVERVIEW ? ATOMIC_OVERVIEW_ANIM_THRESHOLD
295                 : 1f - ATOMIC_OVERVIEW_ANIM_THRESHOLD;
296         boolean passedThreshold = progress >= threshold;
297         if (passedThreshold != mPassedOverviewAtomicThreshold) {
298             LauncherState atomicFromState = passedThreshold ? fromState: toState;
299             LauncherState atomicToState = passedThreshold ? toState : fromState;
300             mPassedOverviewAtomicThreshold = passedThreshold;
301             if (mAtomicAnim != null) {
302                 mAtomicAnim.cancel();
303             }
304             mAtomicAnim = createAtomicAnimForState(atomicFromState, atomicToState, ATOMIC_DURATION);
305             mAtomicAnim.addListener(new AnimatorListenerAdapter() {
306                 @Override
307                 public void onAnimationEnd(Animator animation) {
308                     mAtomicAnim = null;
309                 }
310             });
311             mAtomicAnim.start();
312             mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
313         }
314     }
315 
createAtomicAnimForState(LauncherState fromState, LauncherState targetState, long duration)316     private AnimatorSet createAtomicAnimForState(LauncherState fromState, LauncherState targetState,
317             long duration) {
318         AnimatorSetBuilder builder = new AnimatorSetBuilder();
319         mLauncher.getStateManager().prepareForAtomicAnimation(fromState, targetState, builder);
320         AnimationConfig config = new AnimationConfig();
321         config.animComponents = ATOMIC_COMPONENT;
322         config.duration = duration;
323         for (StateHandler handler : mLauncher.getStateManager().getStateHandlers()) {
324             handler.setStateWithAnimation(targetState, builder, config);
325         }
326         return builder.build();
327     }
328 
329     @Override
onDragEnd(float velocity, boolean fling)330     public void onDragEnd(float velocity, boolean fling) {
331         final int logAction = fling ? Touch.FLING : Touch.SWIPE;
332 
333         boolean blockedFling = fling && mFlingBlockCheck.isBlocked();
334         if (blockedFling) {
335             fling = false;
336         }
337 
338         final LauncherState targetState;
339         final float progress = mCurrentAnimation.getProgressFraction();
340         if (fling) {
341             targetState =
342                     Float.compare(Math.signum(velocity), Math.signum(mProgressMultiplier)) == 0
343                             ? mToState : mFromState;
344             // snap to top or bottom using the release velocity
345         } else {
346             float successProgress = mToState == ALL_APPS
347                     ? MIN_PROGRESS_TO_ALL_APPS : SUCCESS_TRANSITION_PROGRESS;
348             targetState = (progress > successProgress) ? mToState : mFromState;
349         }
350 
351         final float endProgress;
352         final float startProgress;
353         final long duration;
354         // Increase the duration if we prevented the fling, as we are going against a high velocity.
355         final int durationMultiplier = blockedFling && targetState == mFromState
356                 ? LauncherAnimUtils.blockedFlingDurationFactor(velocity) : 1;
357 
358         if (targetState == mToState) {
359             endProgress = 1;
360             if (progress >= 1) {
361                 duration = 0;
362                 startProgress = 1;
363             } else {
364                 startProgress = Utilities.boundToRange(
365                         progress + velocity * SINGLE_FRAME_MS * mProgressMultiplier, 0f, 1f);
366                 duration = SwipeDetector.calculateDuration(velocity,
367                         endProgress - Math.max(progress, 0)) * durationMultiplier;
368             }
369         } else {
370             // Let the state manager know that the animation didn't go to the target state,
371             // but don't cancel ourselves (we already clean up when the animation completes).
372             Runnable onCancel = mCurrentAnimation.getOnCancelRunnable();
373             mCurrentAnimation.setOnCancelRunnable(null);
374             mCurrentAnimation.dispatchOnCancel();
375             mCurrentAnimation.setOnCancelRunnable(onCancel);
376 
377             endProgress = 0;
378             if (progress <= 0) {
379                 duration = 0;
380                 startProgress = 0;
381             } else {
382                 startProgress = Utilities.boundToRange(
383                         progress + velocity * SINGLE_FRAME_MS * mProgressMultiplier, 0f, 1f);
384                 duration = SwipeDetector.calculateDuration(velocity,
385                         Math.min(progress, 1) - endProgress) * durationMultiplier;
386             }
387         }
388 
389         mCurrentAnimation.setEndAction(() -> onSwipeInteractionCompleted(targetState, logAction));
390         ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
391         anim.setFloatValues(startProgress, endProgress);
392         maybeUpdateAtomicAnim(mFromState, targetState, targetState == mToState ? 1f : 0f);
393         updateSwipeCompleteAnimation(anim, Math.max(duration, getRemainingAtomicDuration()),
394                 targetState, velocity, fling);
395         mCurrentAnimation.dispatchOnStart();
396         if (fling && targetState == LauncherState.ALL_APPS) {
397             mLauncher.getAppsView().addSpringFromFlingUpdateListener(anim, velocity);
398         }
399         anim.start();
400         if (mAtomicAnim == null) {
401             startAtomicComponentsAnim(endProgress, anim.getDuration());
402         } else {
403             mAtomicAnim.addListener(new AnimationSuccessListener() {
404                 @Override
405                 public void onAnimationSuccess(Animator animator) {
406                     startAtomicComponentsAnim(endProgress, anim.getDuration());
407                 }
408             });
409         }
410     }
411 
412     /**
413      * Animates the atomic components from the current progress to the final progress.
414      *
415      * Note that this only applies when we are controlling the atomic components separately from
416      * the non-atomic components, which only happens if we reinit before the atomic animation
417      * finishes.
418      */
startAtomicComponentsAnim(float toProgress, long duration)419     private void startAtomicComponentsAnim(float toProgress, long duration) {
420         if (mAtomicComponentsController != null) {
421             ValueAnimator atomicAnim = mAtomicComponentsController.getAnimationPlayer();
422             atomicAnim.setFloatValues(mAtomicComponentsController.getProgressFraction(), toProgress);
423             atomicAnim.setDuration(duration);
424             atomicAnim.start();
425             atomicAnim.addListener(new AnimatorListenerAdapter() {
426                 @Override
427                 public void onAnimationEnd(Animator animation) {
428                     mAtomicComponentsController = null;
429                 }
430             });
431         }
432     }
433 
getRemainingAtomicDuration()434     private long getRemainingAtomicDuration() {
435         if (mAtomicAnim == null) {
436             return 0;
437         }
438         if (Utilities.ATLEAST_OREO) {
439             return mAtomicAnim.getTotalDuration() - mAtomicAnim.getCurrentPlayTime();
440         } else {
441             long remainingDuration = 0;
442             for (Animator anim : mAtomicAnim.getChildAnimations()) {
443                 remainingDuration = Math.max(remainingDuration, anim.getDuration());
444             }
445             return remainingDuration;
446         }
447     }
448 
updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration, LauncherState targetState, float velocity, boolean isFling)449     protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration,
450             LauncherState targetState, float velocity, boolean isFling) {
451         animator.setDuration(expectedDuration)
452                 .setInterpolator(scrollInterpolatorForVelocity(velocity));
453     }
454 
getDirectionForLog()455     protected int getDirectionForLog() {
456         return mToState.ordinal > mFromState.ordinal ? Direction.UP : Direction.DOWN;
457     }
458 
onSwipeInteractionCompleted(LauncherState targetState, int logAction)459     protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
460         clearState();
461         boolean shouldGoToTargetState = true;
462         if (mPendingAnimation != null) {
463             boolean reachedTarget = mToState == targetState;
464             mPendingAnimation.finish(reachedTarget, logAction);
465             mPendingAnimation = null;
466             shouldGoToTargetState = !reachedTarget;
467         }
468         if (shouldGoToTargetState) {
469             if (targetState != mStartState) {
470                 logReachedState(logAction, targetState);
471             }
472             mLauncher.getStateManager().goToState(targetState, false /* animated */);
473         }
474     }
475 
logReachedState(int logAction, LauncherState targetState)476     private void logReachedState(int logAction, LauncherState targetState) {
477         // Transition complete. log the action
478         mLauncher.getUserEventDispatcher().logStateChangeAction(logAction,
479                 getDirectionForLog(),
480                 mStartContainerType,
481                 mStartState.containerType,
482                 targetState.containerType,
483                 mLauncher.getWorkspace().getCurrentPage());
484     }
485 
clearState()486     protected void clearState() {
487         mCurrentAnimation = null;
488         cancelAtomicComponentsController();
489         mDetector.finishedScrolling();
490         mDetector.setDetectableScrollConditions(0, false);
491     }
492 
cancelAtomicComponentsController()493     private void cancelAtomicComponentsController() {
494         if (mAtomicComponentsController != null) {
495             mAtomicComponentsController.getAnimationPlayer().cancel();
496             mAtomicComponentsController = null;
497         }
498     }
499 }
500