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