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