• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.row.ExpandableNotificationRow;
32 import com.android.systemui.statusbar.notification.row.ExpandableView;
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_SWIPE = 260;
48     public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220;
49     public static final int ANIMATION_DURATION_CLOSE_REMOTE_INPUT = 150;
50     public static final int ANIMATION_DURATION_HEADS_UP_APPEAR = 550;
51     public static final int ANIMATION_DURATION_HEADS_UP_APPEAR_CLOSED
52             = (int) (ANIMATION_DURATION_HEADS_UP_APPEAR
53                     * HeadsUpAppearInterpolator.getFractionUntilOvershoot());
54     public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 300;
55     public static final int ANIMATION_DURATION_PULSE_APPEAR =
56             KeyguardSliceView.DEFAULT_ANIM_DURATION;
57     public static final int ANIMATION_DURATION_BLOCKING_HELPER_FADE = 240;
58     public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80;
59     public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32;
60     public static final int ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE = 48;
61     public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2;
62     public static final int ANIMATION_DELAY_HEADS_UP = 120;
63     public static final int ANIMATION_DELAY_HEADS_UP_CLICKED= 120;
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 
StackStateAnimator(NotificationStackScrollLayout hostLayout)91     public StackStateAnimator(NotificationStackScrollLayout hostLayout) {
92         mHostLayout = hostLayout;
93         mGoToFullShadeAppearingTranslation =
94                 hostLayout.getContext().getResources().getDimensionPixelSize(
95                         R.dimen.go_to_full_shade_appearing_translation);
96         mPulsingAppearingTranslation =
97                 hostLayout.getContext().getResources().getDimensionPixelSize(
98                         R.dimen.pulsing_notification_appear_translation);
99         mAnimationProperties = new AnimationProperties() {
100             @Override
101             public AnimationFilter getAnimationFilter() {
102                 return mAnimationFilter;
103             }
104 
105             @Override
106             public AnimatorListenerAdapter getAnimationFinishListener() {
107                 return getGlobalAnimationFinishedListener();
108             }
109 
110             @Override
111             public boolean wasAdded(View view) {
112                 return mNewAddChildren.contains(view);
113             }
114 
115             @Override
116             public Interpolator getCustomInterpolator(View child, Property property) {
117                 if (mHeadsUpAppearChildren.contains(child) && View.TRANSLATION_Y.equals(property)) {
118                     return Interpolators.HEADS_UP_APPEAR;
119                 }
120                 return null;
121             }
122         };
123     }
124 
isRunning()125     public boolean isRunning() {
126         return !mAnimatorSet.isEmpty();
127     }
128 
startAnimationForEvents( ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents, long additionalDelay)129     public void startAnimationForEvents(
130             ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents,
131             long additionalDelay) {
132 
133         processAnimationEvents(mAnimationEvents);
134 
135         int childCount = mHostLayout.getChildCount();
136         mAnimationFilter.applyCombination(mNewEvents);
137         mCurrentAdditionalDelay = additionalDelay;
138         mCurrentLength = NotificationStackScrollLayout.AnimationEvent.combineLength(mNewEvents);
139         // Used to stagger concurrent animations' delays and durations for visual effect
140         int animationStaggerCount = 0;
141         for (int i = 0; i < childCount; i++) {
142             final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
143 
144             ExpandableViewState viewState = child.getViewState();
145             if (viewState == null || child.getVisibility() == View.GONE
146                     || applyWithoutAnimation(child, viewState)) {
147                 continue;
148             }
149 
150             if (mAnimationProperties.wasAdded(child) && animationStaggerCount < MAX_STAGGER_COUNT) {
151                 animationStaggerCount++;
152             }
153             initAnimationProperties(child, viewState, animationStaggerCount);
154             viewState.animateTo(child, mAnimationProperties);
155         }
156         if (!isRunning()) {
157             // no child has preformed any animation, lets finish
158             onAnimationFinished();
159         }
160         mHeadsUpAppearChildren.clear();
161         mHeadsUpDisappearChildren.clear();
162         mNewEvents.clear();
163         mNewAddChildren.clear();
164     }
165 
initAnimationProperties(ExpandableView child, ExpandableViewState viewState, int animationStaggerCount)166     private void initAnimationProperties(ExpandableView child,
167             ExpandableViewState viewState, int animationStaggerCount) {
168         boolean wasAdded = mAnimationProperties.wasAdded(child);
169         mAnimationProperties.duration = mCurrentLength;
170         adaptDurationWhenGoingToFullShade(child, viewState, wasAdded, animationStaggerCount);
171         mAnimationProperties.delay = 0;
172         if (wasAdded || mAnimationFilter.hasDelays
173                         && (viewState.yTranslation != child.getTranslationY()
174                         || viewState.zTranslation != child.getTranslationZ()
175                         || viewState.alpha != child.getAlpha()
176                         || viewState.height != child.getActualHeight()
177                         || viewState.clipTopAmount != child.getClipTopAmount()
178                         || viewState.dark != child.isDark())) {
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                     StatusBarIconView icon = row.getEntry().icon;
456                     if (icon.getParent() != null) {
457                         icon.getLocationOnScreen(mTmpLocation);
458                         float iconPosition = mTmpLocation[0] - icon.getTranslationX()
459                                 + ViewState.getFinalTranslationX(icon) + icon.getWidth() * 0.25f;
460                         mHostLayout.getLocationOnScreen(mTmpLocation);
461                         targetLocation = iconPosition - mTmpLocation[0];
462                     }
463                 }
464 
465                 if (needsAnimation) {
466                     // We need to add the global animation listener, since once no animations are
467                     // running anymore, the panel will instantly hide itself. We need to wait until
468                     // the animation is fully finished for this though.
469                     long removeAnimationDelay = changingView.performRemoveAnimation(
470                             ANIMATION_DURATION_HEADS_UP_DISAPPEAR + ANIMATION_DELAY_HEADS_UP,
471                             extraDelay, 0.0f, true /* isHeadsUpAppear */, targetLocation,
472                             endRunnable, getGlobalAnimationFinishedListener());
473                     mAnimationProperties.delay += removeAnimationDelay;
474                 } else if (endRunnable != null) {
475                     endRunnable.run();
476                 }
477             }
478             mNewEvents.add(event);
479         }
480     }
481 
removeTransientView(ExpandableView viewToRemove)482     public static void removeTransientView(ExpandableView viewToRemove) {
483         if (viewToRemove.getTransientContainer() != null) {
484             viewToRemove.getTransientContainer().removeTransientView(viewToRemove);
485         }
486     }
487 
animateOverScrollToAmount(float targetAmount, final boolean onTop, final boolean isRubberbanded)488     public void animateOverScrollToAmount(float targetAmount, final boolean onTop,
489             final boolean isRubberbanded) {
490         final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop);
491         if (targetAmount == startOverScrollAmount) {
492             return;
493         }
494         cancelOverScrollAnimators(onTop);
495         ValueAnimator overScrollAnimator = ValueAnimator.ofFloat(startOverScrollAmount,
496                 targetAmount);
497         overScrollAnimator.setDuration(ANIMATION_DURATION_STANDARD);
498         overScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
499             @Override
500             public void onAnimationUpdate(ValueAnimator animation) {
501                 float currentOverScroll = (float) animation.getAnimatedValue();
502                 mHostLayout.setOverScrollAmount(
503                         currentOverScroll, onTop, false /* animate */, false /* cancelAnimators */,
504                         isRubberbanded);
505             }
506         });
507         overScrollAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
508         overScrollAnimator.addListener(new AnimatorListenerAdapter() {
509             @Override
510             public void onAnimationEnd(Animator animation) {
511                 if (onTop) {
512                     mTopOverScrollAnimator = null;
513                 } else {
514                     mBottomOverScrollAnimator = null;
515                 }
516             }
517         });
518         overScrollAnimator.start();
519         if (onTop) {
520             mTopOverScrollAnimator = overScrollAnimator;
521         } else {
522             mBottomOverScrollAnimator = overScrollAnimator;
523         }
524     }
525 
cancelOverScrollAnimators(boolean onTop)526     public void cancelOverScrollAnimators(boolean onTop) {
527         ValueAnimator currentAnimator = onTop ? mTopOverScrollAnimator : mBottomOverScrollAnimator;
528         if (currentAnimator != null) {
529             currentAnimator.cancel();
530         }
531     }
532 
setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom)533     public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) {
534         mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom;
535     }
536 
setShadeExpanded(boolean shadeExpanded)537     public void setShadeExpanded(boolean shadeExpanded) {
538         mShadeExpanded = shadeExpanded;
539     }
540 
setShelf(NotificationShelf shelf)541     public void setShelf(NotificationShelf shelf) {
542         mShelf = shelf;
543     }
544 }
545