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