• 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.uioverrides.touchcontrollers;
17 
18 import static android.view.MotionEvent.ACTION_DOWN;
19 
20 import static com.android.app.animation.Interpolators.ACCELERATE_0_75;
21 import static com.android.app.animation.Interpolators.DECELERATE_3;
22 import static com.android.app.animation.Interpolators.LINEAR;
23 import static com.android.app.animation.Interpolators.scrollInterpolatorForVelocity;
24 import static com.android.launcher3.LauncherAnimUtils.newCancelListener;
25 import static com.android.launcher3.LauncherState.NORMAL;
26 import static com.android.launcher3.LauncherState.OVERVIEW;
27 import static com.android.launcher3.LauncherState.OVERVIEW_ACTIONS;
28 import static com.android.launcher3.LauncherState.QUICK_SWITCH_FROM_HOME;
29 import static com.android.launcher3.MotionEventsUtils.isTrackpadFourFingerSwipe;
30 import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent;
31 import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
32 import static com.android.launcher3.anim.AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD;
33 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
34 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
35 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT;
36 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEDOWN;
37 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEUP;
38 import static com.android.launcher3.logging.StatsLogManager.getLauncherAtomEvent;
39 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
40 import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH;
41 import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
42 import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
43 import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_SCALE;
44 import static com.android.launcher3.states.StateAnimationConfig.SKIP_ALL_ANIMATIONS;
45 import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
46 import static com.android.launcher3.states.StateAnimationConfig.SKIP_SCRIM;
47 import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_RIGHT;
48 import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_UP;
49 import static com.android.launcher3.util.NavigationMode.THREE_BUTTONS;
50 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
51 import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs;
52 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
53 import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
54 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
55 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
56 import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
57 import static com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA;
58 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
59 
60 import android.animation.Animator;
61 import android.animation.Animator.AnimatorListener;
62 import android.animation.AnimatorListenerAdapter;
63 import android.animation.ValueAnimator;
64 import android.graphics.PointF;
65 import android.view.MotionEvent;
66 import android.view.animation.Interpolator;
67 
68 import com.android.launcher3.LauncherState;
69 import com.android.launcher3.R;
70 import com.android.launcher3.Utilities;
71 import com.android.launcher3.anim.AnimatedFloat;
72 import com.android.launcher3.anim.AnimatorPlaybackController;
73 import com.android.launcher3.anim.PendingAnimation;
74 import com.android.launcher3.states.StateAnimationConfig;
75 import com.android.launcher3.touch.BaseSwipeDetector;
76 import com.android.launcher3.touch.BothAxesSwipeDetector;
77 import com.android.launcher3.uioverrides.QuickstepLauncher;
78 import com.android.launcher3.util.DisplayController;
79 import com.android.launcher3.util.TouchController;
80 import com.android.launcher3.util.VibratorWrapper;
81 import com.android.quickstep.SystemUiProxy;
82 import com.android.quickstep.util.AnimatorControllerWithResistance;
83 import com.android.quickstep.util.LayoutUtils;
84 import com.android.quickstep.util.MotionPauseDetector;
85 import com.android.quickstep.util.WorkspaceRevealAnim;
86 import com.android.quickstep.views.DesktopTaskView;
87 import com.android.quickstep.views.LauncherRecentsView;
88 import com.android.quickstep.views.RecentsView;
89 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
90 
91 /**
92  * Handles quick switching to a recent task from the home screen. To give as much flexibility to
93  * the user as possible, also handles swipe up and hold to go to overview and swiping back home.
94  */
95 public class NoButtonQuickSwitchTouchController implements TouchController,
96         BothAxesSwipeDetector.Listener {
97 
98     private static final float Y_ANIM_MIN_PROGRESS = 0.25f;
99     private static final Interpolator FADE_OUT_INTERPOLATOR = DECELERATE_3;
100     private static final Interpolator TRANSLATE_OUT_INTERPOLATOR = ACCELERATE_0_75;
101     private static final Interpolator SCALE_DOWN_INTERPOLATOR = LINEAR;
102     private static final long ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW = 300;
103 
104     private final QuickstepLauncher mLauncher;
105     private final BothAxesSwipeDetector mSwipeDetector;
106     private final float mXRange;
107     private final float mYRange;
108     private final float mMaxYProgress;
109     private final MotionPauseDetector mMotionPauseDetector;
110     private final float mMotionPauseMinDisplacement;
111     private final LauncherRecentsView mRecentsView;
112     protected final AnimatorListener mClearStateOnCancelListener =
113             newCancelListener(this::clearState);
114 
115     private boolean mNoIntercept;
116     private LauncherState mStartState;
117 
118     private boolean mIsHomeScreenVisible = true;
119 
120     // As we drag, we control 3 animations: one to get non-overview components out of the way,
121     // and the other two to set overview properties based on x and y progress.
122     private AnimatorPlaybackController mNonOverviewAnim;
123     private AnimatorPlaybackController mXOverviewAnim;
124     private AnimatedFloat mYOverviewAnim;
125 
NoButtonQuickSwitchTouchController(QuickstepLauncher launcher)126     public NoButtonQuickSwitchTouchController(QuickstepLauncher launcher) {
127         mLauncher = launcher;
128         mSwipeDetector = new BothAxesSwipeDetector(mLauncher, this);
129         mRecentsView = mLauncher.getOverviewPanel();
130         mXRange = mLauncher.getDeviceProfile().widthPx / 2f;
131         mYRange = LayoutUtils.getShelfTrackingDistance(
132             mLauncher, mLauncher.getDeviceProfile(), mRecentsView.getPagedOrientationHandler());
133         mMaxYProgress = mLauncher.getDeviceProfile().heightPx / mYRange;
134         mMotionPauseDetector = new MotionPauseDetector(mLauncher);
135         mMotionPauseMinDisplacement = mLauncher.getResources().getDimension(
136                 R.dimen.motion_pause_detector_min_displacement_from_app);
137     }
138 
139     @Override
onControllerInterceptTouchEvent(MotionEvent ev)140     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
141         if (ev.getActionMasked() == ACTION_DOWN) {
142             mNoIntercept = !canInterceptTouch(ev);
143             if (mNoIntercept) {
144                 return false;
145             }
146 
147             // Only detect horizontal swipe for intercept, then we will allow swipe up as well.
148             mSwipeDetector.setDetectableScrollConditions(DIRECTION_RIGHT,
149                     false /* ignoreSlopWhenSettling */);
150         }
151 
152         if (mNoIntercept) {
153             return false;
154         }
155 
156         onControllerTouchEvent(ev);
157         return mSwipeDetector.isDraggingOrSettling();
158     }
159 
160     @Override
onControllerTouchEvent(MotionEvent ev)161     public boolean onControllerTouchEvent(MotionEvent ev) {
162         return mSwipeDetector.onTouchEvent(ev);
163     }
164 
canInterceptTouch(MotionEvent ev)165     private boolean canInterceptTouch(MotionEvent ev) {
166         if (!isTrackpadMotionEvent(ev) && DisplayController.getNavigationMode(mLauncher)
167                 == THREE_BUTTONS) {
168             return false;
169         }
170         if (!mLauncher.isInState(LauncherState.NORMAL)) {
171             return false;
172         }
173         if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) == 0) {
174             return false;
175         }
176         int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
177         if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) {
178             return false;
179         }
180         if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) {
181             // TODO(b/268075592): add support for quickswitch to/from desktop
182             return false;
183         }
184         if (isTrackpadMultiFingerSwipe(ev)) {
185             return isTrackpadFourFingerSwipe(ev);
186         }
187         return true;
188     }
189 
190     @Override
onDragStart(boolean start)191     public void onDragStart(boolean start) {
192         mMotionPauseDetector.clear();
193         if (start) {
194             InteractionJankMonitorWrapper.begin(mRecentsView,
195                     InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
196 
197             mStartState = mLauncher.getStateManager().getState();
198 
199             mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseDetected);
200 
201             // We have detected horizontal drag start, now allow swipe up as well.
202             mSwipeDetector.setDetectableScrollConditions(DIRECTION_RIGHT | DIRECTION_UP,
203                     false /* ignoreSlopWhenSettling */);
204 
205             setupAnimators();
206         }
207     }
208 
onMotionPauseDetected()209     private void onMotionPauseDetected() {
210         VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
211     }
212 
setupAnimators()213     private void setupAnimators() {
214         // Animate the non-overview components (e.g. workspace, shelf) out of the way.
215         StateAnimationConfig nonOverviewBuilder = new StateAnimationConfig();
216         nonOverviewBuilder.setInterpolator(ANIM_WORKSPACE_FADE, FADE_OUT_INTERPOLATOR);
217         nonOverviewBuilder.setInterpolator(ANIM_ALL_APPS_FADE, FADE_OUT_INTERPOLATOR);
218         nonOverviewBuilder.setInterpolator(ANIM_WORKSPACE_SCALE, FADE_OUT_INTERPOLATOR);
219         nonOverviewBuilder.setInterpolator(ANIM_DEPTH, FADE_OUT_INTERPOLATOR);
220         nonOverviewBuilder.setInterpolator(ANIM_VERTICAL_PROGRESS, TRANSLATE_OUT_INTERPOLATOR);
221         updateNonOverviewAnim(QUICK_SWITCH_FROM_HOME, nonOverviewBuilder);
222         mNonOverviewAnim.dispatchOnStart();
223 
224         if (mRecentsView.getTaskViewCount() == 0) {
225             mRecentsView.setOnEmptyMessageUpdatedListener(isEmpty -> {
226                 if (!isEmpty && mSwipeDetector.isDraggingState()) {
227                     // We have loaded tasks, update the animators to start at the correct scale etc.
228                     setupOverviewAnimators();
229                 }
230             });
231         }
232 
233         setupOverviewAnimators();
234     }
235 
236     /** Create state animation to control non-overview components. */
updateNonOverviewAnim(LauncherState toState, StateAnimationConfig config)237     private void updateNonOverviewAnim(LauncherState toState, StateAnimationConfig config) {
238         config.duration = (long) (Math.max(mXRange, mYRange) * 2);
239         config.animFlags |= SKIP_OVERVIEW | SKIP_SCRIM;
240         mNonOverviewAnim = mLauncher.getStateManager()
241                 .createAnimationToNewWorkspace(toState, config);
242         mNonOverviewAnim.getTarget().addListener(mClearStateOnCancelListener);
243     }
244 
setupOverviewAnimators()245     private void setupOverviewAnimators() {
246         final LauncherState fromState = QUICK_SWITCH_FROM_HOME;
247         final LauncherState toState = OVERVIEW;
248 
249         // Set RecentView's initial properties.
250         RECENTS_SCALE_PROPERTY.set(mRecentsView, fromState.getOverviewScaleAndOffset(mLauncher)[0]);
251         ADJACENT_PAGE_HORIZONTAL_OFFSET.set(mRecentsView, 1f);
252         TASK_THUMBNAIL_SPLASH_ALPHA.set(mRecentsView, fromState.showTaskThumbnailSplash() ? 1f : 0);
253         mRecentsView.setContentAlpha(1);
254         mRecentsView.setFullscreenProgress(fromState.getOverviewFullscreenProgress());
255         mLauncher.getActionsView().getVisibilityAlpha().setValue(
256                 (fromState.getVisibleElements(mLauncher) & OVERVIEW_ACTIONS) != 0 ? 1f : 0f);
257         mRecentsView.setTaskIconScaledDown(true);
258 
259         float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
260         // As we drag right, animate the following properties:
261         //   - RecentsView translationX
262         //   - OverviewScrim
263         //   - RecentsView fade (if it's empty)
264         PendingAnimation xAnim = new PendingAnimation((long) (mXRange * 2));
265         xAnim.setFloat(mRecentsView, ADJACENT_PAGE_HORIZONTAL_OFFSET, scaleAndOffset[1], LINEAR);
266         // Use QuickSwitchState instead of OverviewState to determine scrim color,
267         // since we need to take potential taskbar into account.
268         xAnim.setViewBackgroundColor(mLauncher.getScrimView(),
269                 QUICK_SWITCH_FROM_HOME.getWorkspaceScrimColor(mLauncher), LINEAR);
270         if (mRecentsView.getTaskViewCount() == 0) {
271             xAnim.addFloat(mRecentsView, CONTENT_ALPHA, 0f, 1f, LINEAR);
272         }
273         mXOverviewAnim = xAnim.createPlaybackController();
274         mXOverviewAnim.dispatchOnStart();
275 
276         // As we drag up, animate the following properties:
277         //   - RecentsView scale
278         //   - RecentsView fullscreenProgress
279         PendingAnimation yAnim = new PendingAnimation((long) (mYRange * 2));
280         yAnim.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
281                 SCALE_DOWN_INTERPOLATOR);
282         yAnim.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
283                 toState.getOverviewFullscreenProgress(), SCALE_DOWN_INTERPOLATOR);
284         AnimatorPlaybackController yNormalController = yAnim.createPlaybackController();
285         AnimatorControllerWithResistance yAnimWithResistance = AnimatorControllerWithResistance
286                 .createForRecents(yNormalController, mLauncher,
287                         mRecentsView.getPagedViewOrientedState(), mLauncher.getDeviceProfile(),
288                         mRecentsView, RECENTS_SCALE_PROPERTY, mRecentsView,
289                         TASK_SECONDARY_TRANSLATION);
290         mYOverviewAnim = new AnimatedFloat(() -> {
291             if (mYOverviewAnim != null) {
292                 yAnimWithResistance.setProgress(mYOverviewAnim.value, mMaxYProgress);
293             }
294         });
295         yNormalController.dispatchOnStart();
296     }
297 
298     @Override
onDrag(PointF displacement, MotionEvent ev)299     public boolean onDrag(PointF displacement, MotionEvent ev) {
300         float xProgress = Math.max(0, displacement.x) / mXRange;
301         float yProgress = Math.max(0, -displacement.y) / mYRange;
302         yProgress = Utilities.mapRange(yProgress, Y_ANIM_MIN_PROGRESS, 1f);
303 
304         boolean wasHomeScreenVisible = mIsHomeScreenVisible;
305         if (wasHomeScreenVisible && mNonOverviewAnim != null) {
306             mNonOverviewAnim.setPlayFraction(xProgress);
307         }
308         mIsHomeScreenVisible = FADE_OUT_INTERPOLATOR.getInterpolation(xProgress)
309                 <= 1 - ALPHA_CUTOFF_THRESHOLD;
310 
311         mMotionPauseDetector.setDisallowPause(-displacement.y < mMotionPauseMinDisplacement);
312         mMotionPauseDetector.addPosition(ev);
313 
314         if (mXOverviewAnim != null) {
315             mXOverviewAnim.setPlayFraction(xProgress);
316         }
317         if (mYOverviewAnim != null) {
318             mYOverviewAnim.updateValue(yProgress);
319         }
320         return true;
321     }
322 
323     @Override
onDragEnd(PointF velocity)324     public void onDragEnd(PointF velocity) {
325         boolean horizontalFling = mSwipeDetector.isFling(velocity.x);
326         boolean verticalFling = mSwipeDetector.isFling(velocity.y);
327         boolean noFling = !horizontalFling && !verticalFling;
328         if (mMotionPauseDetector.isPaused() && noFling) {
329             // Going to Overview.
330             cancelAnimations();
331             InteractionJankMonitorWrapper.cancel(InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
332 
333             StateAnimationConfig config = new StateAnimationConfig();
334             config.duration = ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW;
335             Animator overviewAnim = mLauncher.getStateManager().createAtomicAnimation(
336                     mStartState, OVERVIEW, config);
337             overviewAnim.addListener(new AnimatorListenerAdapter() {
338                 @Override
339                 public void onAnimationEnd(Animator animation) {
340                     onAnimationToStateCompleted(OVERVIEW);
341                     // Animate the icon after onAnimationToStateCompleted() so it doesn't clobber.
342                     mRecentsView.animateUpTaskIconScale();
343                 }
344             });
345             overviewAnim.start();
346 
347             // Create an empty state transition so StateListeners get onStateTransitionStart().
348             mLauncher.getStateManager().createAnimationToNewWorkspace(
349                     OVERVIEW, config.duration, StateAnimationConfig.SKIP_ALL_ANIMATIONS)
350                     .dispatchOnStart();
351             return;
352         }
353 
354         final LauncherState targetState;
355         if (horizontalFling && verticalFling) {
356             if (velocity.x < 0) {
357                 // Flinging left and up or down both go back home.
358                 targetState = NORMAL;
359             } else {
360                 if (velocity.y > 0) {
361                     // Flinging right and down goes to quick switch.
362                     targetState = QUICK_SWITCH_FROM_HOME;
363                 } else {
364                     // Flinging up and right could go either home or to quick switch.
365                     // Determine the target based on the higher velocity.
366                     targetState = Math.abs(velocity.x) > Math.abs(velocity.y)
367                         ? QUICK_SWITCH_FROM_HOME : NORMAL;
368                 }
369             }
370         } else if (horizontalFling) {
371             targetState = velocity.x > 0 ? QUICK_SWITCH_FROM_HOME : NORMAL;
372         } else if (verticalFling) {
373             targetState = velocity.y > 0 ? QUICK_SWITCH_FROM_HOME : NORMAL;
374         } else {
375             // If user isn't flinging, just snap to the closest state.
376             boolean passedHorizontalThreshold = mXOverviewAnim.getInterpolatedProgress() > 0.5f;
377             boolean passedVerticalThreshold = mYOverviewAnim.value > 1f;
378             targetState = passedHorizontalThreshold && !passedVerticalThreshold
379                     ? QUICK_SWITCH_FROM_HOME : NORMAL;
380         }
381 
382         // Animate the various components to the target state.
383 
384         float xProgress = mXOverviewAnim.getProgressFraction();
385         float startXProgress = Utilities.boundToRange(xProgress
386                 + velocity.x * getSingleFrameMs(mLauncher) / mXRange, 0f, 1f);
387         final float endXProgress = targetState == NORMAL ? 0 : 1;
388         long xDuration = BaseSwipeDetector.calculateDuration(velocity.x,
389                 Math.abs(endXProgress - startXProgress));
390         ValueAnimator xOverviewAnim = mXOverviewAnim.getAnimationPlayer();
391         xOverviewAnim.setFloatValues(startXProgress, endXProgress);
392         xOverviewAnim.setDuration(xDuration)
393                 .setInterpolator(scrollInterpolatorForVelocity(velocity.x));
394         mXOverviewAnim.dispatchOnStart();
395 
396         boolean flingUpToNormal = verticalFling && velocity.y < 0 && targetState == NORMAL;
397 
398         float yProgress = mYOverviewAnim.value;
399         float startYProgress = Utilities.boundToRange(yProgress
400                 - velocity.y * getSingleFrameMs(mLauncher) / mYRange, 0f, mMaxYProgress);
401         final float endYProgress;
402         if (flingUpToNormal) {
403             endYProgress = 1;
404         } else if (targetState == NORMAL) {
405             // Keep overview at its current scale/translationY as it slides off the screen.
406             endYProgress = startYProgress;
407         } else {
408             endYProgress = 0;
409         }
410         float yDistanceToCover = Math.abs(endYProgress - startYProgress) * mYRange;
411         long yDuration = (long) (yDistanceToCover / Math.max(1f, Math.abs(velocity.y)));
412         ValueAnimator yOverviewAnim = mYOverviewAnim.animateToValue(startYProgress, endYProgress);
413         yOverviewAnim.setDuration(yDuration);
414         mYOverviewAnim.updateValue(startYProgress);
415 
416         ValueAnimator nonOverviewAnim = mNonOverviewAnim.getAnimationPlayer();
417         if (flingUpToNormal && !mIsHomeScreenVisible) {
418             // We are flinging to home while workspace is invisible, run the same staggered
419             // animation as from an app.
420             StateAnimationConfig config = new StateAnimationConfig();
421             // Update mNonOverviewAnim to do nothing so it doesn't interfere.
422             config.animFlags = SKIP_ALL_ANIMATIONS;
423             updateNonOverviewAnim(targetState, config);
424             nonOverviewAnim = mNonOverviewAnim.getAnimationPlayer();
425             mNonOverviewAnim.dispatchOnStart();
426 
427             new WorkspaceRevealAnim(mLauncher, false /* animateOverviewScrim */).start();
428         } else {
429             boolean canceled = targetState == NORMAL;
430             if (canceled) {
431                 // Let the state manager know that the animation didn't go to the target state,
432                 // but don't clean up yet (we already clean up when the animation completes).
433                 mNonOverviewAnim.getTarget().removeListener(mClearStateOnCancelListener);
434                 mNonOverviewAnim.dispatchOnCancel();
435             }
436             float startProgress = mNonOverviewAnim.getProgressFraction();
437             float endProgress = canceled ? 0 : 1;
438             nonOverviewAnim.setFloatValues(startProgress, endProgress);
439             mNonOverviewAnim.dispatchOnStart();
440         }
441         if (targetState == QUICK_SWITCH_FROM_HOME) {
442             // Navigating to quick switch, add scroll feedback since the first time is not
443             // considered a scroll by the RecentsView.
444             VibratorWrapper.INSTANCE.get(mLauncher).vibrate(
445                     RecentsView.SCROLL_VIBRATION_PRIMITIVE,
446                     RecentsView.SCROLL_VIBRATION_PRIMITIVE_SCALE,
447                     RecentsView.SCROLL_VIBRATION_FALLBACK);
448         } else {
449             InteractionJankMonitorWrapper.cancel(InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
450         }
451 
452         nonOverviewAnim.setDuration(Math.max(xDuration, yDuration));
453         mNonOverviewAnim.setEndAction(() -> onAnimationToStateCompleted(targetState));
454 
455         cancelAnimations();
456         xOverviewAnim.start();
457         yOverviewAnim.start();
458         nonOverviewAnim.start();
459     }
460 
onAnimationToStateCompleted(LauncherState targetState)461     private void onAnimationToStateCompleted(LauncherState targetState) {
462         mLauncher.getStatsLogManager().logger()
463                 .withSrcState(LAUNCHER_STATE_HOME)
464                 .withDstState(targetState.statsLogOrdinal)
465                 .log(getLauncherAtomEvent(mStartState.statsLogOrdinal, targetState.statsLogOrdinal,
466                         targetState == QUICK_SWITCH_FROM_HOME
467                                 ? LAUNCHER_QUICKSWITCH_RIGHT
468                                 : targetState.ordinal > mStartState.ordinal
469                                         ? LAUNCHER_UNKNOWN_SWIPEUP
470                                         : LAUNCHER_UNKNOWN_SWIPEDOWN));
471 
472         if (targetState == QUICK_SWITCH_FROM_HOME) {
473             InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
474         }
475 
476         mLauncher.getStateManager().goToState(targetState, false, forEndCallback(this::clearState));
477     }
478 
cancelAnimations()479     private void cancelAnimations() {
480         if (mNonOverviewAnim != null) {
481             mNonOverviewAnim.getAnimationPlayer().cancel();
482         }
483         if (mXOverviewAnim != null) {
484             mXOverviewAnim.getAnimationPlayer().cancel();
485         }
486         if (mYOverviewAnim != null) {
487             mYOverviewAnim.cancelAnimation();
488         }
489         mMotionPauseDetector.clear();
490     }
491 
clearState()492     private void clearState() {
493         cancelAnimations();
494         mNonOverviewAnim = null;
495         mXOverviewAnim = null;
496         mYOverviewAnim = null;
497         mIsHomeScreenVisible = true;
498         mSwipeDetector.finishedScrolling();
499         mRecentsView.setOnEmptyMessageUpdatedListener(null);
500     }
501 }
502