1 /* 2 * Copyright (C) 2020 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 17 package com.android.launcher3.uioverrides.touchcontrollers; 18 19 import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE; 20 import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR; 21 import static com.android.launcher3.LauncherAnimUtils.newSingleUseCancelListener; 22 import static com.android.launcher3.LauncherState.ALL_APPS; 23 import static com.android.launcher3.LauncherState.HINT_STATE; 24 import static com.android.launcher3.LauncherState.NORMAL; 25 import static com.android.launcher3.LauncherState.OVERVIEW; 26 import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent; 27 import static com.android.launcher3.Utilities.EDGE_NAV_BAR; 28 import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback; 29 import static com.android.launcher3.util.NavigationMode.THREE_BUTTONS; 30 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC; 31 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ONE_HANDED_ACTIVE; 32 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED; 33 34 import android.animation.AnimatorSet; 35 import android.animation.ObjectAnimator; 36 import android.animation.ValueAnimator; 37 import android.graphics.PointF; 38 import android.view.MotionEvent; 39 import android.view.ViewConfiguration; 40 41 import com.android.internal.jank.Cuj; 42 import com.android.launcher3.Launcher; 43 import com.android.launcher3.LauncherState; 44 import com.android.launcher3.Utilities; 45 import com.android.launcher3.anim.AnimatorPlaybackController; 46 import com.android.launcher3.config.FeatureFlags; 47 import com.android.launcher3.states.StateAnimationConfig; 48 import com.android.launcher3.taskbar.LauncherTaskbarUIController; 49 import com.android.launcher3.uioverrides.QuickstepLauncher; 50 import com.android.launcher3.util.DisplayController; 51 import com.android.launcher3.util.VibratorWrapper; 52 import com.android.quickstep.SystemUiProxy; 53 import com.android.quickstep.util.AnimatorControllerWithResistance; 54 import com.android.quickstep.util.MotionPauseDetector; 55 import com.android.quickstep.util.OverviewToHomeAnim; 56 import com.android.quickstep.views.RecentsView; 57 import com.android.systemui.shared.system.InteractionJankMonitorWrapper; 58 59 import java.util.function.BiConsumer; 60 61 /** 62 * Touch controller which handles swipe and hold from the nav bar to go to Overview. Swiping above 63 * the nav bar falls back to go to All Apps. Swiping from the nav bar without holding goes to the 64 * first home screen instead of to Overview. 65 */ 66 public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouchController { 67 private static final float ONE_HANDED_ACTIVATED_SLOP_MULTIPLIER = 2.5f; 68 69 // How much of the movement to use for translating overview after swipe and hold. 70 private static final float OVERVIEW_MOVEMENT_FACTOR = 0.25f; 71 private static final long TRANSLATION_ANIM_MIN_DURATION_MS = 80; 72 private static final float TRANSLATION_ANIM_VELOCITY_DP_PER_MS = 0.8f; 73 74 private final VibratorWrapper mVibratorWrapper; 75 private final BiConsumer<AnimatorSet, Long> mCancelSplitRunnable; 76 private final RecentsView mRecentsView; 77 private final MotionPauseDetector mMotionPauseDetector; 78 private final float mMotionPauseMinDisplacement; 79 80 private boolean mDidTouchStartInNavBar; 81 private boolean mStartedOverview; 82 private boolean mReachedOverview; 83 // The last recorded displacement before we reached overview. 84 private PointF mStartDisplacement = new PointF(); 85 private float mStartY; 86 private AnimatorPlaybackController mOverviewResistYAnim; 87 88 // Normal to Hint animation has flag SKIP_OVERVIEW, so we update this scrim with this animator. 89 private ObjectAnimator mNormalToHintOverviewScrimAnimator; 90 91 /** 92 * @param cancelSplitRunnable Called when split placeholder view needs to be cancelled. 93 * Animation should be added to the provided AnimatorSet 94 */ NoButtonNavbarToOverviewTouchController(Launcher l, BiConsumer<AnimatorSet, Long> cancelSplitRunnable)95 public NoButtonNavbarToOverviewTouchController(Launcher l, 96 BiConsumer<AnimatorSet, Long> cancelSplitRunnable) { 97 super(l); 98 mRecentsView = l.getOverviewPanel(); 99 mMotionPauseDetector = new MotionPauseDetector(l); 100 mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop(); 101 mVibratorWrapper = VibratorWrapper.INSTANCE.get(l.getApplicationContext()); 102 mCancelSplitRunnable = cancelSplitRunnable; 103 } 104 105 @Override canInterceptTouch(MotionEvent ev)106 protected boolean canInterceptTouch(MotionEvent ev) { 107 if (!isTrackpadMotionEvent(ev) && DisplayController.getNavigationMode(mLauncher) 108 == THREE_BUTTONS) { 109 return false; 110 } 111 mDidTouchStartInNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0; 112 boolean isOneHandedModeActive = (SystemUiProxy.INSTANCE.get(mLauncher) 113 .getLastSystemUiStateFlags() & SYSUI_STATE_ONE_HANDED_ACTIVE) != 0; 114 // Reset touch slop multiplier to default 1.0f if one-handed-mode is not active 115 mDetector.setTouchSlopMultiplier( 116 isOneHandedModeActive ? ONE_HANDED_ACTIVATED_SLOP_MULTIPLIER : 1f /* default */); 117 return super.canInterceptTouch(ev) && !mLauncher.isInState(HINT_STATE); 118 } 119 120 @Override getTargetState(LauncherState fromState, boolean isDragTowardPositive)121 protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) { 122 if (fromState == NORMAL && mDidTouchStartInNavBar) { 123 return HINT_STATE; 124 } 125 return super.getTargetState(fromState, isDragTowardPositive); 126 } 127 128 @Override initCurrentAnimation()129 protected float initCurrentAnimation() { 130 float progressMultiplier = super.initCurrentAnimation(); 131 if (mToState == HINT_STATE) { 132 // Track the drag across the entire height of the screen. 133 progressMultiplier = -1f / mLauncher.getDeviceProfile().heightPx; 134 } 135 return progressMultiplier; 136 } 137 138 @Override onDragStart(boolean start, float startDisplacement)139 public void onDragStart(boolean start, float startDisplacement) { 140 if (mLauncher.isInState(ALL_APPS)) { 141 LauncherTaskbarUIController controller = 142 ((QuickstepLauncher) mLauncher).getTaskbarUIController(); 143 if (controller != null) { 144 controller.setShouldDelayLauncherStateAnim(true); 145 } 146 } 147 148 super.onDragStart(start, startDisplacement); 149 150 mMotionPauseDetector.clear(); 151 152 if (handlingOverviewAnim()) { 153 InteractionJankMonitorWrapper.begin(mRecentsView, Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS, 154 "Home"); 155 mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseDetected); 156 } 157 158 if (mFromState == NORMAL && mToState == HINT_STATE) { 159 mNormalToHintOverviewScrimAnimator = ObjectAnimator.ofArgb( 160 mLauncher.getScrimView(), 161 VIEW_BACKGROUND_COLOR, 162 mFromState.getWorkspaceScrimColor(mLauncher), 163 mToState.getWorkspaceScrimColor(mLauncher)); 164 } 165 mStartedOverview = false; 166 mReachedOverview = false; 167 mOverviewResistYAnim = null; 168 } 169 170 @Override updateProgress(float fraction)171 protected void updateProgress(float fraction) { 172 super.updateProgress(fraction); 173 if (mNormalToHintOverviewScrimAnimator != null) { 174 mNormalToHintOverviewScrimAnimator.setCurrentFraction(fraction); 175 } 176 } 177 178 @Override onDragEnd(float velocity)179 public void onDragEnd(float velocity) { 180 LauncherTaskbarUIController controller = 181 ((QuickstepLauncher) mLauncher).getTaskbarUIController(); 182 if (controller != null) { 183 controller.setShouldDelayLauncherStateAnim(false); 184 } 185 186 if (mStartedOverview) { 187 goToOverviewOrHomeOnDragEnd(velocity); 188 } else { 189 InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS); 190 super.onDragEnd(velocity); 191 } 192 193 mMotionPauseDetector.clear(); 194 mNormalToHintOverviewScrimAnimator = null; 195 if (mLauncher.isInState(OVERVIEW)) { 196 // Normally we would cleanup the state based on mCurrentAnimation, but since we stop 197 // using that when we pause to go to Overview, we need to clean up ourselves. 198 clearState(); 199 } 200 } 201 202 @Override updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration, LauncherState targetState, float velocity, boolean isFling)203 protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration, 204 LauncherState targetState, float velocity, boolean isFling) { 205 super.updateSwipeCompleteAnimation(animator, expectedDuration, targetState, velocity, 206 isFling); 207 if (targetState == HINT_STATE) { 208 // Normally we compute the duration based on the velocity and distance to the given 209 // state, but since the hint state tracks the entire screen without a clear endpoint, we 210 // need to manually set the duration to a reasonable value. 211 long duration = HINT_STATE.getTransitionDuration(mLauncher, true /* isToState */); 212 animator.setDuration(duration); 213 AnimatorSet animatorSet = new AnimatorSet(); 214 mCancelSplitRunnable.accept(animatorSet, duration); 215 animatorSet.start(); 216 } 217 if (FeatureFlags.ENABLE_PREMIUM_HAPTICS_ALL_APPS.get() && 218 ((mFromState == NORMAL && mToState == ALL_APPS) 219 || (mFromState == ALL_APPS && mToState == NORMAL)) && isFling) { 220 mVibratorWrapper.vibrateForDragBump(); 221 } 222 } 223 onMotionPauseDetected()224 private void onMotionPauseDetected() { 225 if (mCurrentAnimation == null) { 226 return; 227 } 228 mNormalToHintOverviewScrimAnimator = null; 229 mCurrentAnimation.getTarget().addListener(newSingleUseCancelListener(() -> 230 mLauncher.getStateManager().goToState(OVERVIEW, true, forSuccessCallback(() -> { 231 mOverviewResistYAnim = AnimatorControllerWithResistance 232 .createRecentsResistanceFromOverviewAnim(mLauncher, null) 233 .createPlaybackController(); 234 mReachedOverview = true; 235 maybeSwipeInteractionToOverviewComplete(); 236 })))); 237 238 mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener); 239 mCurrentAnimation.dispatchOnCancel(); 240 mStartedOverview = true; 241 VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC); 242 } 243 maybeSwipeInteractionToOverviewComplete()244 private void maybeSwipeInteractionToOverviewComplete() { 245 if (mReachedOverview && !mDetector.isDraggingState()) { 246 InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS); 247 onSwipeInteractionCompleted(OVERVIEW); 248 } 249 } 250 handlingOverviewAnim()251 private boolean handlingOverviewAnim() { 252 long stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags(); 253 return mDidTouchStartInNavBar && mStartState == NORMAL 254 && (stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0; 255 } 256 257 @Override onDrag(float yDisplacement, float xDisplacement, MotionEvent event)258 public boolean onDrag(float yDisplacement, float xDisplacement, MotionEvent event) { 259 if (mStartedOverview) { 260 if (!mReachedOverview) { 261 mStartDisplacement.set(xDisplacement, yDisplacement); 262 mStartY = event.getY(); 263 } else { 264 mRecentsView.setTranslationX((xDisplacement - mStartDisplacement.x) 265 * OVERVIEW_MOVEMENT_FACTOR); 266 float yProgress = (mStartDisplacement.y - yDisplacement) / mStartY; 267 if (yProgress > 0 && mOverviewResistYAnim != null) { 268 mOverviewResistYAnim.setPlayFraction(yProgress); 269 } else { 270 mRecentsView.setTranslationY((yDisplacement - mStartDisplacement.y) 271 * OVERVIEW_MOVEMENT_FACTOR); 272 } 273 } 274 } 275 276 float upDisplacement = -yDisplacement; 277 mMotionPauseDetector.setDisallowPause(!handlingOverviewAnim() 278 || upDisplacement < mMotionPauseMinDisplacement); 279 mMotionPauseDetector.addPosition(event); 280 281 // Stay in Overview. 282 return mStartedOverview || super.onDrag(yDisplacement, xDisplacement, event); 283 } 284 goToOverviewOrHomeOnDragEnd(float velocity)285 private void goToOverviewOrHomeOnDragEnd(float velocity) { 286 boolean goToHomeInsteadOfOverview = !mMotionPauseDetector.isPaused(); 287 if (goToHomeInsteadOfOverview) { 288 new OverviewToHomeAnim(mLauncher, () -> onSwipeInteractionCompleted(NORMAL), null) 289 .animateWithVelocity(velocity); 290 InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS); 291 } 292 if (mReachedOverview) { 293 float distanceDp = dpiFromPx(Math.max( 294 Math.abs(mRecentsView.getTranslationX()), 295 Math.abs(mRecentsView.getTranslationY()))); 296 long duration = (long) Math.max(TRANSLATION_ANIM_MIN_DURATION_MS, 297 distanceDp / TRANSLATION_ANIM_VELOCITY_DP_PER_MS); 298 mRecentsView.animate() 299 .translationX(0) 300 .translationY(0) 301 .setInterpolator(ACCELERATE_DECELERATE) 302 .setDuration(duration) 303 .withEndAction(goToHomeInsteadOfOverview 304 ? null 305 : this::maybeSwipeInteractionToOverviewComplete); 306 if (!goToHomeInsteadOfOverview) { 307 // Return to normal properties for the overview state. 308 StateAnimationConfig config = new StateAnimationConfig(); 309 config.duration = duration; 310 LauncherState state = mLauncher.getStateManager().getState(); 311 mLauncher.getStateManager().createAtomicAnimation(state, state, config).start(); 312 } 313 } 314 } 315 dpiFromPx(float pixels)316 private float dpiFromPx(float pixels) { 317 return Utilities.dpiFromPx(pixels, mLauncher.getResources().getDisplayMetrics().densityDpi); 318 } 319 } 320