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