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