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