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