1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.systemui.statusbar.notification.stack; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ValueAnimator; 22 import android.util.Property; 23 import android.view.View; 24 25 import com.android.keyguard.KeyguardSliceView; 26 import com.android.systemui.R; 27 import com.android.systemui.animation.Interpolators; 28 import com.android.systemui.shared.clocks.AnimatableClockView; 29 import com.android.systemui.statusbar.NotificationShelf; 30 import com.android.systemui.statusbar.StatusBarIconView; 31 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 32 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 33 import com.android.systemui.statusbar.notification.row.ExpandableView; 34 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView; 35 36 import java.util.ArrayList; 37 import java.util.HashSet; 38 import java.util.Stack; 39 40 /** 41 * An stack state animator which handles animations to new StackScrollStates 42 */ 43 public class StackStateAnimator { 44 45 public static final int ANIMATION_DURATION_STANDARD = 360; 46 public static final int ANIMATION_DURATION_CORNER_RADIUS = 200; 47 public static final int ANIMATION_DURATION_WAKEUP = 500; 48 public static final int ANIMATION_DURATION_WAKEUP_SCRIM = 667; 49 public static final int ANIMATION_DURATION_GO_TO_FULL_SHADE = 448; 50 public static final int ANIMATION_DURATION_APPEAR_DISAPPEAR = 464; 51 public static final int ANIMATION_DURATION_SWIPE = 200; 52 public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220; 53 public static final int ANIMATION_DURATION_CLOSE_REMOTE_INPUT = 150; 54 public static final int ANIMATION_DURATION_HEADS_UP_APPEAR = 400; 55 public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 400; 56 public static final int ANIMATION_DURATION_FOLD_TO_AOD = 57 AnimatableClockView.ANIMATION_DURATION_FOLD_TO_AOD; 58 public static final int ANIMATION_DURATION_PULSE_APPEAR = 59 KeyguardSliceView.DEFAULT_ANIM_DURATION; 60 public static final int ANIMATION_DURATION_BLOCKING_HELPER_FADE = 240; 61 public static final int ANIMATION_DURATION_PRIORITY_CHANGE = 500; 62 public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80; 63 public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32; 64 public static final int ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE = 48; 65 public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2; 66 private static final int MAX_STAGGER_COUNT = 5; 67 68 private final int mGoToFullShadeAppearingTranslation; 69 private final int mPulsingAppearingTranslation; 70 private final ExpandableViewState mTmpState = new ExpandableViewState(); 71 private final AnimationProperties mAnimationProperties; 72 public NotificationStackScrollLayout mHostLayout; 73 private ArrayList<NotificationStackScrollLayout.AnimationEvent> mNewEvents = 74 new ArrayList<>(); 75 private ArrayList<View> mNewAddChildren = new ArrayList<>(); 76 private HashSet<View> mHeadsUpAppearChildren = new HashSet<>(); 77 private HashSet<View> mHeadsUpDisappearChildren = new HashSet<>(); 78 private HashSet<Animator> mAnimatorSet = new HashSet<>(); 79 private Stack<AnimatorListenerAdapter> mAnimationListenerPool = new Stack<>(); 80 private AnimationFilter mAnimationFilter = new AnimationFilter(); 81 private long mCurrentLength; 82 private long mCurrentAdditionalDelay; 83 84 private ValueAnimator mTopOverScrollAnimator; 85 private ValueAnimator mBottomOverScrollAnimator; 86 private int mHeadsUpAppearHeightBottom; 87 private boolean mShadeExpanded; 88 private ArrayList<ExpandableView> mTransientViewsToRemove = new ArrayList<>(); 89 private NotificationShelf mShelf; 90 private float mStatusBarIconLocation; 91 private int[] mTmpLocation = new int[2]; 92 private StackStateLogger mLogger; 93 StackStateAnimator(NotificationStackScrollLayout hostLayout)94 public StackStateAnimator(NotificationStackScrollLayout hostLayout) { 95 mHostLayout = hostLayout; 96 mGoToFullShadeAppearingTranslation = 97 hostLayout.getContext().getResources().getDimensionPixelSize( 98 R.dimen.go_to_full_shade_appearing_translation); 99 mPulsingAppearingTranslation = 100 hostLayout.getContext().getResources().getDimensionPixelSize( 101 R.dimen.pulsing_notification_appear_translation); 102 mAnimationProperties = new AnimationProperties() { 103 @Override 104 public AnimationFilter getAnimationFilter() { 105 return mAnimationFilter; 106 } 107 108 @Override 109 public AnimatorListenerAdapter getAnimationFinishListener(Property property) { 110 return getGlobalAnimationFinishedListener(); 111 } 112 113 @Override 114 public boolean wasAdded(View view) { 115 return mNewAddChildren.contains(view); 116 } 117 }; 118 } 119 setLogger(StackStateLogger logger)120 protected void setLogger(StackStateLogger logger) { 121 mLogger = logger; 122 } 123 isRunning()124 public boolean isRunning() { 125 return !mAnimatorSet.isEmpty(); 126 } 127 startAnimationForEvents( ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents, long additionalDelay)128 public void startAnimationForEvents( 129 ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents, 130 long additionalDelay) { 131 132 processAnimationEvents(mAnimationEvents); 133 134 int childCount = mHostLayout.getChildCount(); 135 mAnimationFilter.applyCombination(mNewEvents); 136 mCurrentAdditionalDelay = additionalDelay; 137 mCurrentLength = NotificationStackScrollLayout.AnimationEvent.combineLength(mNewEvents); 138 // Used to stagger concurrent animations' delays and durations for visual effect 139 int animationStaggerCount = 0; 140 for (int i = 0; i < childCount; i++) { 141 final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i); 142 143 ExpandableViewState viewState = child.getViewState(); 144 if (viewState == null || child.getVisibility() == View.GONE 145 || applyWithoutAnimation(child, viewState)) { 146 continue; 147 } 148 149 if (mAnimationProperties.wasAdded(child) && animationStaggerCount < MAX_STAGGER_COUNT) { 150 animationStaggerCount++; 151 } 152 initAnimationProperties(child, viewState, animationStaggerCount); 153 viewState.animateTo(child, mAnimationProperties); 154 } 155 if (!isRunning()) { 156 // no child has preformed any animation, lets finish 157 onAnimationFinished(); 158 } 159 mHeadsUpAppearChildren.clear(); 160 mHeadsUpDisappearChildren.clear(); 161 mNewEvents.clear(); 162 mNewAddChildren.clear(); 163 } 164 initAnimationProperties(ExpandableView child, ExpandableViewState viewState, int animationStaggerCount)165 private void initAnimationProperties(ExpandableView child, 166 ExpandableViewState viewState, int animationStaggerCount) { 167 boolean wasAdded = mAnimationProperties.wasAdded(child); 168 mAnimationProperties.duration = mCurrentLength; 169 adaptDurationWhenGoingToFullShade(child, viewState, wasAdded, animationStaggerCount); 170 mAnimationProperties.delay = 0; 171 if (wasAdded || mAnimationFilter.hasDelays 172 && (viewState.getYTranslation() != child.getTranslationY() 173 || viewState.getZTranslation() != child.getTranslationZ() 174 || viewState.getAlpha() != child.getAlpha() 175 || viewState.height != child.getActualHeight() 176 || viewState.clipTopAmount != child.getClipTopAmount())) { 177 mAnimationProperties.delay = mCurrentAdditionalDelay 178 + calculateChildAnimationDelay(viewState, animationStaggerCount); 179 } 180 } 181 adaptDurationWhenGoingToFullShade(ExpandableView child, ExpandableViewState viewState, boolean wasAdded, int animationStaggerCount)182 private void adaptDurationWhenGoingToFullShade(ExpandableView child, 183 ExpandableViewState viewState, boolean wasAdded, int animationStaggerCount) { 184 boolean isDecorView = child instanceof StackScrollerDecorView; 185 boolean needsAdjustment = wasAdded || isDecorView; 186 if (needsAdjustment && mAnimationFilter.hasGoToFullShadeEvent) { 187 int startOffset = 0; 188 if (!isDecorView) { 189 startOffset = mGoToFullShadeAppearingTranslation; 190 float longerDurationFactor = (float) Math.pow(animationStaggerCount, 0.7f); 191 mAnimationProperties.duration = ANIMATION_DURATION_APPEAR_DISAPPEAR + 50 192 + (long) (100 * longerDurationFactor); 193 } 194 child.setTranslationY(viewState.getYTranslation() + startOffset); 195 } 196 } 197 198 /** 199 * Determines if a view should not perform an animation and applies it directly. 200 * 201 * @return true if no animation should be performed 202 */ applyWithoutAnimation(ExpandableView child, ExpandableViewState viewState)203 private boolean applyWithoutAnimation(ExpandableView child, ExpandableViewState viewState) { 204 if (mShadeExpanded) { 205 return false; 206 } 207 if (ViewState.isAnimatingY(child)) { 208 // A Y translation animation is running 209 return false; 210 } 211 if (mHeadsUpDisappearChildren.contains(child) || mHeadsUpAppearChildren.contains(child)) { 212 // This is a heads up animation 213 return false; 214 } 215 if (NotificationStackScrollLayout.isPinnedHeadsUp(child)) { 216 // This is another headsUp which might move. Let's animate! 217 return false; 218 } 219 viewState.applyToView(child); 220 return true; 221 } 222 calculateChildAnimationDelay(ExpandableViewState viewState, int animationStaggerCount)223 private long calculateChildAnimationDelay(ExpandableViewState viewState, 224 int animationStaggerCount) { 225 if (mAnimationFilter.hasGoToFullShadeEvent) { 226 return calculateDelayGoToFullShade(viewState, animationStaggerCount); 227 } 228 if (mAnimationFilter.customDelay != AnimationFilter.NO_DELAY) { 229 return mAnimationFilter.customDelay; 230 } 231 long minDelay = 0; 232 for (NotificationStackScrollLayout.AnimationEvent event : mNewEvents) { 233 long delayPerElement = ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING; 234 switch (event.animationType) { 235 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD: { 236 int ownIndex = viewState.notGoneIndex; 237 int changingIndex = 238 ((ExpandableView) (event.mChangingView)).getViewState().notGoneIndex; 239 int difference = Math.abs(ownIndex - changingIndex); 240 difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE, 241 difference - 1)); 242 long delay = (DELAY_EFFECT_MAX_INDEX_DIFFERENCE - difference) * delayPerElement; 243 minDelay = Math.max(delay, minDelay); 244 break; 245 } 246 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT: 247 delayPerElement = ANIMATION_DELAY_PER_ELEMENT_MANUAL; 248 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE: { 249 int ownIndex = viewState.notGoneIndex; 250 boolean noNextView = event.viewAfterChangingView == null; 251 ExpandableView viewAfterChangingView = noNextView 252 ? mHostLayout.getLastChildNotGone() 253 : (ExpandableView) event.viewAfterChangingView; 254 if (viewAfterChangingView == null) { 255 // This can happen when the last view in the list is removed. 256 // Since the shelf is still around and the only view, the code still goes 257 // in here and tries to calculate the delay for it when case its properties 258 // have changed. 259 continue; 260 } 261 int nextIndex = viewAfterChangingView.getViewState().notGoneIndex; 262 if (ownIndex >= nextIndex) { 263 // we only have the view afterwards 264 ownIndex++; 265 } 266 int difference = Math.abs(ownIndex - nextIndex); 267 difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE, 268 difference - 1)); 269 long delay = difference * delayPerElement; 270 minDelay = Math.max(delay, minDelay); 271 break; 272 } 273 default: 274 break; 275 } 276 } 277 return minDelay; 278 } 279 calculateDelayGoToFullShade(ExpandableViewState viewState, int animationStaggerCount)280 private long calculateDelayGoToFullShade(ExpandableViewState viewState, 281 int animationStaggerCount) { 282 int shelfIndex = mShelf.getNotGoneIndex(); 283 float index = viewState.notGoneIndex; 284 long result = 0; 285 if (index > shelfIndex) { 286 float diff = (float) Math.pow(animationStaggerCount, 0.7f); 287 result += (long) (diff * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE * 0.25); 288 index = shelfIndex; 289 } 290 index = (float) Math.pow(index, 0.7f); 291 result += (long) (index * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE); 292 return result; 293 } 294 295 /** 296 * @return an adapter which ensures that onAnimationFinished is called once no animation is 297 * running anymore 298 */ getGlobalAnimationFinishedListener()299 private AnimatorListenerAdapter getGlobalAnimationFinishedListener() { 300 if (!mAnimationListenerPool.empty()) { 301 return mAnimationListenerPool.pop(); 302 } 303 304 // We need to create a new one, no reusable ones found 305 return new AnimatorListenerAdapter() { 306 private boolean mWasCancelled; 307 308 @Override 309 public void onAnimationEnd(Animator animation) { 310 mAnimatorSet.remove(animation); 311 if (mAnimatorSet.isEmpty() && !mWasCancelled) { 312 onAnimationFinished(); 313 } 314 mAnimationListenerPool.push(this); 315 } 316 317 @Override 318 public void onAnimationCancel(Animator animation) { 319 mWasCancelled = true; 320 } 321 322 @Override 323 public void onAnimationStart(Animator animation) { 324 mWasCancelled = false; 325 mAnimatorSet.add(animation); 326 } 327 }; 328 } 329 onAnimationFinished()330 private void onAnimationFinished() { 331 mHostLayout.onChildAnimationFinished(); 332 333 for (ExpandableView transientViewToRemove : mTransientViewsToRemove) { 334 transientViewToRemove.removeFromTransientContainer(); 335 } 336 mTransientViewsToRemove.clear(); 337 } 338 339 /** 340 * Process the animationEvents for a new animation 341 * 342 * @param animationEvents the animation events for the animation to perform 343 */ processAnimationEvents( ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents)344 private void processAnimationEvents( 345 ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents) { 346 for (NotificationStackScrollLayout.AnimationEvent event : animationEvents) { 347 final ExpandableView changingView = (ExpandableView) event.mChangingView; 348 boolean loggable = false; 349 boolean isHeadsUp = false; 350 String key = null; 351 if (changingView instanceof ExpandableNotificationRow && mLogger != null) { 352 loggable = true; 353 isHeadsUp = ((ExpandableNotificationRow) changingView).isHeadsUp(); 354 key = ((ExpandableNotificationRow) changingView).getEntry().getKey(); 355 } 356 if (event.animationType == 357 NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD) { 358 359 // This item is added, initialize it's properties. 360 ExpandableViewState viewState = changingView.getViewState(); 361 if (viewState == null || viewState.gone) { 362 // The position for this child was never generated, let's continue. 363 continue; 364 } 365 if (loggable && isHeadsUp) { 366 mLogger.logHUNViewAppearingWithAddEvent(key); 367 } 368 viewState.applyToView(changingView); 369 mNewAddChildren.add(changingView); 370 371 } else if (event.animationType == 372 NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE) { 373 if (changingView.getVisibility() != View.VISIBLE) { 374 changingView.removeFromTransientContainer(); 375 continue; 376 } 377 378 // Find the amount to translate up. This is needed in order to understand the 379 // direction of the remove animation (either downwards or upwards) 380 // upwards by default 381 float translationDirection = -1.0f; 382 if (event.viewAfterChangingView != null) { 383 float ownPosition = changingView.getTranslationY(); 384 if (changingView instanceof ExpandableNotificationRow 385 && event.viewAfterChangingView instanceof ExpandableNotificationRow) { 386 ExpandableNotificationRow changingRow = 387 (ExpandableNotificationRow) changingView; 388 ExpandableNotificationRow nextRow = 389 (ExpandableNotificationRow) event.viewAfterChangingView; 390 if (changingRow.isRemoved() 391 && changingRow.wasChildInGroupWhenRemoved() 392 && !nextRow.isChildInGroup()) { 393 // the next row isn't actually a child from a group! Let's 394 // compare absolute positions! 395 ownPosition = changingRow.getTranslationWhenRemoved(); 396 } 397 } 398 int actualHeight = changingView.getActualHeight(); 399 // there was a view after this one, Approximate the distance the next child 400 // travelled 401 ExpandableViewState viewState = 402 ((ExpandableView) event.viewAfterChangingView).getViewState(); 403 translationDirection = ((viewState.getYTranslation() 404 - (ownPosition + actualHeight / 2.0f)) * 2 / 405 actualHeight); 406 translationDirection = Math.max(Math.min(translationDirection, 1.0f),-1.0f); 407 408 } 409 Runnable postAnimation = changingView::removeFromTransientContainer; 410 if (loggable && isHeadsUp) { 411 mLogger.logHUNViewDisappearingWithRemoveEvent(key); 412 String finalKey = key; 413 postAnimation = () -> { 414 mLogger.disappearAnimationEnded(finalKey); 415 changingView.removeFromTransientContainer(); 416 }; 417 } 418 changingView.performRemoveAnimation(ANIMATION_DURATION_APPEAR_DISAPPEAR, 419 0 /* delay */, translationDirection, false /* isHeadsUpAppear */, 420 0, postAnimation, null); 421 } else if (event.animationType == 422 NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT) { 423 if (mHostLayout.isFullySwipedOut(changingView)) { 424 changingView.removeFromTransientContainer(); 425 } 426 } else if (event.animationType == NotificationStackScrollLayout 427 .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) { 428 ExpandableNotificationRow row = (ExpandableNotificationRow) event.mChangingView; 429 row.prepareExpansionChanged(); 430 } else if (event.animationType == NotificationStackScrollLayout 431 .AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) { 432 // This item is added, initialize it's properties. 433 ExpandableViewState viewState = changingView.getViewState(); 434 mTmpState.copyFrom(viewState); 435 if (event.headsUpFromBottom) { 436 mTmpState.setYTranslation(mHeadsUpAppearHeightBottom); 437 } else { 438 Runnable onAnimationEnd = null; 439 if (loggable) { 440 String finalKey = key; 441 onAnimationEnd = () -> mLogger.appearAnimationEnded(finalKey); 442 } 443 changingView.performAddAnimation(0, ANIMATION_DURATION_HEADS_UP_APPEAR, 444 true /* isHeadsUpAppear */, onAnimationEnd); 445 } 446 mHeadsUpAppearChildren.add(changingView); 447 // this only captures HEADS_UP_APPEAR animations, but HUNs can appear with normal 448 // ADD animations, which would not be logged here. 449 if (loggable) { 450 mLogger.logHUNViewAppearing(key); 451 } 452 453 mTmpState.applyToView(changingView); 454 } else if (event.animationType == NotificationStackScrollLayout 455 .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR || 456 event.animationType == NotificationStackScrollLayout 457 .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK) { 458 mHeadsUpDisappearChildren.add(changingView); 459 Runnable endRunnable = null; 460 if (changingView.getParent() == null) { 461 // This notification was actually removed, so we need to add it transiently 462 mHostLayout.addTransientView(changingView, 0); 463 changingView.setTransientContainer(mHostLayout); 464 mTmpState.initFrom(changingView); 465 endRunnable = changingView::removeFromTransientContainer; 466 } 467 float targetLocation = 0; 468 boolean needsAnimation = true; 469 if (changingView instanceof ExpandableNotificationRow) { 470 ExpandableNotificationRow row = (ExpandableNotificationRow) changingView; 471 if (row.isDismissed()) { 472 needsAnimation = false; 473 } 474 475 NotificationEntry entry = row.getEntry(); 476 StatusBarIconView icon = entry.getIcons().getStatusBarIcon(); 477 final StatusBarIconView centeredIcon = entry.getIcons().getCenteredIcon(); 478 if (centeredIcon != null && centeredIcon.getParent() != null) { 479 icon = centeredIcon; 480 } 481 if (icon.getParent() != null) { 482 icon.getLocationOnScreen(mTmpLocation); 483 float iconPosition = mTmpLocation[0] - icon.getTranslationX() 484 + ViewState.getFinalTranslationX(icon) 485 + icon.getWidth() * 0.25f; 486 mHostLayout.getLocationOnScreen(mTmpLocation); 487 targetLocation = iconPosition - mTmpLocation[0]; 488 } 489 } 490 491 if (needsAnimation) { 492 // We need to add the global animation listener, since once no animations are 493 // running anymore, the panel will instantly hide itself. We need to wait until 494 // the animation is fully finished for this though. 495 Runnable postAnimation = endRunnable; 496 if (loggable) { 497 mLogger.logHUNViewDisappearing(key); 498 499 Runnable finalEndRunnable = endRunnable; 500 String finalKey1 = key; 501 postAnimation = () -> { 502 mLogger.disappearAnimationEnded(finalKey1); 503 if (finalEndRunnable != null) finalEndRunnable.run(); 504 }; 505 } 506 long removeAnimationDelay = changingView.performRemoveAnimation( 507 ANIMATION_DURATION_HEADS_UP_DISAPPEAR, 508 0, 0.0f, true /* isHeadsUpAppear */, targetLocation, 509 postAnimation, getGlobalAnimationFinishedListener()); 510 mAnimationProperties.delay += removeAnimationDelay; 511 } else if (endRunnable != null) { 512 endRunnable.run(); 513 } 514 } 515 mNewEvents.add(event); 516 } 517 } 518 animateOverScrollToAmount(float targetAmount, final boolean onTop, final boolean isRubberbanded)519 public void animateOverScrollToAmount(float targetAmount, final boolean onTop, 520 final boolean isRubberbanded) { 521 final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop); 522 if (targetAmount == startOverScrollAmount) { 523 return; 524 } 525 cancelOverScrollAnimators(onTop); 526 ValueAnimator overScrollAnimator = ValueAnimator.ofFloat(startOverScrollAmount, 527 targetAmount); 528 overScrollAnimator.setDuration(ANIMATION_DURATION_STANDARD); 529 overScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 530 @Override 531 public void onAnimationUpdate(ValueAnimator animation) { 532 float currentOverScroll = (float) animation.getAnimatedValue(); 533 mHostLayout.setOverScrollAmount( 534 currentOverScroll, onTop, false /* animate */, false /* cancelAnimators */, 535 isRubberbanded); 536 } 537 }); 538 overScrollAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 539 overScrollAnimator.addListener(new AnimatorListenerAdapter() { 540 @Override 541 public void onAnimationEnd(Animator animation) { 542 if (onTop) { 543 mTopOverScrollAnimator = null; 544 } else { 545 mBottomOverScrollAnimator = null; 546 } 547 } 548 }); 549 overScrollAnimator.start(); 550 if (onTop) { 551 mTopOverScrollAnimator = overScrollAnimator; 552 } else { 553 mBottomOverScrollAnimator = overScrollAnimator; 554 } 555 } 556 cancelOverScrollAnimators(boolean onTop)557 public void cancelOverScrollAnimators(boolean onTop) { 558 ValueAnimator currentAnimator = onTop ? mTopOverScrollAnimator : mBottomOverScrollAnimator; 559 if (currentAnimator != null) { 560 currentAnimator.cancel(); 561 } 562 } 563 setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom)564 public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) { 565 mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom; 566 } 567 setShadeExpanded(boolean shadeExpanded)568 public void setShadeExpanded(boolean shadeExpanded) { 569 mShadeExpanded = shadeExpanded; 570 } 571 setShelf(NotificationShelf shelf)572 public void setShelf(NotificationShelf shelf) { 573 mShelf = shelf; 574 } 575 } 576