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