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