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