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