1 package com.android.launcher3.allapps; 2 3 import android.animation.Animator; 4 import android.animation.AnimatorInflater; 5 import android.animation.AnimatorListenerAdapter; 6 import android.animation.AnimatorSet; 7 import android.animation.ArgbEvaluator; 8 import android.animation.ObjectAnimator; 9 import android.graphics.Color; 10 import android.support.animation.SpringAnimation; 11 import android.support.v4.graphics.ColorUtils; 12 import android.support.v4.view.animation.FastOutSlowInInterpolator; 13 import android.view.MotionEvent; 14 import android.view.View; 15 import android.view.animation.AccelerateInterpolator; 16 import android.view.animation.DecelerateInterpolator; 17 import android.view.animation.Interpolator; 18 19 import com.android.launcher3.AbstractFloatingView; 20 import com.android.launcher3.Hotseat; 21 import com.android.launcher3.Launcher; 22 import com.android.launcher3.LauncherAnimUtils; 23 import com.android.launcher3.R; 24 import com.android.launcher3.Utilities; 25 import com.android.launcher3.Workspace; 26 import com.android.launcher3.anim.SpringAnimationHandler; 27 import com.android.launcher3.config.FeatureFlags; 28 import com.android.launcher3.graphics.GradientView; 29 import com.android.launcher3.touch.SwipeDetector; 30 import com.android.launcher3.userevent.nano.LauncherLogProto.Action; 31 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; 32 import com.android.launcher3.util.SystemUiController; 33 import com.android.launcher3.util.Themes; 34 import com.android.launcher3.util.TouchController; 35 36 /** 37 * Handles AllApps view transition. 38 * 1) Slides all apps view using direct manipulation 39 * 2) When finger is released, animate to either top or bottom accordingly. 40 * <p/> 41 * Algorithm: 42 * If release velocity > THRES1, snap according to the direction of movement. 43 * If release velocity < THRES1, snap according to either top or bottom depending on whether it's 44 * closer to top or closer to the page indicator. 45 */ 46 public class AllAppsTransitionController implements TouchController, SwipeDetector.Listener, 47 SearchUiManager.OnScrollRangeChangeListener { 48 49 private static final String TAG = "AllAppsTrans"; 50 private static final boolean DBG = false; 51 52 private final Interpolator mWorkspaceAccelnterpolator = new AccelerateInterpolator(2f); 53 private final Interpolator mHotseatAccelInterpolator = new AccelerateInterpolator(1.5f); 54 private final Interpolator mDecelInterpolator = new DecelerateInterpolator(3f); 55 private final Interpolator mFastOutSlowInInterpolator = new FastOutSlowInInterpolator(); 56 private final SwipeDetector.ScrollInterpolator mScrollInterpolator 57 = new SwipeDetector.ScrollInterpolator(); 58 59 private static final float PARALLAX_COEFFICIENT = .125f; 60 private static final int SINGLE_FRAME_MS = 16; 61 62 private AllAppsContainerView mAppsView; 63 private int mAllAppsBackgroundColor; 64 private Workspace mWorkspace; 65 private Hotseat mHotseat; 66 private int mHotseatBackgroundColor; 67 68 private AllAppsCaretController mCaretController; 69 70 private float mStatusBarHeight; 71 72 private final Launcher mLauncher; 73 private final SwipeDetector mDetector; 74 private final ArgbEvaluator mEvaluator; 75 private final boolean mIsDarkTheme; 76 77 // Animation in this class is controlled by a single variable {@link mProgress}. 78 // Visually, it represents top y coordinate of the all apps container if multiplied with 79 // {@link mShiftRange}. 80 81 // When {@link mProgress} is 0, all apps container is pulled up. 82 // When {@link mProgress} is 1, all apps container is pulled down. 83 private float mShiftStart; // [0, mShiftRange] 84 private float mShiftRange; // changes depending on the orientation 85 private float mProgress; // [0, 1], mShiftRange * mProgress = shiftCurrent 86 87 // Velocity of the container. Unit is in px/ms. 88 private float mContainerVelocity; 89 90 private static final float DEFAULT_SHIFT_RANGE = 10; 91 92 private static final float RECATCH_REJECTION_FRACTION = .0875f; 93 94 private long mAnimationDuration; 95 96 private AnimatorSet mCurrentAnimation; 97 private boolean mNoIntercept; 98 private boolean mTouchEventStartedOnHotseat; 99 100 // Used in discovery bounce animation to provide the transition without workspace changing. 101 private boolean mIsTranslateWithoutWorkspace = false; 102 private Animator mDiscoBounceAnimation; 103 private GradientView mGradientView; 104 105 private SpringAnimation mSearchSpring; 106 private SpringAnimationHandler mSpringAnimationHandler; 107 108 public AllAppsTransitionController(Launcher l) { 109 mLauncher = l; 110 mDetector = new SwipeDetector(l, this, SwipeDetector.VERTICAL); 111 mShiftRange = DEFAULT_SHIFT_RANGE; 112 mProgress = 1f; 113 114 mEvaluator = new ArgbEvaluator(); 115 mAllAppsBackgroundColor = Themes.getAttrColor(l, android.R.attr.colorPrimary); 116 mIsDarkTheme = Themes.getAttrBoolean(mLauncher, R.attr.isMainColorDark); 117 } 118 119 @Override 120 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 121 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 122 mNoIntercept = false; 123 mTouchEventStartedOnHotseat = mLauncher.getDragLayer().isEventOverHotseat(ev); 124 if (!mLauncher.isAllAppsVisible() && mLauncher.getWorkspace().workspaceInModalState()) { 125 mNoIntercept = true; 126 } else if (mLauncher.isAllAppsVisible() && 127 !mAppsView.shouldContainerScroll(ev)) { 128 mNoIntercept = true; 129 } else if (AbstractFloatingView.getTopOpenView(mLauncher) != null) { 130 mNoIntercept = true; 131 } else { 132 // Now figure out which direction scroll events the controller will start 133 // calling the callbacks. 134 int directionsToDetectScroll = 0; 135 boolean ignoreSlopWhenSettling = false; 136 137 if (mDetector.isIdleState()) { 138 if (mLauncher.isAllAppsVisible()) { 139 directionsToDetectScroll |= SwipeDetector.DIRECTION_NEGATIVE; 140 } else { 141 directionsToDetectScroll |= SwipeDetector.DIRECTION_POSITIVE; 142 } 143 } else { 144 if (isInDisallowRecatchBottomZone()) { 145 directionsToDetectScroll |= SwipeDetector.DIRECTION_POSITIVE; 146 } else if (isInDisallowRecatchTopZone()) { 147 directionsToDetectScroll |= SwipeDetector.DIRECTION_NEGATIVE; 148 } else { 149 directionsToDetectScroll |= SwipeDetector.DIRECTION_BOTH; 150 ignoreSlopWhenSettling = true; 151 } 152 } 153 mDetector.setDetectableScrollConditions(directionsToDetectScroll, 154 ignoreSlopWhenSettling); 155 } 156 } 157 158 if (mNoIntercept) { 159 return false; 160 } 161 mDetector.onTouchEvent(ev); 162 if (mDetector.isSettlingState() && (isInDisallowRecatchBottomZone() || isInDisallowRecatchTopZone())) { 163 return false; 164 } 165 return mDetector.isDraggingOrSettling(); 166 } 167 168 @Override 169 public boolean onControllerTouchEvent(MotionEvent ev) { 170 if (hasSpringAnimationHandler()) { 171 mSpringAnimationHandler.addMovement(ev); 172 } 173 return mDetector.onTouchEvent(ev); 174 } 175 176 private boolean isInDisallowRecatchTopZone() { 177 return mProgress < RECATCH_REJECTION_FRACTION; 178 } 179 180 private boolean isInDisallowRecatchBottomZone() { 181 return mProgress > 1 - RECATCH_REJECTION_FRACTION; 182 } 183 184 @Override 185 public void onDragStart(boolean start) { 186 mCaretController.onDragStart(); 187 cancelAnimation(); 188 mCurrentAnimation = LauncherAnimUtils.createAnimatorSet(); 189 mShiftStart = mAppsView.getTranslationY(); 190 preparePull(start); 191 if (hasSpringAnimationHandler()) { 192 mSpringAnimationHandler.skipToEnd(); 193 } 194 } 195 196 @Override 197 public boolean onDrag(float displacement, float velocity) { 198 if (mAppsView == null) { 199 return false; // early termination. 200 } 201 202 mContainerVelocity = velocity; 203 204 float shift = Math.min(Math.max(0, mShiftStart + displacement), mShiftRange); 205 setProgress(shift / mShiftRange); 206 207 return true; 208 } 209 210 @Override 211 public void onDragEnd(float velocity, boolean fling) { 212 if (mAppsView == null) { 213 return; // early termination. 214 } 215 216 final int containerType = mTouchEventStartedOnHotseat 217 ? ContainerType.HOTSEAT : ContainerType.WORKSPACE; 218 219 if (fling) { 220 if (velocity < 0) { 221 calculateDuration(velocity, mAppsView.getTranslationY()); 222 223 if (!mLauncher.isAllAppsVisible()) { 224 mLauncher.getUserEventDispatcher().logActionOnContainer( 225 Action.Touch.FLING, 226 Action.Direction.UP, 227 containerType); 228 } 229 mLauncher.showAppsView(true /* animated */, false /* updatePredictedApps */); 230 if (hasSpringAnimationHandler()) { 231 mSpringAnimationHandler.add(mSearchSpring, true /* setDefaultValues */); 232 // The icons are moving upwards, so we go to 0 from 1. (y-axis 1 is below 0.) 233 mSpringAnimationHandler.animateToFinalPosition(0 /* pos */, 1 /* startValue */); 234 } 235 } else { 236 calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY())); 237 mLauncher.showWorkspace(true); 238 } 239 // snap to top or bottom using the release velocity 240 } else { 241 if (mAppsView.getTranslationY() > mShiftRange / 2) { 242 calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY())); 243 mLauncher.showWorkspace(true); 244 } else { 245 calculateDuration(velocity, Math.abs(mAppsView.getTranslationY())); 246 if (!mLauncher.isAllAppsVisible()) { 247 mLauncher.getUserEventDispatcher().logActionOnContainer( 248 Action.Touch.SWIPE, 249 Action.Direction.UP, 250 containerType); 251 } 252 mLauncher.showAppsView(true, /* animated */ false /* updatePredictedApps */); 253 } 254 } 255 } 256 257 public boolean isTransitioning() { 258 return mDetector.isDraggingOrSettling(); 259 } 260 261 /** 262 * @param start {@code true} if start of new drag. 263 */ 264 public void preparePull(boolean start) { 265 if (start) { 266 // Initialize values that should not change until #onDragEnd 267 mStatusBarHeight = mLauncher.getDragLayer().getInsets().top; 268 mHotseat.setVisibility(View.VISIBLE); 269 mHotseatBackgroundColor = mHotseat.getBackgroundDrawableColor(); 270 mHotseat.setBackgroundTransparent(true /* transparent */); 271 if (!mLauncher.isAllAppsVisible()) { 272 mLauncher.tryAndUpdatePredictedApps(); 273 mAppsView.setVisibility(View.VISIBLE); 274 if (!FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS) { 275 mAppsView.setRevealDrawableColor(mHotseatBackgroundColor); 276 } 277 } 278 } 279 } 280 281 private void updateLightStatusBar(float shift) { 282 // Do not modify status bar on landscape as all apps is not full bleed. 283 if (!FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS 284 && mLauncher.getDeviceProfile().isVerticalBarLayout()) { 285 return; 286 } 287 288 // Use a light system UI (dark icons) if all apps is behind at least half of the status bar. 289 boolean forceChange = FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS ? 290 shift <= mShiftRange / 4 : 291 shift <= mStatusBarHeight / 2; 292 if (forceChange) { 293 mLauncher.getSystemUiController().updateUiState( 294 SystemUiController.UI_STATE_ALL_APPS, !mIsDarkTheme); 295 } else { 296 mLauncher.getSystemUiController().updateUiState( 297 SystemUiController.UI_STATE_ALL_APPS, 0); 298 } 299 } 300 301 private void updateAllAppsBg(float progress) { 302 // gradient 303 if (mGradientView == null) { 304 mGradientView = (GradientView) mLauncher.findViewById(R.id.gradient_bg); 305 mGradientView.setVisibility(View.VISIBLE); 306 } 307 mGradientView.setProgress(progress); 308 } 309 310 /** 311 * @param progress value between 0 and 1, 0 shows all apps and 1 shows workspace 312 */ 313 public void setProgress(float progress) { 314 float shiftPrevious = mProgress * mShiftRange; 315 mProgress = progress; 316 float shiftCurrent = progress * mShiftRange; 317 318 float workspaceHotseatAlpha = Utilities.boundToRange(progress, 0f, 1f); 319 float alpha = 1 - workspaceHotseatAlpha; 320 float workspaceAlpha = mWorkspaceAccelnterpolator.getInterpolation(workspaceHotseatAlpha); 321 float hotseatAlpha = mHotseatAccelInterpolator.getInterpolation(workspaceHotseatAlpha); 322 323 int color = (Integer) mEvaluator.evaluate(mDecelInterpolator.getInterpolation(alpha), 324 mHotseatBackgroundColor, mAllAppsBackgroundColor); 325 int bgAlpha = Color.alpha((int) mEvaluator.evaluate(alpha, 326 mHotseatBackgroundColor, mAllAppsBackgroundColor)); 327 328 if (FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS) { 329 updateAllAppsBg(alpha); 330 } else { 331 mAppsView.setRevealDrawableColor(ColorUtils.setAlphaComponent(color, bgAlpha)); 332 } 333 334 mAppsView.getContentView().setAlpha(alpha); 335 mAppsView.setTranslationY(shiftCurrent); 336 337 if (!mLauncher.getDeviceProfile().isVerticalBarLayout()) { 338 mWorkspace.setHotseatTranslationAndAlpha(Workspace.Direction.Y, -mShiftRange + shiftCurrent, 339 hotseatAlpha); 340 } else { 341 mWorkspace.setHotseatTranslationAndAlpha(Workspace.Direction.Y, 342 PARALLAX_COEFFICIENT * (-mShiftRange + shiftCurrent), 343 hotseatAlpha); 344 } 345 346 if (mIsTranslateWithoutWorkspace) { 347 return; 348 } 349 mWorkspace.setWorkspaceYTranslationAndAlpha( 350 PARALLAX_COEFFICIENT * (-mShiftRange + shiftCurrent), workspaceAlpha); 351 352 if (!mDetector.isDraggingState()) { 353 mContainerVelocity = mDetector.computeVelocity(shiftCurrent - shiftPrevious, 354 System.currentTimeMillis()); 355 } 356 357 mCaretController.updateCaret(progress, mContainerVelocity, mDetector.isDraggingState()); 358 updateLightStatusBar(shiftCurrent); 359 } 360 361 public float getProgress() { 362 return mProgress; 363 } 364 365 private void calculateDuration(float velocity, float disp) { 366 mAnimationDuration = SwipeDetector.calculateDuration(velocity, disp / mShiftRange); 367 } 368 369 public boolean animateToAllApps(AnimatorSet animationOut, long duration) { 370 boolean shouldPost = true; 371 if (animationOut == null) { 372 return shouldPost; 373 } 374 Interpolator interpolator; 375 if (mDetector.isIdleState()) { 376 preparePull(true); 377 mAnimationDuration = duration; 378 mShiftStart = mAppsView.getTranslationY(); 379 interpolator = mFastOutSlowInInterpolator; 380 } else { 381 mScrollInterpolator.setVelocityAtZero(Math.abs(mContainerVelocity)); 382 interpolator = mScrollInterpolator; 383 float nextFrameProgress = mProgress + mContainerVelocity * SINGLE_FRAME_MS / mShiftRange; 384 if (nextFrameProgress >= 0f) { 385 mProgress = nextFrameProgress; 386 } 387 shouldPost = false; 388 } 389 390 ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress", 391 mProgress, 0f); 392 driftAndAlpha.setDuration(mAnimationDuration); 393 driftAndAlpha.setInterpolator(interpolator); 394 animationOut.play(driftAndAlpha); 395 396 animationOut.addListener(new AnimatorListenerAdapter() { 397 boolean canceled = false; 398 399 @Override 400 public void onAnimationCancel(Animator animation) { 401 canceled = true; 402 } 403 404 @Override 405 public void onAnimationEnd(Animator animation) { 406 if (canceled) { 407 return; 408 } else { 409 finishPullUp(); 410 cleanUpAnimation(); 411 mDetector.finishedScrolling(); 412 } 413 } 414 }); 415 mCurrentAnimation = animationOut; 416 return shouldPost; 417 } 418 419 public void showDiscoveryBounce() { 420 // cancel existing animation in case user locked and unlocked at a super human speed. 421 cancelDiscoveryAnimation(); 422 423 // assumption is that this variable is always null 424 mDiscoBounceAnimation = AnimatorInflater.loadAnimator(mLauncher, 425 R.animator.discovery_bounce); 426 mDiscoBounceAnimation.addListener(new AnimatorListenerAdapter() { 427 @Override 428 public void onAnimationStart(Animator animator) { 429 mIsTranslateWithoutWorkspace = true; 430 preparePull(true); 431 } 432 433 @Override 434 public void onAnimationEnd(Animator animator) { 435 finishPullDown(); 436 mDiscoBounceAnimation = null; 437 mIsTranslateWithoutWorkspace = false; 438 } 439 }); 440 mDiscoBounceAnimation.setTarget(this); 441 mAppsView.post(new Runnable() { 442 @Override 443 public void run() { 444 if (mDiscoBounceAnimation == null) { 445 return; 446 } 447 mDiscoBounceAnimation.start(); 448 } 449 }); 450 } 451 452 public boolean animateToWorkspace(AnimatorSet animationOut, long duration) { 453 boolean shouldPost = true; 454 if (animationOut == null) { 455 return shouldPost; 456 } 457 Interpolator interpolator; 458 if (mDetector.isIdleState()) { 459 preparePull(true); 460 mAnimationDuration = duration; 461 mShiftStart = mAppsView.getTranslationY(); 462 interpolator = mFastOutSlowInInterpolator; 463 } else { 464 mScrollInterpolator.setVelocityAtZero(Math.abs(mContainerVelocity)); 465 interpolator = mScrollInterpolator; 466 float nextFrameProgress = mProgress + mContainerVelocity * SINGLE_FRAME_MS / mShiftRange; 467 if (nextFrameProgress <= 1f) { 468 mProgress = nextFrameProgress; 469 } 470 shouldPost = false; 471 } 472 473 ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress", 474 mProgress, 1f); 475 driftAndAlpha.setDuration(mAnimationDuration); 476 driftAndAlpha.setInterpolator(interpolator); 477 animationOut.play(driftAndAlpha); 478 479 animationOut.addListener(new AnimatorListenerAdapter() { 480 boolean canceled = false; 481 482 @Override 483 public void onAnimationCancel(Animator animation) { 484 canceled = true; 485 } 486 487 @Override 488 public void onAnimationEnd(Animator animation) { 489 if (canceled) { 490 return; 491 } else { 492 finishPullDown(); 493 cleanUpAnimation(); 494 mDetector.finishedScrolling(); 495 } 496 } 497 }); 498 mCurrentAnimation = animationOut; 499 return shouldPost; 500 } 501 502 public void finishPullUp() { 503 mHotseat.setVisibility(View.INVISIBLE); 504 if (hasSpringAnimationHandler()) { 505 mSpringAnimationHandler.remove(mSearchSpring); 506 mSpringAnimationHandler.reset(); 507 } 508 setProgress(0f); 509 } 510 511 public void finishPullDown() { 512 mAppsView.setVisibility(View.INVISIBLE); 513 mHotseat.setBackgroundTransparent(false /* transparent */); 514 mHotseat.setVisibility(View.VISIBLE); 515 mAppsView.reset(); 516 if (hasSpringAnimationHandler()) { 517 mSpringAnimationHandler.reset(); 518 } 519 setProgress(1f); 520 } 521 522 private void cancelAnimation() { 523 if (mCurrentAnimation != null) { 524 mCurrentAnimation.cancel(); 525 mCurrentAnimation = null; 526 } 527 cancelDiscoveryAnimation(); 528 } 529 530 public void cancelDiscoveryAnimation() { 531 if (mDiscoBounceAnimation == null) { 532 return; 533 } 534 mDiscoBounceAnimation.cancel(); 535 mDiscoBounceAnimation = null; 536 } 537 538 private void cleanUpAnimation() { 539 mCurrentAnimation = null; 540 } 541 542 public void setupViews(AllAppsContainerView appsView, Hotseat hotseat, Workspace workspace) { 543 mAppsView = appsView; 544 mHotseat = hotseat; 545 mWorkspace = workspace; 546 mHotseat.bringToFront(); 547 mCaretController = new AllAppsCaretController( 548 mWorkspace.getPageIndicator().getCaretDrawable(), mLauncher); 549 mAppsView.getSearchUiManager().addOnScrollRangeChangeListener(this); 550 mSpringAnimationHandler = mAppsView.getSpringAnimationHandler(); 551 mSearchSpring = mAppsView.getSearchUiManager().getSpringForFling(); 552 } 553 554 private boolean hasSpringAnimationHandler() { 555 return FeatureFlags.LAUNCHER3_PHYSICS && mSpringAnimationHandler != null; 556 } 557 558 @Override 559 public void onScrollRangeChanged(int scrollRange) { 560 mShiftRange = scrollRange; 561 setProgress(mProgress); 562 } 563 } 564