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.touch; 17 18 import static com.android.launcher3.LauncherAnimUtils.MIN_PROGRESS_TO_ALL_APPS; 19 import static com.android.launcher3.LauncherState.ALL_APPS; 20 import static com.android.launcher3.LauncherState.NORMAL; 21 import static com.android.launcher3.LauncherState.OVERVIEW; 22 import static com.android.launcher3.LauncherStateManager.ANIM_ALL; 23 import static com.android.launcher3.LauncherStateManager.ATOMIC_COMPONENT; 24 import static com.android.launcher3.LauncherStateManager.NON_ATOMIC_COMPONENT; 25 import static com.android.launcher3.Utilities.SINGLE_FRAME_MS; 26 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity; 27 28 import android.animation.Animator; 29 import android.animation.AnimatorListenerAdapter; 30 import android.animation.AnimatorSet; 31 import android.animation.ValueAnimator; 32 import android.view.HapticFeedbackConstants; 33 import android.view.MotionEvent; 34 35 import com.android.launcher3.Launcher; 36 import com.android.launcher3.LauncherAnimUtils; 37 import com.android.launcher3.LauncherState; 38 import com.android.launcher3.LauncherStateManager.AnimationComponents; 39 import com.android.launcher3.LauncherStateManager.AnimationConfig; 40 import com.android.launcher3.LauncherStateManager.StateHandler; 41 import com.android.launcher3.Utilities; 42 import com.android.launcher3.anim.AnimationSuccessListener; 43 import com.android.launcher3.anim.AnimatorPlaybackController; 44 import com.android.launcher3.anim.AnimatorSetBuilder; 45 import com.android.launcher3.userevent.nano.LauncherLogProto; 46 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; 47 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; 48 import com.android.launcher3.util.FlingBlockCheck; 49 import com.android.launcher3.util.PendingAnimation; 50 import com.android.launcher3.util.TouchController; 51 52 /** 53 * TouchController for handling state changes 54 */ 55 public abstract class AbstractStateChangeTouchController 56 implements TouchController, SwipeDetector.Listener { 57 58 private static final String TAG = "ASCTouchController"; 59 60 // Progress after which the transition is assumed to be a success in case user does not fling 61 public static final float SUCCESS_TRANSITION_PROGRESS = 0.5f; 62 63 /** 64 * Play an atomic recents animation when the progress from NORMAL to OVERVIEW reaches this. 65 */ 66 public static final float ATOMIC_OVERVIEW_ANIM_THRESHOLD = 0.5f; 67 protected static final long ATOMIC_DURATION = 200; 68 69 protected final Launcher mLauncher; 70 protected final SwipeDetector mDetector; 71 72 private boolean mNoIntercept; 73 protected int mStartContainerType; 74 75 protected LauncherState mStartState; 76 protected LauncherState mFromState; 77 protected LauncherState mToState; 78 protected AnimatorPlaybackController mCurrentAnimation; 79 protected PendingAnimation mPendingAnimation; 80 81 private float mStartProgress; 82 // Ratio of transition process [0, 1] to drag displacement (px) 83 private float mProgressMultiplier; 84 private float mDisplacementShift; 85 private boolean mCanBlockFling; 86 private FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck(); 87 88 private AnimatorSet mAtomicAnim; 89 private boolean mPassedOverviewAtomicThreshold; 90 // mAtomicAnim plays the atomic components of the state animations when we pass the threshold. 91 // However, if we reinit to transition to a new state (e.g. OVERVIEW -> ALL_APPS) before the 92 // atomic animation finishes, we only control the non-atomic components so that we don't 93 // interfere with the atomic animation. When the atomic animation ends, we start controlling 94 // the atomic components as well, using this controller. 95 private AnimatorPlaybackController mAtomicComponentsController; 96 private float mAtomicComponentsStartProgress; 97 AbstractStateChangeTouchController(Launcher l, SwipeDetector.Direction dir)98 public AbstractStateChangeTouchController(Launcher l, SwipeDetector.Direction dir) { 99 mLauncher = l; 100 mDetector = new SwipeDetector(l, this, dir); 101 } 102 canInterceptTouch(MotionEvent ev)103 protected abstract boolean canInterceptTouch(MotionEvent ev); 104 105 @Override onControllerInterceptTouchEvent(MotionEvent ev)106 public final boolean onControllerInterceptTouchEvent(MotionEvent ev) { 107 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 108 mNoIntercept = !canInterceptTouch(ev); 109 if (mNoIntercept) { 110 return false; 111 } 112 113 // Now figure out which direction scroll events the controller will start 114 // calling the callbacks. 115 final int directionsToDetectScroll; 116 boolean ignoreSlopWhenSettling = false; 117 118 if (mCurrentAnimation != null) { 119 directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH; 120 ignoreSlopWhenSettling = true; 121 } else { 122 directionsToDetectScroll = getSwipeDirection(); 123 if (directionsToDetectScroll == 0) { 124 mNoIntercept = true; 125 return false; 126 } 127 } 128 mDetector.setDetectableScrollConditions( 129 directionsToDetectScroll, ignoreSlopWhenSettling); 130 } 131 132 if (mNoIntercept) { 133 return false; 134 } 135 136 onControllerTouchEvent(ev); 137 return mDetector.isDraggingOrSettling(); 138 } 139 getSwipeDirection()140 private int getSwipeDirection() { 141 LauncherState fromState = mLauncher.getStateManager().getState(); 142 int swipeDirection = 0; 143 if (getTargetState(fromState, true /* isDragTowardPositive */) != fromState) { 144 swipeDirection |= SwipeDetector.DIRECTION_POSITIVE; 145 } 146 if (getTargetState(fromState, false /* isDragTowardPositive */) != fromState) { 147 swipeDirection |= SwipeDetector.DIRECTION_NEGATIVE; 148 } 149 return swipeDirection; 150 } 151 152 @Override onControllerTouchEvent(MotionEvent ev)153 public final boolean onControllerTouchEvent(MotionEvent ev) { 154 return mDetector.onTouchEvent(ev); 155 } 156 getShiftRange()157 protected float getShiftRange() { 158 return mLauncher.getAllAppsController().getShiftRange(); 159 } 160 161 /** 162 * Returns the state to go to from fromState given the drag direction. If there is no state in 163 * that direction, returns fromState. 164 */ getTargetState(LauncherState fromState, boolean isDragTowardPositive)165 protected abstract LauncherState getTargetState(LauncherState fromState, 166 boolean isDragTowardPositive); 167 initCurrentAnimation(@nimationComponents int animComponents)168 protected abstract float initCurrentAnimation(@AnimationComponents int animComponents); 169 170 /** 171 * Returns the container that the touch started from when leaving NORMAL state. 172 */ getLogContainerTypeForNormalState()173 protected abstract int getLogContainerTypeForNormalState(); 174 reinitCurrentAnimation(boolean reachedToState, boolean isDragTowardPositive)175 private boolean reinitCurrentAnimation(boolean reachedToState, boolean isDragTowardPositive) { 176 LauncherState newFromState = mFromState == null ? mLauncher.getStateManager().getState() 177 : reachedToState ? mToState : mFromState; 178 LauncherState newToState = getTargetState(newFromState, isDragTowardPositive); 179 180 if (newFromState == mFromState && newToState == mToState || (newFromState == newToState)) { 181 return false; 182 } 183 184 mFromState = newFromState; 185 mToState = newToState; 186 187 mStartProgress = 0; 188 mPassedOverviewAtomicThreshold = false; 189 if (mCurrentAnimation != null) { 190 mCurrentAnimation.setOnCancelRunnable(null); 191 } 192 int animComponents = goingBetweenNormalAndOverview(mFromState, mToState) 193 ? NON_ATOMIC_COMPONENT : ANIM_ALL; 194 if (mAtomicAnim != null) { 195 // Control the non-atomic components until the atomic animation finishes, then control 196 // the atomic components as well. 197 animComponents = NON_ATOMIC_COMPONENT; 198 mAtomicAnim.addListener(new AnimationSuccessListener() { 199 @Override 200 public void onAnimationSuccess(Animator animation) { 201 cancelAtomicComponentsController(); 202 if (mCurrentAnimation != null) { 203 mAtomicComponentsStartProgress = mCurrentAnimation.getProgressFraction(); 204 long duration = (long) (getShiftRange() * 2); 205 mAtomicComponentsController = AnimatorPlaybackController.wrap( 206 createAtomicAnimForState(mFromState, mToState, duration), duration); 207 mAtomicComponentsController.dispatchOnStart(); 208 } 209 } 210 }); 211 } 212 if (goingBetweenNormalAndOverview(mFromState, mToState)) { 213 cancelAtomicComponentsController(); 214 } 215 mProgressMultiplier = initCurrentAnimation(animComponents); 216 mCurrentAnimation.dispatchOnStart(); 217 return true; 218 } 219 goingBetweenNormalAndOverview(LauncherState fromState, LauncherState toState)220 private boolean goingBetweenNormalAndOverview(LauncherState fromState, LauncherState toState) { 221 return (fromState == NORMAL || fromState == OVERVIEW) 222 && (toState == NORMAL || toState == OVERVIEW) 223 && mPendingAnimation == null; 224 } 225 226 @Override onDragStart(boolean start)227 public void onDragStart(boolean start) { 228 mStartState = mLauncher.getStateManager().getState(); 229 if (mStartState == ALL_APPS) { 230 mStartContainerType = LauncherLogProto.ContainerType.ALLAPPS; 231 } else if (mStartState == NORMAL) { 232 mStartContainerType = getLogContainerTypeForNormalState(); 233 } else if (mStartState == OVERVIEW){ 234 mStartContainerType = LauncherLogProto.ContainerType.TASKSWITCHER; 235 } 236 if (mCurrentAnimation == null) { 237 mFromState = mStartState; 238 mToState = null; 239 mAtomicComponentsController = null; 240 reinitCurrentAnimation(false, mDetector.wasInitialTouchPositive()); 241 mDisplacementShift = 0; 242 } else { 243 mCurrentAnimation.pause(); 244 mStartProgress = mCurrentAnimation.getProgressFraction(); 245 } 246 mCanBlockFling = mFromState == NORMAL; 247 mFlingBlockCheck.unblockFling(); 248 } 249 250 @Override onDrag(float displacement, float velocity)251 public boolean onDrag(float displacement, float velocity) { 252 float deltaProgress = mProgressMultiplier * (displacement - mDisplacementShift); 253 float progress = deltaProgress + mStartProgress; 254 updateProgress(progress); 255 boolean isDragTowardPositive = (displacement - mDisplacementShift) < 0; 256 if (progress <= 0) { 257 if (reinitCurrentAnimation(false, isDragTowardPositive)) { 258 mDisplacementShift = displacement; 259 if (mCanBlockFling) { 260 mFlingBlockCheck.blockFling(); 261 } 262 } 263 } else if (progress >= 1) { 264 if (reinitCurrentAnimation(true, isDragTowardPositive)) { 265 mDisplacementShift = displacement; 266 if (mCanBlockFling) { 267 mFlingBlockCheck.blockFling(); 268 } 269 } 270 } else { 271 mFlingBlockCheck.onEvent(); 272 } 273 274 return true; 275 } 276 277 protected void updateProgress(float fraction) { 278 mCurrentAnimation.setPlayFraction(fraction); 279 if (mAtomicComponentsController != null) { 280 mAtomicComponentsController.setPlayFraction(fraction - mAtomicComponentsStartProgress); 281 } 282 maybeUpdateAtomicAnim(mFromState, mToState, fraction); 283 } 284 285 /** 286 * When going between normal and overview states, see if we passed the overview threshold and 287 * play the appropriate atomic animation if so. 288 */ 289 private void maybeUpdateAtomicAnim(LauncherState fromState, LauncherState toState, 290 float progress) { 291 if (!goingBetweenNormalAndOverview(fromState, toState)) { 292 return; 293 } 294 float threshold = toState == OVERVIEW ? ATOMIC_OVERVIEW_ANIM_THRESHOLD 295 : 1f - ATOMIC_OVERVIEW_ANIM_THRESHOLD; 296 boolean passedThreshold = progress >= threshold; 297 if (passedThreshold != mPassedOverviewAtomicThreshold) { 298 LauncherState atomicFromState = passedThreshold ? fromState: toState; 299 LauncherState atomicToState = passedThreshold ? toState : fromState; 300 mPassedOverviewAtomicThreshold = passedThreshold; 301 if (mAtomicAnim != null) { 302 mAtomicAnim.cancel(); 303 } 304 mAtomicAnim = createAtomicAnimForState(atomicFromState, atomicToState, ATOMIC_DURATION); 305 mAtomicAnim.addListener(new AnimatorListenerAdapter() { 306 @Override 307 public void onAnimationEnd(Animator animation) { 308 mAtomicAnim = null; 309 } 310 }); 311 mAtomicAnim.start(); 312 mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK); 313 } 314 } 315 createAtomicAnimForState(LauncherState fromState, LauncherState targetState, long duration)316 private AnimatorSet createAtomicAnimForState(LauncherState fromState, LauncherState targetState, 317 long duration) { 318 AnimatorSetBuilder builder = new AnimatorSetBuilder(); 319 mLauncher.getStateManager().prepareForAtomicAnimation(fromState, targetState, builder); 320 AnimationConfig config = new AnimationConfig(); 321 config.animComponents = ATOMIC_COMPONENT; 322 config.duration = duration; 323 for (StateHandler handler : mLauncher.getStateManager().getStateHandlers()) { 324 handler.setStateWithAnimation(targetState, builder, config); 325 } 326 return builder.build(); 327 } 328 329 @Override onDragEnd(float velocity, boolean fling)330 public void onDragEnd(float velocity, boolean fling) { 331 final int logAction = fling ? Touch.FLING : Touch.SWIPE; 332 333 boolean blockedFling = fling && mFlingBlockCheck.isBlocked(); 334 if (blockedFling) { 335 fling = false; 336 } 337 338 final LauncherState targetState; 339 final float progress = mCurrentAnimation.getProgressFraction(); 340 if (fling) { 341 targetState = 342 Float.compare(Math.signum(velocity), Math.signum(mProgressMultiplier)) == 0 343 ? mToState : mFromState; 344 // snap to top or bottom using the release velocity 345 } else { 346 float successProgress = mToState == ALL_APPS 347 ? MIN_PROGRESS_TO_ALL_APPS : SUCCESS_TRANSITION_PROGRESS; 348 targetState = (progress > successProgress) ? mToState : mFromState; 349 } 350 351 final float endProgress; 352 final float startProgress; 353 final long duration; 354 // Increase the duration if we prevented the fling, as we are going against a high velocity. 355 final int durationMultiplier = blockedFling && targetState == mFromState 356 ? LauncherAnimUtils.blockedFlingDurationFactor(velocity) : 1; 357 358 if (targetState == mToState) { 359 endProgress = 1; 360 if (progress >= 1) { 361 duration = 0; 362 startProgress = 1; 363 } else { 364 startProgress = Utilities.boundToRange( 365 progress + velocity * SINGLE_FRAME_MS * mProgressMultiplier, 0f, 1f); 366 duration = SwipeDetector.calculateDuration(velocity, 367 endProgress - Math.max(progress, 0)) * durationMultiplier; 368 } 369 } else { 370 // Let the state manager know that the animation didn't go to the target state, 371 // but don't cancel ourselves (we already clean up when the animation completes). 372 Runnable onCancel = mCurrentAnimation.getOnCancelRunnable(); 373 mCurrentAnimation.setOnCancelRunnable(null); 374 mCurrentAnimation.dispatchOnCancel(); 375 mCurrentAnimation.setOnCancelRunnable(onCancel); 376 377 endProgress = 0; 378 if (progress <= 0) { 379 duration = 0; 380 startProgress = 0; 381 } else { 382 startProgress = Utilities.boundToRange( 383 progress + velocity * SINGLE_FRAME_MS * mProgressMultiplier, 0f, 1f); 384 duration = SwipeDetector.calculateDuration(velocity, 385 Math.min(progress, 1) - endProgress) * durationMultiplier; 386 } 387 } 388 389 mCurrentAnimation.setEndAction(() -> onSwipeInteractionCompleted(targetState, logAction)); 390 ValueAnimator anim = mCurrentAnimation.getAnimationPlayer(); 391 anim.setFloatValues(startProgress, endProgress); 392 maybeUpdateAtomicAnim(mFromState, targetState, targetState == mToState ? 1f : 0f); 393 updateSwipeCompleteAnimation(anim, Math.max(duration, getRemainingAtomicDuration()), 394 targetState, velocity, fling); 395 mCurrentAnimation.dispatchOnStart(); 396 if (fling && targetState == LauncherState.ALL_APPS) { 397 mLauncher.getAppsView().addSpringFromFlingUpdateListener(anim, velocity); 398 } 399 anim.start(); 400 if (mAtomicAnim == null) { 401 startAtomicComponentsAnim(endProgress, anim.getDuration()); 402 } else { 403 mAtomicAnim.addListener(new AnimationSuccessListener() { 404 @Override 405 public void onAnimationSuccess(Animator animator) { 406 startAtomicComponentsAnim(endProgress, anim.getDuration()); 407 } 408 }); 409 } 410 } 411 412 /** 413 * Animates the atomic components from the current progress to the final progress. 414 * 415 * Note that this only applies when we are controlling the atomic components separately from 416 * the non-atomic components, which only happens if we reinit before the atomic animation 417 * finishes. 418 */ startAtomicComponentsAnim(float toProgress, long duration)419 private void startAtomicComponentsAnim(float toProgress, long duration) { 420 if (mAtomicComponentsController != null) { 421 ValueAnimator atomicAnim = mAtomicComponentsController.getAnimationPlayer(); 422 atomicAnim.setFloatValues(mAtomicComponentsController.getProgressFraction(), toProgress); 423 atomicAnim.setDuration(duration); 424 atomicAnim.start(); 425 atomicAnim.addListener(new AnimatorListenerAdapter() { 426 @Override 427 public void onAnimationEnd(Animator animation) { 428 mAtomicComponentsController = null; 429 } 430 }); 431 } 432 } 433 getRemainingAtomicDuration()434 private long getRemainingAtomicDuration() { 435 if (mAtomicAnim == null) { 436 return 0; 437 } 438 if (Utilities.ATLEAST_OREO) { 439 return mAtomicAnim.getTotalDuration() - mAtomicAnim.getCurrentPlayTime(); 440 } else { 441 long remainingDuration = 0; 442 for (Animator anim : mAtomicAnim.getChildAnimations()) { 443 remainingDuration = Math.max(remainingDuration, anim.getDuration()); 444 } 445 return remainingDuration; 446 } 447 } 448 updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration, LauncherState targetState, float velocity, boolean isFling)449 protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration, 450 LauncherState targetState, float velocity, boolean isFling) { 451 animator.setDuration(expectedDuration) 452 .setInterpolator(scrollInterpolatorForVelocity(velocity)); 453 } 454 getDirectionForLog()455 protected int getDirectionForLog() { 456 return mToState.ordinal > mFromState.ordinal ? Direction.UP : Direction.DOWN; 457 } 458 onSwipeInteractionCompleted(LauncherState targetState, int logAction)459 protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) { 460 clearState(); 461 boolean shouldGoToTargetState = true; 462 if (mPendingAnimation != null) { 463 boolean reachedTarget = mToState == targetState; 464 mPendingAnimation.finish(reachedTarget, logAction); 465 mPendingAnimation = null; 466 shouldGoToTargetState = !reachedTarget; 467 } 468 if (shouldGoToTargetState) { 469 if (targetState != mStartState) { 470 logReachedState(logAction, targetState); 471 } 472 mLauncher.getStateManager().goToState(targetState, false /* animated */); 473 } 474 } 475 logReachedState(int logAction, LauncherState targetState)476 private void logReachedState(int logAction, LauncherState targetState) { 477 // Transition complete. log the action 478 mLauncher.getUserEventDispatcher().logStateChangeAction(logAction, 479 getDirectionForLog(), 480 mStartContainerType, 481 mStartState.containerType, 482 targetState.containerType, 483 mLauncher.getWorkspace().getCurrentPage()); 484 } 485 clearState()486 protected void clearState() { 487 mCurrentAnimation = null; 488 cancelAtomicComponentsController(); 489 mDetector.finishedScrolling(); 490 mDetector.setDetectableScrollConditions(0, false); 491 } 492 cancelAtomicComponentsController()493 private void cancelAtomicComponentsController() { 494 if (mAtomicComponentsController != null) { 495 mAtomicComponentsController.getAnimationPlayer().cancel(); 496 mAtomicComponentsController = null; 497 } 498 } 499 } 500