From 4fdba14cfb737dc3d571ae35c761283b8f660c3e Mon Sep 17 00:00:00 2001 From: Tony Wickham Date: Mon, 4 Nov 2019 16:23:51 -0800 Subject: [PATCH] Two-zone model: swipe up from nav bar vs above it When ENABLE_OVERVIEW_ACTIONS flag is enabled, swiping up from the nav bar goes to overview if you hold, or the first home screen if you don't. - Added NoButtonNavBarToOverviewTouchController, which extends FlingAndHoldTouchController but only works if you start from the nav bar. Otherwise it falls back to PortraitStatesTouchController to handle normal state transition to all apps. - Added HintState. This is where the workspace/hotseat/qsb scale down when you swipe up from the nav bar, to hint that you're about to either go to overview or the first home screen. - Added getQsbScaleAndTranslation() to allow Overview and Hint states to treat it as part of the hotseat. - Since Overview needs to show above the QSB as it's animating, bring Overview to the front and update OverviewScrim to handle the case where there's no view above Overview to draw the scrim beneath. - ENABLE_OVERVIEW_ACTIONS is always false in 2-button mode, since the shelf is crucial to that mode. Bug: 143361609 Change-Id: I743481bb239dc77f7024dc98ba68a43534da2637 --- .../uioverrides/QuickstepLauncher.java | 7 +- .../uioverrides/states/OverviewState.java | 26 ++- .../FlingAndHoldTouchController.java | 85 +++++---- ...ButtonNavbarToOverviewTouchController.java | 174 ++++++++++++++++++ .../NoButtonQuickSwitchTouchController.java | 8 +- .../android/quickstep/util/ShelfPeekAnim.java | 3 +- .../QuickstepProcessInitializer.java | 22 +++ .../quickstep/views/ShelfScrimView.java | 14 +- src/com/android/launcher3/Launcher.java | 5 + .../android/launcher3/LauncherAnimUtils.java | 5 +- src/com/android/launcher3/LauncherState.java | 14 +- src/com/android/launcher3/Utilities.java | 3 +- src/com/android/launcher3/Workspace.java | 3 +- .../WorkspaceStateTransitionAnimation.java | 30 ++- .../anim/AnimatorPlaybackController.java | 13 +- .../launcher3/config/FeatureFlags.java | 3 +- .../launcher3/dragndrop/DragLayer.java | 3 + .../launcher3/graphics/OverviewScrim.java | 7 +- .../android/launcher3/states/HintState.java | 54 ++++++ .../launcher3/testing/TestProtocol.java | 3 + .../AbstractStateChangeTouchController.java | 6 +- 21 files changed, 419 insertions(+), 69 deletions(-) create mode 100644 quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java create mode 100644 src/com/android/launcher3/states/HintState.java diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index 4b5ba951b3..c359423085 100644 --- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -37,6 +37,7 @@ import com.android.launcher3.popup.SystemShortcut; import com.android.launcher3.uioverrides.touchcontrollers.FlingAndHoldTouchController; import com.android.launcher3.uioverrides.touchcontrollers.LandscapeEdgeSwipeController; import com.android.launcher3.uioverrides.touchcontrollers.NavBarToHomeTouchController; +import com.android.launcher3.uioverrides.touchcontrollers.NoButtonNavbarToOverviewTouchController; import com.android.launcher3.uioverrides.touchcontrollers.NoButtonQuickSwitchTouchController; import com.android.launcher3.uioverrides.touchcontrollers.OverviewToAllAppsTouchController; import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController; @@ -224,7 +225,11 @@ public class QuickstepLauncher extends BaseQuickstepLauncher { if (mode == NO_BUTTON) { list.add(new NoButtonQuickSwitchTouchController(this)); list.add(new NavBarToHomeTouchController(this)); - list.add(new FlingAndHoldTouchController(this)); + if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) { + list.add(new NoButtonNavbarToOverviewTouchController(this)); + } else { + list.add(new FlingAndHoldTouchController(this)); + } } else { if (getDeviceProfile().isVerticalBarLayout()) { list.add(new OverviewToAllAppsTouchController(this)); diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java index 73c0c97f08..9c78af9af1 100644 --- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java @@ -30,11 +30,13 @@ import static com.android.launcher3.anim.Interpolators.ACCEL; import static com.android.launcher3.anim.Interpolators.DEACCEL_2; import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2; import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_7; +import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS; import static com.android.launcher3.logging.LoggerUtils.newContainerTarget; import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE; import android.graphics.Rect; import android.view.View; +import android.view.animation.Interpolator; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.DeviceProfile; @@ -44,7 +46,6 @@ import com.android.launcher3.R; import com.android.launcher3.Workspace; import com.android.launcher3.allapps.DiscoveryBounce; import com.android.launcher3.anim.AnimatorSetBuilder; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.userevent.nano.LauncherLogProto.Action; import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; import com.android.quickstep.SysUINavigationMode; @@ -115,6 +116,15 @@ public class OverviewState extends LauncherState { return new ScaleAndTranslation(1f, 0f, 0f); } + @Override + public ScaleAndTranslation getQsbScaleAndTranslation(Launcher launcher) { + if (this == OVERVIEW && ENABLE_OVERVIEW_ACTIONS.get()) { + // Treat the QSB as part of the hotseat so they move together. + return getHotseatScaleAndTranslation(launcher); + } + return super.getQsbScaleAndTranslation(launcher); + } + @Override public void onStateEnabled(Launcher launcher) { AbstractFloatingView.closeAllOpenViews(launcher); @@ -141,7 +151,7 @@ public class OverviewState extends LauncherState { if (launcher.getDeviceProfile().isVerticalBarLayout()) { return VERTICAL_SWIPE_INDICATOR | RECENTS_CLEAR_ALL_BUTTON; } else { - if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) { + if (ENABLE_OVERVIEW_ACTIONS.get()) { return VERTICAL_SWIPE_INDICATOR | RECENTS_CLEAR_ALL_BUTTON; } @@ -195,9 +205,10 @@ public class OverviewState extends LauncherState { @Override public void prepareForAtomicAnimation(Launcher launcher, LauncherState fromState, AnimatorSetBuilder builder) { - if (fromState == NORMAL && this == OVERVIEW) { + if ((fromState == NORMAL || fromState == HINT_STATE) && this == OVERVIEW) { if (SysUINavigationMode.getMode(launcher) == SysUINavigationMode.Mode.NO_BUTTON) { - builder.setInterpolator(ANIM_WORKSPACE_SCALE, ACCEL); + builder.setInterpolator(ANIM_WORKSPACE_SCALE, + fromState == NORMAL ? ACCEL : OVERSHOOT_1_2); builder.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCEL); } else { builder.setInterpolator(ANIM_WORKSPACE_SCALE, OVERSHOOT_1_2); @@ -210,8 +221,11 @@ public class OverviewState extends LauncherState { } builder.setInterpolator(ANIM_WORKSPACE_FADE, OVERSHOOT_1_2); builder.setInterpolator(ANIM_OVERVIEW_SCALE, OVERSHOOT_1_2); - builder.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, OVERSHOOT_1_7); - builder.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, OVERSHOOT_1_7); + Interpolator translationInterpolator = ENABLE_OVERVIEW_ACTIONS.get() + ? OVERSHOOT_1_2 + : OVERSHOOT_1_7; + builder.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, translationInterpolator); + builder.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, translationInterpolator); builder.setInterpolator(ANIM_OVERVIEW_FADE, OVERSHOOT_1_2); } } diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java index d388f495bf..ff1b5f61e0 100644 --- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java @@ -60,7 +60,7 @@ public class FlingAndHoldTouchController extends PortraitStatesTouchController { private static final long PEEK_OUT_ANIM_DURATION = 100; private static final float MAX_DISPLACEMENT_PERCENT = 0.75f; - private final MotionPauseDetector mMotionPauseDetector; + protected final MotionPauseDetector mMotionPauseDetector; private final float mMotionPauseMinDisplacement; private final float mMotionPauseMaxDisplacement; @@ -85,37 +85,39 @@ public class FlingAndHoldTouchController extends PortraitStatesTouchController { super.onDragStart(start); if (handlingOverviewAnim()) { - mMotionPauseDetector.setOnMotionPauseListener(isPaused -> { - RecentsView recentsView = mLauncher.getOverviewPanel(); - recentsView.setOverviewStateEnabled(isPaused); - if (mPeekAnim != null) { - mPeekAnim.cancel(); - } - LauncherState fromState = isPaused ? NORMAL : OVERVIEW_PEEK; - LauncherState toState = isPaused ? OVERVIEW_PEEK : NORMAL; - long peekDuration = isPaused ? PEEK_IN_ANIM_DURATION : PEEK_OUT_ANIM_DURATION; - mPeekAnim = mLauncher.getStateManager().createAtomicAnimation(fromState, toState, - new AnimatorSetBuilder(), ATOMIC_OVERVIEW_PEEK_COMPONENT, peekDuration); - mPeekAnim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mPeekAnim = null; - } - }); - mPeekAnim.start(); - VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC); - - mLauncher.getDragLayer().getScrim().animateToSysuiMultiplier(isPaused ? 0 : 1, - peekDuration, 0); - }); + mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseChanged); } } + protected void onMotionPauseChanged(boolean isPaused) { + RecentsView recentsView = mLauncher.getOverviewPanel(); + recentsView.setOverviewStateEnabled(isPaused); + if (mPeekAnim != null) { + mPeekAnim.cancel(); + } + LauncherState fromState = isPaused ? NORMAL : OVERVIEW_PEEK; + LauncherState toState = isPaused ? OVERVIEW_PEEK : NORMAL; + long peekDuration = isPaused ? PEEK_IN_ANIM_DURATION : PEEK_OUT_ANIM_DURATION; + mPeekAnim = mLauncher.getStateManager().createAtomicAnimation(fromState, toState, + new AnimatorSetBuilder(), ATOMIC_OVERVIEW_PEEK_COMPONENT, peekDuration); + mPeekAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mPeekAnim = null; + } + }); + mPeekAnim.start(); + VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC); + + mLauncher.getDragLayer().getScrim().animateToSysuiMultiplier(isPaused ? 0 : 1, + peekDuration, 0); + } + /** * @return Whether we are handling the overview animation, rather than * having it as part of the existing animation to the target state. */ - private boolean handlingOverviewAnim() { + protected boolean handlingOverviewAnim() { int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags(); return mStartState == NORMAL && (stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0; } @@ -162,7 +164,8 @@ public class FlingAndHoldTouchController extends PortraitStatesTouchController { @Override public boolean onDrag(float displacement, MotionEvent event) { float upDisplacement = -displacement; - mMotionPauseDetector.setDisallowPause(upDisplacement < mMotionPauseMinDisplacement + mMotionPauseDetector.setDisallowPause(!handlingOverviewAnim() + || upDisplacement < mMotionPauseMinDisplacement || upDisplacement > mMotionPauseMaxDisplacement); mMotionPauseDetector.addPosition(displacement, event.getEventTime()); return super.onDrag(displacement, event); @@ -171,19 +174,7 @@ public class FlingAndHoldTouchController extends PortraitStatesTouchController { @Override public void onDragEnd(float velocity) { if (mMotionPauseDetector.isPaused() && handlingOverviewAnim()) { - if (mPeekAnim != null) { - mPeekAnim.cancel(); - } - - Animator overviewAnim = mLauncher.getAppTransitionManager().createStateElementAnimation( - INDEX_PAUSE_TO_OVERVIEW_ANIM); - overviewAnim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - onSwipeInteractionCompleted(OVERVIEW, Touch.SWIPE); - } - }); - overviewAnim.start(); + goToOverviewOnDragEnd(velocity); } else { super.onDragEnd(velocity); } @@ -195,6 +186,22 @@ public class FlingAndHoldTouchController extends PortraitStatesTouchController { mMotionPauseDetector.clear(); } + protected void goToOverviewOnDragEnd(float velocity) { + if (mPeekAnim != null) { + mPeekAnim.cancel(); + } + + Animator overviewAnim = mLauncher.getAppTransitionManager().createStateElementAnimation( + INDEX_PAUSE_TO_OVERVIEW_ANIM); + overviewAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + onSwipeInteractionCompleted(OVERVIEW, Touch.SWIPE); + } + }); + overviewAnim.start(); + } + @Override protected void goToTargetState(LauncherState targetState, int logAction) { if (mPeekAnim != null && mPeekAnim.isStarted()) { diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java new file mode 100644 index 0000000000..a993f3f53b --- /dev/null +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.uioverrides.touchcontrollers; + +import static com.android.launcher3.LauncherState.HINT_STATE; +import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.LauncherStateManager.ATOMIC_OVERVIEW_PEEK_COMPONENT; +import static com.android.launcher3.Utilities.EDGE_NAV_BAR; +import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.view.MotionEvent; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimationSuccessListener; +import com.android.launcher3.anim.AnimatorSetBuilder; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.launcher3.util.VibratorWrapper; +import com.android.quickstep.util.StaggeredWorkspaceAnim; + +/** + * Touch controller which handles swipe and hold from the nav bar to go to Overview. Swiping above + * the nav bar falls back to go to All Apps. Swiping from the nav bar without holding goes to the + * first home screen instead of to Overview. + */ +public class NoButtonNavbarToOverviewTouchController extends FlingAndHoldTouchController { + + private boolean mDidTouchStartInNavBar; + private boolean mReachedOverview; + + public NoButtonNavbarToOverviewTouchController(Launcher l) { + super(l); + } + + @Override + protected boolean canInterceptTouch(MotionEvent ev) { + mDidTouchStartInNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0; + return super.canInterceptTouch(ev); + } + + @Override + protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) { + if (fromState == NORMAL && mDidTouchStartInNavBar) { + return HINT_STATE; + } else if (fromState == OVERVIEW && isDragTowardPositive) { + // Don't allow swiping up to all apps. + return OVERVIEW; + } + return super.getTargetState(fromState, isDragTowardPositive); + } + + @Override + protected float initCurrentAnimation(int animComponents) { + float progressMultiplier = super.initCurrentAnimation(animComponents); + if (mToState == HINT_STATE) { + // Track the drag across the entire height of the screen. + progressMultiplier = -1 / getShiftRange(); + } + return progressMultiplier; + } + + @Override + public void onDragStart(boolean start) { + super.onDragStart(start); + + mReachedOverview = false; + } + + @Override + protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration, + LauncherState targetState, float velocity, boolean isFling) { + super.updateSwipeCompleteAnimation(animator, expectedDuration, targetState, velocity, + isFling); + if (targetState == HINT_STATE) { + // Normally we compute the duration based on the velocity and distance to the given + // state, but since the hint state tracks the entire screen without a clear endpoint, we + // need to manually set the duration to a reasonable value. + animator.setDuration(HINT_STATE.transitionDuration); + } + } + + @Override + protected void onMotionPauseChanged(boolean isPaused) { + if (mCurrentAnimation == null) { + return; + } + mCurrentAnimation.dispatchOnCancelWithoutCancelRunnable(() -> { + mLauncher.getStateManager().goToState(OVERVIEW, true, () -> { + mReachedOverview = true; + maybeSwipeInteractionToOverviewComplete(); + }); + }); + VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC); + } + + private void maybeSwipeInteractionToOverviewComplete() { + if (mReachedOverview && mDetector.isSettlingState()) { + onSwipeInteractionCompleted(OVERVIEW, Touch.SWIPE); + } + } + + @Override + protected boolean handlingOverviewAnim() { + return mDidTouchStartInNavBar && super.handlingOverviewAnim(); + } + + @Override + public boolean onDrag(float displacement, MotionEvent event) { + if (mMotionPauseDetector.isPaused()) { + // Stay in Overview. + return true; + } + return super.onDrag(displacement, event); + } + + @Override + protected void goToOverviewOnDragEnd(float velocity) { + float velocityDp = dpiFromPx(velocity); + boolean isFling = Math.abs(velocityDp) > 1; + LauncherStateManager stateManager = mLauncher.getStateManager(); + if (isFling) { + // When flinging, go back to home instead of overview. + if (velocity > 0) { + stateManager.goToState(NORMAL, true, + () -> onSwipeInteractionCompleted(NORMAL, Touch.FLING)); + } else { + StaggeredWorkspaceAnim staggeredWorkspaceAnim = new StaggeredWorkspaceAnim( + mLauncher, velocity, false /* animateOverviewScrim */); + staggeredWorkspaceAnim.start(); + + // StaggeredWorkspaceAnim doesn't animate overview, so we handle it here. + stateManager.cancelAnimation(); + AnimatorSetBuilder builder = new AnimatorSetBuilder(); + long duration = OVERVIEW.transitionDuration; + AnimatorSet anim = stateManager.createAtomicAnimation( + stateManager.getState(), NORMAL, builder, + ATOMIC_OVERVIEW_PEEK_COMPONENT, duration); + anim.addListener(new AnimationSuccessListener() { + @Override + public void onAnimationSuccess(Animator animator) { + onSwipeInteractionCompleted(NORMAL, Touch.SWIPE); + } + }); + anim.start(); + } + } else { + maybeSwipeInteractionToOverviewComplete(); + } + } + + private float dpiFromPx(float pixels) { + return Utilities.dpiFromPx(pixels, mLauncher.getResources().getDisplayMetrics()); + } +} diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java index 4e08df9c70..8628db0107 100644 --- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java @@ -62,6 +62,7 @@ import com.android.launcher3.Utilities; import com.android.launcher3.allapps.AllAppsTransitionController; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.AnimatorSetBuilder; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.graphics.OverviewScrim; import com.android.launcher3.touch.BaseSwipeDetector; import com.android.launcher3.touch.BothAxesSwipeDetector; @@ -181,6 +182,12 @@ public class NoButtonQuickSwitchTouchController implements TouchController, @Override public void onMotionPauseChanged(boolean isPaused) { + VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC); + + if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) { + return; + } + ShelfAnimState shelfState = isPaused ? PEEK : HIDE; if (shelfState == PEEK) { // Some shelf elements (e.g. qsb) were hidden, but we need them visible when peeking. @@ -197,7 +204,6 @@ public class NoButtonQuickSwitchTouchController implements TouchController, } mShelfPeekAnim.setShelfState(shelfState, ShelfPeekAnim.INTERPOLATOR, ShelfPeekAnim.DURATION); - VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC); } private void setupAnimators() { diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ShelfPeekAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ShelfPeekAnim.java index 41be6834f7..217eca559d 100644 --- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ShelfPeekAnim.java +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ShelfPeekAnim.java @@ -25,6 +25,7 @@ import android.animation.AnimatorListenerAdapter; import android.view.animation.Interpolator; import com.android.launcher3.Launcher; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.uioverrides.states.OverviewState; /** @@ -48,7 +49,7 @@ public class ShelfPeekAnim { * Animates to the given state, canceling the previous animation if it was still running. */ public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator, long duration) { - if (mShelfState == shelfState) { + if (mShelfState == shelfState || FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) { return; } mLauncher.getStateManager().cancelStateElementAnimation(INDEX_SHELF_ANIM); diff --git a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java index befeee0db9..9817e32ae9 100644 --- a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java +++ b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java @@ -15,6 +15,11 @@ */ package com.android.quickstep; +import static android.content.Context.MODE_PRIVATE; + +import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS; +import static com.android.launcher3.config.FeatureFlags.FLAGS_PREF_NAME; + import android.content.Context; import android.content.pm.PackageManager; import android.os.UserManager; @@ -22,6 +27,7 @@ import android.util.Log; import com.android.launcher3.BuildConfig; import com.android.launcher3.MainProcessInitializer; +import com.android.launcher3.config.FeatureFlags; import com.android.systemui.shared.system.ThreadedRendererCompat; @SuppressWarnings("unused") @@ -50,5 +56,21 @@ public class QuickstepProcessInitializer extends MainProcessInitializer { // Elevate GPU priority for Quickstep and Remote animations. ThreadedRendererCompat.setContextPriority(ThreadedRendererCompat.EGL_CONTEXT_PRIORITY_HIGH_IMG); + + // Force disable some feature flags based on the system ui navigation mode. + SysUINavigationMode.Mode currMode = SysUINavigationMode.INSTANCE.get(context) + .addModeChangeListener(mode -> disableFeatureFlagsForSysuiNavMode(context, mode)); + disableFeatureFlagsForSysuiNavMode(context, currMode); + } + + private void disableFeatureFlagsForSysuiNavMode(Context ctx, SysUINavigationMode.Mode mode) { + if (mode == SysUINavigationMode.Mode.TWO_BUTTONS) { + ctx.getSharedPreferences(FLAGS_PREF_NAME, MODE_PRIVATE) + .edit() + .putBoolean(ENABLE_OVERVIEW_ACTIONS.key, false) + .apply(); + + FeatureFlags.initialize(ctx); + } } } diff --git a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java index 7885f5cb9b..7ed1e21dfb 100644 --- a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java +++ b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java @@ -41,6 +41,8 @@ import com.android.launcher3.LauncherState; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.uioverrides.states.OverviewState; import com.android.launcher3.util.Themes; import com.android.launcher3.views.ScrimView; import com.android.quickstep.SysUINavigationMode; @@ -155,12 +157,18 @@ public class ShelfScrimView extends ScrimView mRemainingScreenPathValid = false; mShiftRange = mLauncher.getAllAppsController().getShiftRange(); + Context context = getContext(); if ((OVERVIEW.getVisibleElements(mLauncher) & ALL_APPS_HEADER_EXTRA) == 0) { - mMidProgress = 1; mDragHandleProgress = 1; - mMidAlpha = 0; + if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) { + // Fade in all apps background quickly to distinguish from swiping from nav bar. + mMidAlpha = Themes.getAttrInteger(context, R.attr.allAppsInterimScrimAlpha); + mMidProgress = OverviewState.getDefaultVerticalProgress(mLauncher); + } else { + mMidAlpha = 0; + mMidProgress = 1; + } } else { - Context context = getContext(); mMidAlpha = Themes.getAttrInteger(context, R.attr.allAppsInterimScrimAlpha); mMidProgress = OVERVIEW.getVerticalProgress(mLauncher); Rect hotseatPadding = dp.getHotseatLayoutPadding(); diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 06f3453981..2ccdd6f4a9 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -1180,6 +1180,11 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, mDropTargetBar.setup(mDragController); mAllAppsController.setupViews(mAppsView); + + if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) { + // Overview is above all other launcher elements, including qsb, so move it to the top. + mOverviewPanel.bringToFront(); + } } /** diff --git a/src/com/android/launcher3/LauncherAnimUtils.java b/src/com/android/launcher3/LauncherAnimUtils.java index 74362ed5a3..cdfd257d6d 100644 --- a/src/com/android/launcher3/LauncherAnimUtils.java +++ b/src/com/android/launcher3/LauncherAnimUtils.java @@ -16,6 +16,8 @@ package com.android.launcher3; +import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS; + import android.graphics.drawable.Drawable; import android.util.FloatProperty; import android.util.Property; @@ -28,9 +30,10 @@ public class LauncherAnimUtils { * easier access from static classes and enums */ public static final int ALL_APPS_TRANSITION_MS = 320; - public static final int OVERVIEW_TRANSITION_MS = 250; + public static final int OVERVIEW_TRANSITION_MS = ENABLE_OVERVIEW_ACTIONS.get() ? 380 : 250; public static final int SPRING_LOADED_TRANSITION_MS = 150; public static final int SPRING_LOADED_EXIT_DELAY = 500; + public static final int HINT_TRANSITION_MS = 80; // The progress of an animation to all apps must be at least this far along to snap to all apps. public static final float MIN_PROGRESS_TO_ALL_APPS = 0.5f; diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java index d2b447b4b0..8b80cba7ad 100644 --- a/src/com/android/launcher3/LauncherState.java +++ b/src/com/android/launcher3/LauncherState.java @@ -30,9 +30,11 @@ import static com.android.launcher3.anim.Interpolators.ACCEL_2; import static com.android.launcher3.anim.Interpolators.DEACCEL; import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7; import static com.android.launcher3.anim.Interpolators.clampToProgress; +import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS; import static com.android.launcher3.states.RotationHelper.REQUEST_NONE; import static com.android.launcher3.testing.TestProtocol.ALL_APPS_STATE_ORDINAL; import static com.android.launcher3.testing.TestProtocol.BACKGROUND_APP_STATE_ORDINAL; +import static com.android.launcher3.testing.TestProtocol.HINT_STATE_ORDINAL; import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL; import static com.android.launcher3.testing.TestProtocol.OVERVIEW_PEEK_STATE_ORDINAL; import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL; @@ -42,6 +44,7 @@ import static com.android.launcher3.testing.TestProtocol.SPRING_LOADED_STATE_ORD import android.view.animation.Interpolator; import com.android.launcher3.anim.AnimatorSetBuilder; +import com.android.launcher3.states.HintState; import com.android.launcher3.states.SpringLoadedState; import com.android.launcher3.uioverrides.states.AllAppsState; import com.android.launcher3.uioverrides.states.OverviewState; @@ -88,7 +91,7 @@ public class LauncherState { } }; - private static final LauncherState[] sAllStates = new LauncherState[7]; + private static final LauncherState[] sAllStates = new LauncherState[8]; /** * TODO: Create a separate class for NORMAL state. @@ -104,6 +107,7 @@ public class LauncherState { public static final LauncherState SPRING_LOADED = new SpringLoadedState( SPRING_LOADED_STATE_ORDINAL); public static final LauncherState ALL_APPS = new AllAppsState(ALL_APPS_STATE_ORDINAL); + public static final LauncherState HINT_STATE = new HintState(HINT_STATE_ORDINAL); public static final LauncherState OVERVIEW = new OverviewState(OVERVIEW_STATE_ORDINAL); public static final LauncherState OVERVIEW_PEEK = @@ -212,6 +216,10 @@ public class LauncherState { return launcher.getOverviewScaleAndTranslationForNormalState(); } + public ScaleAndTranslation getQsbScaleAndTranslation(Launcher launcher) { + return new ScaleAndTranslation(1, 0, 0); + } + public float getOverviewFullscreenProgress() { return 0; } @@ -319,6 +327,10 @@ public class LauncherState { if (!isHotseatVisible) { hotseat.setScaleX(0.92f); hotseat.setScaleY(0.92f); + if (ENABLE_OVERVIEW_ACTIONS.get()) { + launcher.getAppsView().setScaleX(0.92f); + launcher.getAppsView().setScaleY(0.92f); + } } } else if (this == NORMAL && fromState == OVERVIEW_PEEK) { // Keep fully visible until the very end (when overview is offscreen) to make invisible. diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index af9a1b4e35..8bfc02802c 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -75,7 +75,6 @@ import com.android.launcher3.views.Transposable; import com.android.launcher3.widget.PendingAddShortcutInfo; import java.lang.reflect.Method; -import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.regex.Matcher; @@ -382,7 +381,7 @@ public final class Utilities { return res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; } - public static float dpiFromPx(int size, DisplayMetrics metrics){ + public static float dpiFromPx(float size, DisplayMetrics metrics) { float densityRatio = (float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT; return (size / densityRatio); } diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index 327896013f..c73a5e1efb 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -3256,7 +3256,8 @@ public class Workspace extends PagedView return mOverlayShown; } - void moveToDefaultScreen() { + /** Calls {@link #snapToPage(int)} on the {@link #DEFAULT_PAGE}, then requests focus on it. */ + public void moveToDefaultScreen() { int page = DEFAULT_PAGE; if (!workspaceInModalState() && getNextPage() != page) { snapToPage(page); diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java index 7a7e1fee6c..c33392d53d 100644 --- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java +++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java @@ -36,6 +36,7 @@ import android.view.animation.Interpolator; import com.android.launcher3.LauncherState.PageAlphaProvider; import com.android.launcher3.LauncherState.ScaleAndTranslation; import com.android.launcher3.LauncherStateManager.AnimationConfig; +import com.android.launcher3.allapps.AllAppsContainerView; import com.android.launcher3.anim.AnimatorSetBuilder; import com.android.launcher3.anim.PropertySetter; import com.android.launcher3.graphics.WorkspaceAndHotseatScrim; @@ -77,6 +78,7 @@ public class WorkspaceStateTransitionAnimation { ScaleAndTranslation scaleAndTranslation = state.getWorkspaceScaleAndTranslation(mLauncher); ScaleAndTranslation hotseatScaleAndTranslation = state.getHotseatScaleAndTranslation( mLauncher); + ScaleAndTranslation qsbScaleAndTranslation = state.getQsbScaleAndTranslation(mLauncher); mNewScale = scaleAndTranslation.scale; PageAlphaProvider pageAlphaProvider = state.getWorkspacePageAlphaProvider(mLauncher); final int childCount = mWorkspace.getChildCount(); @@ -90,24 +92,24 @@ public class WorkspaceStateTransitionAnimation { pageAlphaProvider.interpolator); boolean playAtomicComponent = config.playAtomicOverviewScaleComponent(); Hotseat hotseat = mWorkspace.getHotseat(); + // Since we set the pivot relative to mWorkspace, we need to scale a sibling of Workspace. + AllAppsContainerView qsbScaleView = mLauncher.getAppsView(); + View qsbView = qsbScaleView.getSearchView(); if (playAtomicComponent) { Interpolator scaleInterpolator = builder.getInterpolator(ANIM_WORKSPACE_SCALE, ZOOM_OUT); propertySetter.setFloat(mWorkspace, SCALE_PROPERTY, mNewScale, scaleInterpolator); if (!hotseat.getRotationMode().isTransposed) { - // Set the hotseat's pivot point to match the workspace's, so that it scales - // together. Since both hotseat and workspace can move, transform the point - // manually instead of using dragLayer.getDescendantCoordRelativeToSelf and - // related methods. - hotseat.setPivotY(mWorkspace.getPivotY() + mWorkspace.getTop() - hotseat.getTop()); - hotseat.setPivotX(mWorkspace.getPivotX() - + mWorkspace.getLeft() - hotseat.getLeft()); + setPivotToScaleWithWorkspace(hotseat); + setPivotToScaleWithWorkspace(qsbScaleView); } float hotseatScale = hotseatScaleAndTranslation.scale; Interpolator hotseatScaleInterpolator = builder.getInterpolator(ANIM_HOTSEAT_SCALE, scaleInterpolator); propertySetter.setFloat(hotseat, SCALE_PROPERTY, hotseatScale, hotseatScaleInterpolator); + propertySetter.setFloat(qsbScaleView, SCALE_PROPERTY, qsbScaleAndTranslation.scale, + hotseatScaleInterpolator); float hotseatIconsAlpha = (elements & HOTSEAT_ICONS) != 0 ? 1 : 0; propertySetter.setViewAlpha(hotseat, hotseatIconsAlpha, fadeInterpolator); @@ -134,10 +136,24 @@ public class WorkspaceStateTransitionAnimation { hotseatScaleAndTranslation.translationY, hotseatTranslationInterpolator); propertySetter.setFloat(mWorkspace.getPageIndicator(), View.TRANSLATION_Y, hotseatScaleAndTranslation.translationY, hotseatTranslationInterpolator); + propertySetter.setFloat(qsbView, View.TRANSLATION_Y, + qsbScaleAndTranslation.translationY, hotseatTranslationInterpolator); setScrim(propertySetter, state); } + /** + * Set the given view's pivot point to match the workspace's, so that it scales together. Since + * both this view and workspace can move, transform the point manually instead of using + * dragLayer.getDescendantCoordRelativeToSelf and related methods. + */ + private void setPivotToScaleWithWorkspace(View sibling) { + sibling.setPivotY(mWorkspace.getPivotY() + mWorkspace.getTop() + - sibling.getTop() - sibling.getTranslationY()); + sibling.setPivotX(mWorkspace.getPivotX() + mWorkspace.getLeft() + - sibling.getLeft() - sibling.getTranslationX()); + } + public void setScrim(PropertySetter propertySetter, LauncherState state) { WorkspaceAndHotseatScrim scrim = mLauncher.getDragLayer().getScrim(); propertySetter.setFloat(scrim, SCRIM_PROGRESS, state.getWorkspaceScrimAlpha(mLauncher), diff --git a/src/com/android/launcher3/anim/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java index 4a52795f89..1c277ab8c0 100644 --- a/src/com/android/launcher3/anim/AnimatorPlaybackController.java +++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java @@ -26,6 +26,7 @@ import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.util.Log; +import androidx.annotation.Nullable; import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.SpringAnimation; @@ -250,14 +251,24 @@ public abstract class AnimatorPlaybackController implements ValueAnimator.Animat } } + /** @see #dispatchOnCancelWithoutCancelRunnable(Runnable) */ + public void dispatchOnCancelWithoutCancelRunnable() { + dispatchOnCancelWithoutCancelRunnable(null); + } + /** * Sets mOnCancelRunnable = null before dispatching the cancel and restoring the runnable. This * is intended to be used only if you need to cancel but want to defer cleaning up yourself. + * @param callback An optional callback to run after dispatching the cancel but before resetting + * the onCancelRunnable. */ - public void dispatchOnCancelWithoutCancelRunnable() { + public void dispatchOnCancelWithoutCancelRunnable(@Nullable Runnable callback) { Runnable onCancel = mOnCancelRunnable; setOnCancelRunnable(null); dispatchOnCancel(); + if (callback != null) { + callback.run(); + } setOnCancelRunnable(onCancel); } diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index 8ccb369138..b1a2c3368d 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -117,7 +117,8 @@ public final class FeatureFlags { "Show launcher preview in grid picker"); public static final BooleanFlag ENABLE_OVERVIEW_ACTIONS = getDebugFlag( - "ENABLE_OVERVIEW_ACTIONS", false, "Show app actions in Overview"); + "ENABLE_OVERVIEW_ACTIONS", false, "Show app actions instead of the shelf in Overview." + + " As part of this decoupling, also distinguish swipe up from nav bar vs above it."); public static final BooleanFlag ENABLE_DATABASE_RESTORE = getDebugFlag( "ENABLE_DATABASE_RESTORE", true, diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java index 8823bde624..92f35e2b19 100644 --- a/src/com/android/launcher3/dragndrop/DragLayer.java +++ b/src/com/android/launcher3/dragndrop/DragLayer.java @@ -536,6 +536,9 @@ public class DragLayer extends BaseDragLayer { mOverviewScrim.updateCurrentScrimmedView(this); mFocusIndicatorHelper.draw(canvas); super.dispatchDraw(canvas); + if (mOverviewScrim.getScrimmedView() == null) { + mOverviewScrim.draw(canvas); + } } @Override diff --git a/src/com/android/launcher3/graphics/OverviewScrim.java b/src/com/android/launcher3/graphics/OverviewScrim.java index d707403ed2..94acbfdcca 100644 --- a/src/com/android/launcher3/graphics/OverviewScrim.java +++ b/src/com/android/launcher3/graphics/OverviewScrim.java @@ -55,16 +55,17 @@ public class OverviewScrim extends Scrim { mCurrentScrimmedView = mStableScrimmedView; int currentIndex = root.indexOfChild(mCurrentScrimmedView); final int childCount = root.getChildCount(); - while (mCurrentScrimmedView.getVisibility() != VISIBLE && currentIndex < childCount) { + while (mCurrentScrimmedView != null && mCurrentScrimmedView.getVisibility() != VISIBLE + && currentIndex < childCount) { currentIndex++; mCurrentScrimmedView = root.getChildAt(currentIndex); } } /** - * @return The view to draw the scrim behind. + * @return The view to draw the scrim behind, or null if all visible views should be scrimmed. */ - public View getScrimmedView() { + public @Nullable View getScrimmedView() { return mCurrentScrimmedView; } } diff --git a/src/com/android/launcher3/states/HintState.java b/src/com/android/launcher3/states/HintState.java new file mode 100644 index 0000000000..cb56097f05 --- /dev/null +++ b/src/com/android/launcher3/states/HintState.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.states; + +import static com.android.launcher3.LauncherAnimUtils.HINT_TRANSITION_MS; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.Workspace; +import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; + +/** + * Scale down workspace/hotseat to hint at going to either overview (on pause) or first home screen. + */ +public class HintState extends LauncherState { + + private static final int STATE_FLAGS = FLAG_DISABLE_ACCESSIBILITY | FLAG_DISABLE_RESTORE + | FLAG_HAS_SYS_UI_SCRIM; + + public HintState(int id) { + super(id, ContainerType.DEFAULT_CONTAINERTYPE, HINT_TRANSITION_MS, STATE_FLAGS); + } + + @Override + public ScaleAndTranslation getWorkspaceScaleAndTranslation(Launcher launcher) { + return new ScaleAndTranslation(0.9f, 0, 0); + } + + @Override + public ScaleAndTranslation getQsbScaleAndTranslation(Launcher launcher) { + // Treat the QSB as part of the hotseat so they move together. + return getHotseatScaleAndTranslation(launcher); + } + + @Override + public void onStateTransitionEnd(Launcher launcher) { + launcher.getStateManager().goToState(NORMAL); + Workspace workspace = launcher.getWorkspace(); + workspace.post(workspace::moveToDefaultScreen); + } +} diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java index 01c207f907..5c200509d9 100644 --- a/src/com/android/launcher3/testing/TestProtocol.java +++ b/src/com/android/launcher3/testing/TestProtocol.java @@ -31,6 +31,7 @@ public final class TestProtocol { public static final int QUICK_SWITCH_STATE_ORDINAL = 4; public static final int ALL_APPS_STATE_ORDINAL = 5; public static final int BACKGROUND_APP_STATE_ORDINAL = 6; + public static final int HINT_STATE_ORDINAL = 7; public static final String TAPL_EVENTS_TAG = "TaplEvents"; public static String stateOrdinalToString(int ordinal) { @@ -49,6 +50,8 @@ public final class TestProtocol { return "AllApps"; case BACKGROUND_APP_STATE_ORDINAL: return "Background"; + case HINT_STATE_ORDINAL: + return "Hint"; default: return null; } diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java index d193bef40d..3ec480d43e 100644 --- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java +++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java @@ -525,7 +525,11 @@ public abstract class AbstractStateChangeTouchController if (targetState != mStartState) { logReachedState(logAction, targetState); } - mLauncher.getStateManager().goToState(targetState, false /* animated */); + if (!mLauncher.isInState(targetState)) { + // If we're already in the target state, don't jump to it at the end of the animation in + // case the user started interacting with it before the animation finished. + mLauncher.getStateManager().goToState(targetState, false /* animated */); + } mLauncher.getDragLayer().getScrim().animateToSysuiMultiplier(1, 0, 0); }