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