AllAppsTransitionController.java revision 83fb07bb6cb8817f5c35ff63206a9fe2a1d3ebce
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 // Now figure out which direction scroll events the controller will start 101 // calling the callbacks. 102 int conditionsToReportScroll = 0; 103 104 if (mDetector.isRestingState()) { 105 if (mLauncher.isAllAppsVisible()) { 106 conditionsToReportScroll |= VerticalPullDetector.THRESHOLD_DOWN; 107 } else { 108 conditionsToReportScroll |= VerticalPullDetector.THRESHOLD_UP; 109 } 110 } else { 111 if (isInDisallowRecatchBottomZone()) { 112 conditionsToReportScroll |= VerticalPullDetector.THRESHOLD_UP; 113 } else if (isInDisallowRecatchTopZone()) { 114 conditionsToReportScroll |= VerticalPullDetector.THRESHOLD_DOWN; 115 } else { 116 conditionsToReportScroll |= VerticalPullDetector.THRESHOLD_ONLY; 117 } 118 } 119 mDetector.setDetectableScrollConditions(conditionsToReportScroll); 120 } 121 } 122 if (mNoIntercept) { 123 return false; 124 } 125 mDetector.onTouchEvent(ev); 126 if (mDetector.isScrollingState() && (isInDisallowRecatchBottomZone() || isInDisallowRecatchTopZone())) { 127 return false; 128 } 129 return mDetector.shouldIntercept(); 130 } 131 132 private boolean shouldPossiblyIntercept(MotionEvent ev) { 133 DeviceProfile grid = mLauncher.getDeviceProfile(); 134 if (mDetector.isRestingState()) { 135 if (grid.isVerticalBarLayout()) { 136 if (ev.getY() > mLauncher.getDeviceProfile().heightPx - mBezelSwipeUpHeight) { 137 return true; 138 } 139 } else { 140 if ((mLauncher.getDragLayer().isEventOverHotseat(ev) 141 || mLauncher.getDragLayer().isEventOverPageIndicator(ev)) 142 && !grid.isVerticalBarLayout()) { 143 return true; 144 } 145 } 146 return false; 147 } else { 148 return true; 149 } 150 } 151 152 @Override 153 public boolean onTouchEvent(MotionEvent ev) { 154 return mDetector.onTouchEvent(ev); 155 } 156 157 private boolean isInDisallowRecatchTopZone() { 158 return mShiftCurrent / mShiftRange < RECATCH_REJECTION_FRACTION; 159 } 160 161 private boolean isInDisallowRecatchBottomZone() { 162 return mShiftCurrent / mShiftRange > 1 - RECATCH_REJECTION_FRACTION; 163 } 164 165 @Override 166 public void onScrollStart(boolean start) { 167 cancelAnimation(); 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 float progress = Math.min(Math.max(0, mShiftStart + displacement), mShiftRange); 179 setProgress(progress); 180 return true; 181 } 182 183 @Override 184 public void onScrollEnd(float velocity, boolean fling) { 185 if (mAppsView == null) { 186 return; // early termination. 187 } 188 189 if (fling) { 190 if (velocity < 0) { 191 calculateDuration(velocity, mAppsView.getTranslationY()); 192 193 if (!mLauncher.isAllAppsVisible()) { 194 mLauncher.showAppsView(true, true, false, false); 195 } else { 196 animateToAllApps(mCurrentAnimation, mAnimationDuration, true); 197 } 198 } else { 199 calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY())); 200 if (mLauncher.isAllAppsVisible()) { 201 mLauncher.showWorkspace(true); 202 } else { 203 animateToWorkspace(mCurrentAnimation, mAnimationDuration, true); 204 } 205 } 206 // snap to top or bottom using the release velocity 207 } else { 208 if (mAppsView.getTranslationY() > mShiftRange / 2) { 209 calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY())); 210 if (mLauncher.isAllAppsVisible()) { 211 mLauncher.showWorkspace(true); 212 } else { 213 animateToWorkspace(mCurrentAnimation, mAnimationDuration, true); 214 } 215 } else { 216 calculateDuration(velocity, Math.abs(mAppsView.getTranslationY())); 217 if (!mLauncher.isAllAppsVisible()) { 218 mLauncher.showAppsView(true, true, false, false); 219 } else { 220 animateToAllApps(mCurrentAnimation, mAnimationDuration, true); 221 } 222 223 } 224 } 225 } 226 /** 227 * @param start {@code true} if start of new drag. 228 */ 229 public void preparePull(boolean start) { 230 if (start) { 231 // Initialize values that should not change until #onScrollEnd 232 mStatusBarHeight = mLauncher.getDragLayer().getInsets().top; 233 mHotseat.setVisibility(View.VISIBLE); 234 mHotseat.bringToFront(); 235 236 if (!mLauncher.isAllAppsVisible()) { 237 mLauncher.tryAndUpdatePredictedApps(); 238 239 mHotseatBackgroundAlpha = mHotseat.getBackgroundDrawableAlpha() / 255f; 240 mHotseat.setBackgroundTransparent(true /* transparent */); 241 mAppsView.setVisibility(View.VISIBLE); 242 mAppsView.getContentView().setVisibility(View.VISIBLE); 243 mAppsView.getContentView().setBackground(null); 244 mAppsView.getRevealView().setVisibility(View.VISIBLE); 245 mAppsView.getRevealView().setAlpha(mHotseatBackgroundAlpha); 246 } 247 } else { 248 setProgress(mShiftCurrent); 249 } 250 } 251 252 private void updateLightStatusBar(float progress) { 253 boolean enable = (progress < mStatusBarHeight / 2); 254 // Do not modify status bar on landscape as all apps is not full bleed. 255 if (mLauncher.getDeviceProfile().isVerticalBarLayout()) { 256 return; 257 } 258 // Already set correctly 259 if (mLightStatusBar == enable) { 260 return; 261 } 262 int systemUiFlags = mLauncher.getWindow().getDecorView().getSystemUiVisibility(); 263 // SYSTEM_UI_FLAG_LIGHT_NAV_BAR == SYSTEM_UI_FLAG_LIGHT_STATUS_BAR << 1 264 // Use proper constant once API is submitted. 265 if (enable) { 266 mLauncher.getWindow().getDecorView().setSystemUiVisibility(systemUiFlags 267 | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 268 | (View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR << 1)); 269 270 } else { 271 mLauncher.getWindow().getDecorView().setSystemUiVisibility(systemUiFlags 272 & ~(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 273 |(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR << 1))); 274 275 } 276 mLightStatusBar = enable; 277 } 278 279 /** 280 * @param progress y value of the border between hotseat and all apps 281 */ 282 public void setProgress(float progress) { 283 updateLightStatusBar(progress); 284 mShiftCurrent = progress; 285 float alpha = calcAlphaAllApps(progress); 286 float workspaceHotseatAlpha = 1 - alpha; 287 288 mAppsView.getRevealView().setAlpha(Math.min(ALL_APPS_FINAL_ALPHA, Math.max(mHotseatBackgroundAlpha, 289 mDecelInterpolator.getInterpolation(alpha)))); 290 mAppsView.getContentView().setAlpha(alpha); 291 mAppsView.setTranslationY(progress); 292 mWorkspace.setWorkspaceTranslation(Direction.Y, 293 PARALLAX_COEFFICIENT * (-mShiftRange + progress), 294 mAccelInterpolator.getInterpolation(workspaceHotseatAlpha)); 295 if (!mLauncher.getDeviceProfile().isVerticalBarLayout()) { 296 mWorkspace.setHotseatTranslation(Direction.Y, -mShiftRange + progress, 297 mAccelInterpolator.getInterpolation(workspaceHotseatAlpha)); 298 } else { 299 mWorkspace.setHotseatTranslation(Direction.Y, 300 PARALLAX_COEFFICIENT * (-mShiftRange + progress), 301 mAccelInterpolator.getInterpolation(workspaceHotseatAlpha)); 302 } 303 } 304 305 public float getProgress() { 306 return mShiftCurrent; 307 } 308 309 private float calcAlphaAllApps(float progress) { 310 return ((mShiftRange - progress)/ mShiftRange); 311 } 312 313 private void calculateDuration(float velocity, float disp) { 314 // TODO: make these values constants after tuning. 315 float velocityDivisor = Math.max(1.5f, Math.abs(0.5f * velocity)); 316 float travelDistance = Math.max(0.2f, disp / mShiftRange); 317 mAnimationDuration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance); 318 if (DBG) { 319 Log.d(TAG, String.format("calculateDuration=%d, v=%f, d=%f", mAnimationDuration, velocity, disp)); 320 } 321 } 322 323 public void animateToAllApps(AnimatorSet animationOut, long duration, boolean start) { 324 if (animationOut == null){ 325 return; 326 } 327 if (mDetector.isRestingState()) { 328 preparePull(true); 329 mAnimationDuration = duration; 330 mShiftStart = mAppsView.getTranslationY(); 331 } 332 final float fromAllAppsTop = mAppsView.getTranslationY(); 333 final float toAllAppsTop = 0; 334 335 ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress", 336 fromAllAppsTop, toAllAppsTop); 337 driftAndAlpha.setDuration(mAnimationDuration); 338 driftAndAlpha.setInterpolator(new PagedView.ScrollInterpolator()); 339 animationOut.play(driftAndAlpha); 340 341 animationOut.addListener(new AnimatorListenerAdapter() { 342 boolean canceled = false; 343 @Override 344 public void onAnimationCancel(Animator animation) { 345 canceled = true; 346 } 347 @Override 348 public void onAnimationEnd(Animator animation) { 349 if (canceled) { 350 return; 351 } else { 352 finishPullUp(); 353 cleanUpAnimation(); 354 mDetector.finishedScrolling(); 355 } 356 }}); 357 mCurrentAnimation = animationOut; 358 if (start) { 359 mCurrentAnimation.start(); 360 } 361 } 362 363 public void animateToWorkspace(AnimatorSet animationOut, long duration, boolean start) { 364 if (animationOut == null){ 365 return; 366 } 367 if(mDetector.isRestingState()) { 368 preparePull(true); 369 mAnimationDuration = duration; 370 mShiftStart = mAppsView.getTranslationY(); 371 } 372 final float fromAllAppsTop = mAppsView.getTranslationY(); 373 final float toAllAppsTop = mShiftRange; 374 375 ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress", 376 fromAllAppsTop, toAllAppsTop); 377 driftAndAlpha.setDuration(mAnimationDuration); 378 driftAndAlpha.setInterpolator(new PagedView.ScrollInterpolator()); 379 animationOut.play(driftAndAlpha); 380 381 animationOut.addListener(new AnimatorListenerAdapter() { 382 boolean canceled = false; 383 @Override 384 public void onAnimationCancel(Animator animation) { 385 canceled = true; 386 setProgress(mShiftCurrent); 387 } 388 389 @Override 390 public void onAnimationEnd(Animator animation) { 391 if (canceled) { 392 return; 393 } else { 394 finishPullDown(); 395 cleanUpAnimation(); 396 mDetector.finishedScrolling(); 397 } 398 }}); 399 mCurrentAnimation = animationOut; 400 if (start) { 401 mCurrentAnimation.start(); 402 } 403 } 404 405 private void finishPullUp() { 406 mHotseat.setVisibility(View.INVISIBLE); 407 setProgress(0f); 408 } 409 410 public void finishPullDown() { 411 mAppsView.setVisibility(View.INVISIBLE); 412 mHotseat.setBackgroundTransparent(false /* transparent */); 413 mHotseat.setVisibility(View.VISIBLE); 414 setProgress(mShiftRange); 415 } 416 417 private void cancelAnimation() { 418 if (mCurrentAnimation != null) { 419 mCurrentAnimation.setDuration(0); 420 mCurrentAnimation.cancel(); 421 mCurrentAnimation = null; 422 } 423 } 424 425 private void cleanUpAnimation() { 426 mCurrentAnimation = null; 427 } 428 429 public void setupViews(AllAppsContainerView appsView, Hotseat hotseat, Workspace workspace) { 430 mAppsView = appsView; 431 mHotseat = hotseat; 432 mWorkspace = workspace; 433 mHotseat.addOnLayoutChangeListener(new View.OnLayoutChangeListener(){ 434 public void onLayoutChange(View v, int left, int top, int right, int bottom, 435 int oldLeft, int oldTop, int oldRight, int oldBottom) { 436 if (!mLauncher.getDeviceProfile().isVerticalBarLayout()) { 437 mShiftRange = top; 438 } else { 439 mShiftRange = bottom; 440 } 441 if (!mLauncher.isAllAppsVisible()) { 442 setProgress(mShiftRange); 443 } 444 } 445 }); 446 } 447} 448