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