AllAppsTransitionController.java revision 4325a56f5359a83164692ae6109d6463f792bf13
1package com.android.launcher3.allapps; 2 3import android.animation.Animator; 4import android.animation.AnimatorListenerAdapter; 5import android.animation.AnimatorSet; 6import android.animation.ObjectAnimator; 7import android.util.Log; 8import android.view.MotionEvent; 9import android.view.View; 10import android.view.animation.AccelerateInterpolator; 11import android.view.animation.DecelerateInterpolator; 12import android.view.animation.Interpolator; 13 14import com.android.launcher3.CellLayout; 15import com.android.launcher3.DeviceProfile; 16import com.android.launcher3.Hotseat; 17import com.android.launcher3.Launcher; 18import com.android.launcher3.LauncherAnimUtils; 19import com.android.launcher3.PagedView; 20import com.android.launcher3.R; 21import com.android.launcher3.Workspace; 22import com.android.launcher3.Workspace.Direction; 23import com.android.launcher3.util.TouchController; 24 25/** 26 * Handles AllApps view transition. 27 * 1) Slides all apps view using direct manipulation 28 * 2) When finger is released, animate to either top or bottom accordingly. 29 * 30 * Algorithm: 31 * If release velocity > THRES1, snap according to the direction of movement. 32 * If release velocity < THRES1, snap according to either top or bottom depending on whether it's 33 * closer to top or closer to the page indicator. 34 */ 35public class AllAppsTransitionController implements TouchController, VerticalPullDetector.Listener { 36 37 private static final String TAG = "AllAppsTrans"; 38 private static final boolean DBG = false; 39 40 private final Interpolator mAccelInterpolator = new AccelerateInterpolator(2f); 41 private final Interpolator mDecelInterpolator = new DecelerateInterpolator(1f); 42 43 private static final float ANIMATION_DURATION = 1200; 44 public static final float ALL_APPS_FINAL_ALPHA = .9f; 45 46 private static final float PARALLAX_COEFFICIENT = .125f; 47 48 private AllAppsContainerView mAppsView; 49 private Workspace mWorkspace; 50 private Hotseat mHotseat; 51 private float mHotseatBackgroundAlpha; 52 53 private float mStatusBarHeight; 54 55 private final Launcher mLauncher; 56 private final VerticalPullDetector mDetector; 57 58 // Animation in this class is controlled by a single variable {@link mShiftCurrent}. 59 // Visually, it represents top y coordinate of the all apps container. Using the 60 // {@link mShiftRange} as the denominator, this fraction value ranges in [0, 1]. 61 // 62 // When {@link mShiftCurrent} is 0, all apps container is pulled up. 63 // When {@link mShiftCurrent} is {@link mShirtRange}, all apps container is pulled down. 64 private float mShiftStart; // [0, mShiftRange] 65 private float mShiftCurrent; // [0, mShiftRange] 66 private float mShiftRange; // changes depending on the orientation 67 68 69 private static final float RECATCH_REJECTION_FRACTION = .0875f; 70 71 private int mBezelSwipeUpHeight; 72 private long mAnimationDuration; 73 74 75 private AnimatorSet mCurrentAnimation; 76 private boolean mNoIntercept; 77 78 private boolean mLightStatusBar; 79 80 public AllAppsTransitionController(Launcher launcher) { 81 mLauncher = launcher; 82 mDetector = new VerticalPullDetector(launcher); 83 mDetector.setListener(this); 84 mBezelSwipeUpHeight = launcher.getResources().getDimensionPixelSize( 85 R.dimen.all_apps_bezel_swipe_height); 86 } 87 88 @Override 89 public boolean onInterceptTouchEvent(MotionEvent ev) { 90 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 91 mNoIntercept = false; 92 if (mLauncher.getWorkspace().isInOverviewMode() || mLauncher.isWidgetsViewVisible()) { 93 mNoIntercept = true; 94 } else if (mLauncher.isAllAppsVisible() && 95 !mAppsView.shouldContainerScroll(ev.getX(), ev.getY())) { 96 mNoIntercept = true; 97 } else if (!mLauncher.isAllAppsVisible() && !shouldPossiblyIntercept(ev)) { 98 mNoIntercept = true; 99 } else { 100 // This controller is now going to handle all the touch events. 101 // First cancel any animation that is in progress. 102 cancelAnimation(); 103 // Now figure out which direction scroll events the controller will start 104 // calling the callbacks. 105 int conditionsToReportScroll = 0; 106 107 if (mDetector.isRestingState()) { 108 if (mLauncher.isAllAppsVisible()) { 109 conditionsToReportScroll |= VerticalPullDetector.THRESHOLD_DOWN; 110 } else { 111 conditionsToReportScroll |= VerticalPullDetector.THRESHOLD_UP; 112 } 113 } else { 114 if (isInDisallowRecatchBottomZone()) { 115 conditionsToReportScroll |= VerticalPullDetector.THRESHOLD_UP; 116 } else if (isInDisallowRecatchTopZone()) { 117 conditionsToReportScroll |= VerticalPullDetector.THRESHOLD_DOWN; 118 } else { 119 conditionsToReportScroll |= VerticalPullDetector.THRESHOLD_ONLY; 120 } 121 } 122 mDetector.setDetectableScrollConditions(conditionsToReportScroll); 123 } 124 } 125 if (mNoIntercept) { 126 return false; 127 } 128 mDetector.onTouchEvent(ev); 129 if (mDetector.isScrollingState() && (isInDisallowRecatchBottomZone() || isInDisallowRecatchTopZone())) { 130 return false; 131 } 132 return mDetector.shouldIntercept(); 133 } 134 135 private boolean shouldPossiblyIntercept(MotionEvent ev) { 136 DeviceProfile grid = mLauncher.getDeviceProfile(); 137 if (mDetector.isRestingState()) { 138 if (grid.isVerticalBarLayout()) { 139 if (ev.getY() > mLauncher.getDeviceProfile().heightPx - mBezelSwipeUpHeight) { 140 return true; 141 } 142 } else { 143 if (mLauncher.getDragLayer().isEventOverHotseat(ev) && !grid.isVerticalBarLayout()) { 144 return true; 145 } 146 } 147 return false; 148 } else { 149 return true; 150 } 151 } 152 153 @Override 154 public boolean onTouchEvent(MotionEvent ev) { 155 return mDetector.onTouchEvent(ev); 156 } 157 158 private boolean isInDisallowRecatchTopZone() { 159 return mShiftCurrent / mShiftRange < RECATCH_REJECTION_FRACTION; 160 } 161 162 private boolean isInDisallowRecatchBottomZone() { 163 return mShiftCurrent / mShiftRange > 1 - RECATCH_REJECTION_FRACTION; 164 } 165 166 @Override 167 public void onScrollStart(boolean start) { 168 mCurrentAnimation = LauncherAnimUtils.createAnimatorSet(); 169 mShiftStart = mAppsView.getTranslationY(); 170 preparePull(start); 171 } 172 173 @Override 174 public boolean onScroll(float displacement, float velocity) { 175 if (mAppsView == null) { 176 return false; // early termination. 177 } 178 if (0 <= mShiftStart + displacement && mShiftStart + displacement < mShiftRange) { 179 setProgress(mShiftStart + displacement); 180 } 181 return true; 182 } 183 184 @Override 185 public void onScrollEnd(float velocity, boolean fling) { 186 if (mAppsView == null) { 187 return; // early termination. 188 } 189 190 if (fling) { 191 if (velocity < 0) { 192 calculateDuration(velocity, mAppsView.getTranslationY()); 193 194 if (!mLauncher.isAllAppsVisible()) { 195 mLauncher.showAppsView(true, true, false, false); 196 } else { 197 animateToAllApps(mCurrentAnimation, mAnimationDuration, true); 198 } 199 } else { 200 calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY())); 201 if (mLauncher.isAllAppsVisible()) { 202 mLauncher.showWorkspace(true); 203 } else { 204 animateToWorkspace(mCurrentAnimation, mAnimationDuration, true); 205 } 206 } 207 // snap to top or bottom using the release velocity 208 } else { 209 if (mAppsView.getTranslationY() > mShiftRange / 2) { 210 calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY())); 211 if (mLauncher.isAllAppsVisible()) { 212 mLauncher.showWorkspace(true); 213 } else { 214 animateToWorkspace(mCurrentAnimation, mAnimationDuration, true); 215 } 216 } else { 217 calculateDuration(velocity, Math.abs(mAppsView.getTranslationY())); 218 if (!mLauncher.isAllAppsVisible()) { 219 mLauncher.showAppsView(true, true, false, false); 220 } else { 221 animateToAllApps(mCurrentAnimation, mAnimationDuration, true); 222 } 223 224 } 225 } 226 } 227 /** 228 * @param start {@code true} if start of new drag. 229 */ 230 public void preparePull(boolean start) { 231 if (start) { 232 // Initialize values that should not change until #onScrollEnd 233 mStatusBarHeight = mLauncher.getDragLayer().getInsets().top; 234 mHotseat.setVisibility(View.VISIBLE); 235 mHotseat.bringToFront(); 236 if (!mLauncher.getDeviceProfile().isVerticalBarLayout()) { 237 mShiftRange = mHotseat.getTop(); 238 } else { 239 mShiftRange = mHotseat.getBottom(); 240 } 241 if (!mLauncher.isAllAppsVisible()) { 242 mLauncher.tryAndUpdatePredictedApps(); 243 244 mHotseatBackgroundAlpha = mHotseat.getBackground().getAlpha() / 255f; 245 mHotseat.setBackgroundTransparent(true /* transparent */); 246 mAppsView.setVisibility(View.VISIBLE); 247 mAppsView.getContentView().setVisibility(View.VISIBLE); 248 mAppsView.getContentView().setBackground(null); 249 mAppsView.getRevealView().setVisibility(View.VISIBLE); 250 mAppsView.getRevealView().setAlpha(mHotseatBackgroundAlpha); 251 252 DeviceProfile grid= mLauncher.getDeviceProfile(); 253 if (!grid.isVerticalBarLayout()) { 254 mShiftRange = mHotseat.getTop(); 255 } else { 256 mShiftRange = mHotseat.getBottom(); 257 } 258 mAppsView.getRevealView().setAlpha(mHotseatBackgroundAlpha); 259 setProgress(mShiftRange); 260 } else { 261 // TODO: get rid of this workaround to override state change by workspace transition 262 mWorkspace.onLauncherTransitionPrepare(mLauncher, false, false); 263 View child = ((CellLayout) mWorkspace.getChildAt(mWorkspace.getNextPage())) 264 .getShortcutsAndWidgets(); 265 child.setVisibility(View.VISIBLE); 266 child.setAlpha(1f); 267 } 268 } else { 269 setProgress(mShiftCurrent); 270 } 271 } 272 273 private void updateLightStatusBar(float progress) { 274 boolean enable = (progress < mStatusBarHeight / 2); 275 // Already set correctly 276 if (mLightStatusBar == enable) { 277 return; 278 } 279 int systemUiFlags = mLauncher.getWindow().getDecorView().getSystemUiVisibility(); 280 if (enable) { 281 mLauncher.getWindow().getDecorView().setSystemUiVisibility(systemUiFlags 282 | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); 283 284 } else { 285 mLauncher.getWindow().getDecorView().setSystemUiVisibility(systemUiFlags 286 & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); 287 288 } 289 mLightStatusBar = enable; 290 } 291 292 /** 293 * @param progress y value of the border between hotseat and all apps 294 */ 295 public void setProgress(float progress) { 296 updateLightStatusBar(progress); 297 mShiftCurrent = progress; 298 float alpha = calcAlphaAllApps(progress); 299 float workspaceHotseatAlpha = 1 - alpha; 300 301 mAppsView.getRevealView().setAlpha(Math.min(ALL_APPS_FINAL_ALPHA, Math.max(mHotseatBackgroundAlpha, 302 mDecelInterpolator.getInterpolation(alpha)))); 303 mAppsView.getContentView().setAlpha(alpha); 304 mAppsView.setTranslationY(progress); 305 mWorkspace.setWorkspaceTranslation(Direction.Y, 306 PARALLAX_COEFFICIENT * (-mShiftRange + progress), 307 mAccelInterpolator.getInterpolation(workspaceHotseatAlpha)); 308 if (!mLauncher.getDeviceProfile().isVerticalBarLayout()) { 309 mWorkspace.setHotseatTranslation(Direction.Y, -mShiftRange + progress, 310 mAccelInterpolator.getInterpolation(workspaceHotseatAlpha)); 311 } else { 312 mWorkspace.setHotseatTranslation(Direction.Y, 313 PARALLAX_COEFFICIENT * (-mShiftRange + progress), 314 mAccelInterpolator.getInterpolation(workspaceHotseatAlpha)); 315 } 316 } 317 318 public float getProgress() { 319 return mShiftCurrent; 320 } 321 322 private float calcAlphaAllApps(float progress) { 323 return ((mShiftRange - progress)/ mShiftRange); 324 } 325 326 private void calculateDuration(float velocity, float disp) { 327 // TODO: make these values constants after tuning. 328 float velocityDivisor = Math.max(1.5f, Math.abs(0.5f * velocity)); 329 float travelDistance = Math.max(0.2f, disp / mShiftRange); 330 mAnimationDuration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance); 331 if (DBG) { 332 Log.d(TAG, String.format("calculateDuration=%d, v=%f, d=%f", mAnimationDuration, velocity, disp)); 333 } 334 } 335 336 public void animateToAllApps(AnimatorSet animationOut, long duration, boolean start) { 337 if (animationOut == null){ 338 return; 339 } 340 if (mDetector.isRestingState()) { 341 preparePull(true); 342 mAnimationDuration = duration; 343 mShiftStart = mAppsView.getTranslationY(); 344 } 345 final float fromAllAppsTop = mAppsView.getTranslationY(); 346 final float toAllAppsTop = 0; 347 348 ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress", 349 fromAllAppsTop, toAllAppsTop); 350 driftAndAlpha.setDuration(mAnimationDuration); 351 driftAndAlpha.setInterpolator(new PagedView.ScrollInterpolator()); 352 animationOut.play(driftAndAlpha); 353 354 animationOut.addListener(new AnimatorListenerAdapter() { 355 boolean canceled = false; 356 @Override 357 public void onAnimationCancel(Animator animation) { 358 canceled = true; 359 } 360 @Override 361 public void onAnimationEnd(Animator animation) { 362 if (canceled) { 363 return; 364 } else { 365 finishPullUp(); 366 cleanUpAnimation(); 367 mDetector.finishedScrolling(); 368 } 369 }}); 370 mCurrentAnimation = animationOut; 371 if (start) { 372 mCurrentAnimation.start(); 373 } 374 } 375 376 public void animateToWorkspace(AnimatorSet animationOut, long duration, boolean start) { 377 if (animationOut == null){ 378 return; 379 } 380 if(mDetector.isRestingState()) { 381 preparePull(true); 382 mAnimationDuration = duration; 383 mShiftStart = mAppsView.getTranslationY(); 384 } 385 final float fromAllAppsTop = mAppsView.getTranslationY(); 386 final float toAllAppsTop = mShiftRange; 387 388 ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress", 389 fromAllAppsTop, toAllAppsTop); 390 driftAndAlpha.setDuration(mAnimationDuration); 391 driftAndAlpha.setInterpolator(new PagedView.ScrollInterpolator()); 392 animationOut.play(driftAndAlpha); 393 394 animationOut.addListener(new AnimatorListenerAdapter() { 395 boolean canceled = false; 396 @Override 397 public void onAnimationCancel(Animator animation) { 398 canceled = true; 399 setProgress(mShiftCurrent); 400 } 401 402 @Override 403 public void onAnimationEnd(Animator animation) { 404 if (canceled) { 405 return; 406 } else { 407 finishPullDown(); 408 cleanUpAnimation(); 409 mDetector.finishedScrolling(); 410 } 411 }}); 412 mCurrentAnimation = animationOut; 413 if (start) { 414 mCurrentAnimation.start(); 415 } 416 } 417 418 private void finishPullUp() { 419 mHotseat.setVisibility(View.INVISIBLE); 420 setProgress(0f); 421 } 422 423 public void finishPullDown() { 424 if (mHotseat.getBackground() != null) { 425 return; 426 } 427 mAppsView.setVisibility(View.INVISIBLE); 428 mHotseat.setBackgroundTransparent(false /* transparent */); 429 mHotseat.setVisibility(View.VISIBLE); 430 setProgress(mShiftRange); 431 } 432 433 private void cancelAnimation() { 434 if (mCurrentAnimation != null) { 435 mCurrentAnimation.setDuration(0); 436 mCurrentAnimation.cancel(); 437 mCurrentAnimation = null; 438 } 439 } 440 441 private void cleanUpAnimation() { 442 mCurrentAnimation = null; 443 } 444 445 public void setupViews(AllAppsContainerView appsView, Hotseat hotseat, Workspace workspace) { 446 mAppsView = appsView; 447 mHotseat = hotseat; 448 mWorkspace = workspace; 449 } 450} 451