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 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 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 155 public boolean onControllerTouchEvent(MotionEvent ev) { 156 return mDetector.onTouchEvent(ev); 157 } 158 159 private boolean isInDisallowRecatchTopZone() { 160 return mProgress < RECATCH_REJECTION_FRACTION; 161 } 162 163 private boolean isInDisallowRecatchBottomZone() { 164 return mProgress > 1 - RECATCH_REJECTION_FRACTION; 165 } 166 167 @Override 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 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 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 233 public boolean isTransitioning() { 234 return mDetector.isDraggingOrSettling(); 235 } 236 237 /** 238 * @param start {@code true} if start of new drag. 239 */ 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 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 */ 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 311 public float getProgress() { 312 return mProgress; 313 } 314 315 private void calculateDuration(float velocity, float disp) { 316 mAnimationDuration = mDetector.calculateDuration(velocity, disp / mShiftRange); 317 } 318 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 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 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 452 public void finishPullUp() { 453 mHotseat.setVisibility(View.INVISIBLE); 454 setProgress(0f); 455 } 456 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 465 private void cancelAnimation() { 466 if (mCurrentAnimation != null) { 467 mCurrentAnimation.cancel(); 468 mCurrentAnimation = null; 469 } 470 cancelDiscoveryAnimation(); 471 } 472 473 public void cancelDiscoveryAnimation() { 474 if (mDiscoBounceAnimation == null) { 475 return; 476 } 477 mDiscoBounceAnimation.cancel(); 478 mDiscoBounceAnimation = null; 479 } 480 481 private void cleanUpAnimation() { 482 mCurrentAnimation = null; 483 } 484 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 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