• 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 
25 import com.android.keyguard.KeyguardSliceView;
26 import com.android.systemui.R;
27 import com.android.systemui.animation.Interpolators;
28 import com.android.systemui.shared.clocks.AnimatableClockView;
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 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
35 
36 import java.util.ArrayList;
37 import java.util.HashSet;
38 import java.util.Stack;
39 
40 /**
41  * An stack state animator which handles animations to new StackScrollStates
42  */
43 public class StackStateAnimator {
44 
45     public static final int ANIMATION_DURATION_STANDARD = 360;
46     public static final int ANIMATION_DURATION_CORNER_RADIUS = 200;
47     public static final int ANIMATION_DURATION_WAKEUP = 500;
48     public static final int ANIMATION_DURATION_WAKEUP_SCRIM = 667;
49     public static final int ANIMATION_DURATION_GO_TO_FULL_SHADE = 448;
50     public static final int ANIMATION_DURATION_APPEAR_DISAPPEAR = 464;
51     public static final int ANIMATION_DURATION_SWIPE = 200;
52     public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220;
53     public static final int ANIMATION_DURATION_CLOSE_REMOTE_INPUT = 150;
54     public static final int ANIMATION_DURATION_HEADS_UP_APPEAR = 400;
55     public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 400;
56     public static final int ANIMATION_DURATION_FOLD_TO_AOD =
57             AnimatableClockView.ANIMATION_DURATION_FOLD_TO_AOD;
58     public static final int ANIMATION_DURATION_PULSE_APPEAR =
59             KeyguardSliceView.DEFAULT_ANIM_DURATION;
60     public static final int ANIMATION_DURATION_BLOCKING_HELPER_FADE = 240;
61     public static final int ANIMATION_DURATION_PRIORITY_CHANGE = 500;
62     public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80;
63     public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32;
64     public static final int ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE = 48;
65     public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2;
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     private StackStateLogger mLogger;
93 
StackStateAnimator(NotificationStackScrollLayout hostLayout)94     public StackStateAnimator(NotificationStackScrollLayout hostLayout) {
95         mHostLayout = hostLayout;
96         mGoToFullShadeAppearingTranslation =
97                 hostLayout.getContext().getResources().getDimensionPixelSize(
98                         R.dimen.go_to_full_shade_appearing_translation);
99         mPulsingAppearingTranslation =
100                 hostLayout.getContext().getResources().getDimensionPixelSize(
101                         R.dimen.pulsing_notification_appear_translation);
102         mAnimationProperties = new AnimationProperties() {
103             @Override
104             public AnimationFilter getAnimationFilter() {
105                 return mAnimationFilter;
106             }
107 
108             @Override
109             public AnimatorListenerAdapter getAnimationFinishListener(Property property) {
110                 return getGlobalAnimationFinishedListener();
111             }
112 
113             @Override
114             public boolean wasAdded(View view) {
115                 return mNewAddChildren.contains(view);
116             }
117         };
118     }
119 
setLogger(StackStateLogger logger)120     protected void setLogger(StackStateLogger logger) {
121         mLogger = logger;
122     }
123 
isRunning()124     public boolean isRunning() {
125         return !mAnimatorSet.isEmpty();
126     }
127 
startAnimationForEvents( ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents, long additionalDelay)128     public void startAnimationForEvents(
129             ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents,
130             long additionalDelay) {
131 
132         processAnimationEvents(mAnimationEvents);
133 
134         int childCount = mHostLayout.getChildCount();
135         mAnimationFilter.applyCombination(mNewEvents);
136         mCurrentAdditionalDelay = additionalDelay;
137         mCurrentLength = NotificationStackScrollLayout.AnimationEvent.combineLength(mNewEvents);
138         // Used to stagger concurrent animations' delays and durations for visual effect
139         int animationStaggerCount = 0;
140         for (int i = 0; i < childCount; i++) {
141             final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
142 
143             ExpandableViewState viewState = child.getViewState();
144             if (viewState == null || child.getVisibility() == View.GONE
145                     || applyWithoutAnimation(child, viewState)) {
146                 continue;
147             }
148 
149             if (mAnimationProperties.wasAdded(child) && animationStaggerCount < MAX_STAGGER_COUNT) {
150                 animationStaggerCount++;
151             }
152             initAnimationProperties(child, viewState, animationStaggerCount);
153             viewState.animateTo(child, mAnimationProperties);
154         }
155         if (!isRunning()) {
156             // no child has preformed any animation, lets finish
157             onAnimationFinished();
158         }
159         mHeadsUpAppearChildren.clear();
160         mHeadsUpDisappearChildren.clear();
161         mNewEvents.clear();
162         mNewAddChildren.clear();
163     }
164 
initAnimationProperties(ExpandableView child, ExpandableViewState viewState, int animationStaggerCount)165     private void initAnimationProperties(ExpandableView child,
166             ExpandableViewState viewState, int animationStaggerCount) {
167         boolean wasAdded = mAnimationProperties.wasAdded(child);
168         mAnimationProperties.duration = mCurrentLength;
169         adaptDurationWhenGoingToFullShade(child, viewState, wasAdded, animationStaggerCount);
170         mAnimationProperties.delay = 0;
171         if (wasAdded || mAnimationFilter.hasDelays
172                         && (viewState.getYTranslation() != child.getTranslationY()
173                         || viewState.getZTranslation() != child.getTranslationZ()
174                         || viewState.getAlpha() != child.getAlpha()
175                         || viewState.height != child.getActualHeight()
176                         || viewState.clipTopAmount != child.getClipTopAmount())) {
177             mAnimationProperties.delay = mCurrentAdditionalDelay
178                     + calculateChildAnimationDelay(viewState, animationStaggerCount);
179         }
180     }
181 
adaptDurationWhenGoingToFullShade(ExpandableView child, ExpandableViewState viewState, boolean wasAdded, int animationStaggerCount)182     private void adaptDurationWhenGoingToFullShade(ExpandableView child,
183             ExpandableViewState viewState, boolean wasAdded, int animationStaggerCount) {
184         boolean isDecorView = child instanceof StackScrollerDecorView;
185         boolean needsAdjustment = wasAdded || isDecorView;
186         if (needsAdjustment && mAnimationFilter.hasGoToFullShadeEvent) {
187             int startOffset = 0;
188             if (!isDecorView) {
189                 startOffset = mGoToFullShadeAppearingTranslation;
190                 float longerDurationFactor = (float) Math.pow(animationStaggerCount, 0.7f);
191                 mAnimationProperties.duration = ANIMATION_DURATION_APPEAR_DISAPPEAR + 50
192                         + (long) (100 * longerDurationFactor);
193             }
194             child.setTranslationY(viewState.getYTranslation() + startOffset);
195         }
196     }
197 
198     /**
199      * Determines if a view should not perform an animation and applies it directly.
200      *
201      * @return true if no animation should be performed
202      */
applyWithoutAnimation(ExpandableView child, ExpandableViewState viewState)203     private boolean applyWithoutAnimation(ExpandableView child, ExpandableViewState viewState) {
204         if (mShadeExpanded) {
205             return false;
206         }
207         if (ViewState.isAnimatingY(child)) {
208             // A Y translation animation is running
209             return false;
210         }
211         if (mHeadsUpDisappearChildren.contains(child) || mHeadsUpAppearChildren.contains(child)) {
212             // This is a heads up animation
213             return false;
214         }
215         if (NotificationStackScrollLayout.isPinnedHeadsUp(child)) {
216             // This is another headsUp which might move. Let's animate!
217             return false;
218         }
219         viewState.applyToView(child);
220         return true;
221     }
222 
calculateChildAnimationDelay(ExpandableViewState viewState, int animationStaggerCount)223     private long calculateChildAnimationDelay(ExpandableViewState viewState,
224             int animationStaggerCount) {
225         if (mAnimationFilter.hasGoToFullShadeEvent) {
226             return calculateDelayGoToFullShade(viewState, animationStaggerCount);
227         }
228         if (mAnimationFilter.customDelay != AnimationFilter.NO_DELAY) {
229             return mAnimationFilter.customDelay;
230         }
231         long minDelay = 0;
232         for (NotificationStackScrollLayout.AnimationEvent event : mNewEvents) {
233             long delayPerElement = ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING;
234             switch (event.animationType) {
235                 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD: {
236                     int ownIndex = viewState.notGoneIndex;
237                     int changingIndex =
238                             ((ExpandableView) (event.mChangingView)).getViewState().notGoneIndex;
239                     int difference = Math.abs(ownIndex - changingIndex);
240                     difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
241                             difference - 1));
242                     long delay = (DELAY_EFFECT_MAX_INDEX_DIFFERENCE - difference) * delayPerElement;
243                     minDelay = Math.max(delay, minDelay);
244                     break;
245                 }
246                 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT:
247                     delayPerElement = ANIMATION_DELAY_PER_ELEMENT_MANUAL;
248                 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE: {
249                     int ownIndex = viewState.notGoneIndex;
250                     boolean noNextView = event.viewAfterChangingView == null;
251                     ExpandableView viewAfterChangingView = noNextView
252                             ? mHostLayout.getLastChildNotGone()
253                             : (ExpandableView) event.viewAfterChangingView;
254                     if (viewAfterChangingView == null) {
255                         // This can happen when the last view in the list is removed.
256                         // Since the shelf is still around and the only view, the code still goes
257                         // in here and tries to calculate the delay for it when case its properties
258                         // have changed.
259                         continue;
260                     }
261                     int nextIndex = viewAfterChangingView.getViewState().notGoneIndex;
262                     if (ownIndex >= nextIndex) {
263                         // we only have the view afterwards
264                         ownIndex++;
265                     }
266                     int difference = Math.abs(ownIndex - nextIndex);
267                     difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
268                             difference - 1));
269                     long delay = difference * delayPerElement;
270                     minDelay = Math.max(delay, minDelay);
271                     break;
272                 }
273                 default:
274                     break;
275             }
276         }
277         return minDelay;
278     }
279 
calculateDelayGoToFullShade(ExpandableViewState viewState, int animationStaggerCount)280     private long calculateDelayGoToFullShade(ExpandableViewState viewState,
281             int animationStaggerCount) {
282         int shelfIndex = mShelf.getNotGoneIndex();
283         float index = viewState.notGoneIndex;
284         long result = 0;
285         if (index > shelfIndex) {
286             float diff = (float) Math.pow(animationStaggerCount, 0.7f);
287             result += (long) (diff * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE * 0.25);
288             index = shelfIndex;
289         }
290         index = (float) Math.pow(index, 0.7f);
291         result += (long) (index * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE);
292         return result;
293     }
294 
295     /**
296      * @return an adapter which ensures that onAnimationFinished is called once no animation is
297      *         running anymore
298      */
getGlobalAnimationFinishedListener()299     private AnimatorListenerAdapter getGlobalAnimationFinishedListener() {
300         if (!mAnimationListenerPool.empty()) {
301             return mAnimationListenerPool.pop();
302         }
303 
304         // We need to create a new one, no reusable ones found
305         return new AnimatorListenerAdapter() {
306             private boolean mWasCancelled;
307 
308             @Override
309             public void onAnimationEnd(Animator animation) {
310                 mAnimatorSet.remove(animation);
311                 if (mAnimatorSet.isEmpty() && !mWasCancelled) {
312                     onAnimationFinished();
313                 }
314                 mAnimationListenerPool.push(this);
315             }
316 
317             @Override
318             public void onAnimationCancel(Animator animation) {
319                 mWasCancelled = true;
320             }
321 
322             @Override
323             public void onAnimationStart(Animator animation) {
324                 mWasCancelled = false;
325                 mAnimatorSet.add(animation);
326             }
327         };
328     }
329 
onAnimationFinished()330     private void onAnimationFinished() {
331         mHostLayout.onChildAnimationFinished();
332 
333         for (ExpandableView transientViewToRemove : mTransientViewsToRemove) {
334             transientViewToRemove.removeFromTransientContainer();
335         }
336         mTransientViewsToRemove.clear();
337     }
338 
339     /**
340      * Process the animationEvents for a new animation
341      *
342      *  @param animationEvents the animation events for the animation to perform
343      */
processAnimationEvents( ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents)344     private void processAnimationEvents(
345             ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents) {
346         for (NotificationStackScrollLayout.AnimationEvent event : animationEvents) {
347             final ExpandableView changingView = (ExpandableView) event.mChangingView;
348             boolean loggable = false;
349             boolean isHeadsUp = false;
350             String key = null;
351             if (changingView instanceof ExpandableNotificationRow && mLogger != null) {
352                 loggable = true;
353                 isHeadsUp = ((ExpandableNotificationRow) changingView).isHeadsUp();
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 && isHeadsUp) {
411                     mLogger.logHUNViewDisappearingWithRemoveEvent(key);
412                     String finalKey = key;
413                     postAnimation = () -> {
414                         mLogger.disappearAnimationEnded(finalKey);
415                         changingView.removeFromTransientContainer();
416                     };
417                 }
418                 changingView.performRemoveAnimation(ANIMATION_DURATION_APPEAR_DISAPPEAR,
419                         0 /* delay */, translationDirection, false /* isHeadsUpAppear */,
420                         0, postAnimation, null);
421             } else if (event.animationType ==
422                 NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT) {
423                 if (mHostLayout.isFullySwipedOut(changingView)) {
424                     changingView.removeFromTransientContainer();
425                 }
426             } else if (event.animationType == NotificationStackScrollLayout
427                     .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) {
428                 ExpandableNotificationRow row = (ExpandableNotificationRow) event.mChangingView;
429                 row.prepareExpansionChanged();
430             } else if (event.animationType == NotificationStackScrollLayout
431                     .AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) {
432                 // This item is added, initialize it's properties.
433                 ExpandableViewState viewState = changingView.getViewState();
434                 mTmpState.copyFrom(viewState);
435                 if (event.headsUpFromBottom) {
436                     mTmpState.setYTranslation(mHeadsUpAppearHeightBottom);
437                 } else {
438                     Runnable onAnimationEnd = null;
439                     if (loggable) {
440                         String finalKey = key;
441                         onAnimationEnd = () -> mLogger.appearAnimationEnded(finalKey);
442                     }
443                     changingView.performAddAnimation(0, ANIMATION_DURATION_HEADS_UP_APPEAR,
444                             true /* isHeadsUpAppear */, onAnimationEnd);
445                 }
446                 mHeadsUpAppearChildren.add(changingView);
447                 // this only captures HEADS_UP_APPEAR animations, but HUNs can appear with normal
448                 // ADD animations, which would not be logged here.
449                 if (loggable) {
450                     mLogger.logHUNViewAppearing(key);
451                 }
452 
453                 mTmpState.applyToView(changingView);
454             } else if (event.animationType == NotificationStackScrollLayout
455                             .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR ||
456                     event.animationType == NotificationStackScrollLayout
457                             .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK) {
458                 mHeadsUpDisappearChildren.add(changingView);
459                 Runnable endRunnable = null;
460                 if (changingView.getParent() == null) {
461                     // This notification was actually removed, so we need to add it transiently
462                     mHostLayout.addTransientView(changingView, 0);
463                     changingView.setTransientContainer(mHostLayout);
464                     mTmpState.initFrom(changingView);
465                     endRunnable = changingView::removeFromTransientContainer;
466                 }
467                 float targetLocation = 0;
468                 boolean needsAnimation = true;
469                 if (changingView instanceof ExpandableNotificationRow) {
470                     ExpandableNotificationRow row = (ExpandableNotificationRow) changingView;
471                     if (row.isDismissed()) {
472                         needsAnimation = false;
473                     }
474 
475                     NotificationEntry entry = row.getEntry();
476                     StatusBarIconView icon = entry.getIcons().getStatusBarIcon();
477                     final StatusBarIconView centeredIcon = entry.getIcons().getCenteredIcon();
478                     if (centeredIcon != null && centeredIcon.getParent() != null) {
479                         icon = centeredIcon;
480                     }
481                     if (icon.getParent() != null) {
482                         icon.getLocationOnScreen(mTmpLocation);
483                         float iconPosition = mTmpLocation[0] - icon.getTranslationX()
484                                 + ViewState.getFinalTranslationX(icon)
485                                 + icon.getWidth() * 0.25f;
486                         mHostLayout.getLocationOnScreen(mTmpLocation);
487                         targetLocation = iconPosition - mTmpLocation[0];
488                     }
489                 }
490 
491                 if (needsAnimation) {
492                     // We need to add the global animation listener, since once no animations are
493                     // running anymore, the panel will instantly hide itself. We need to wait until
494                     // the animation is fully finished for this though.
495                     Runnable postAnimation = endRunnable;
496                     if (loggable) {
497                         mLogger.logHUNViewDisappearing(key);
498 
499                         Runnable finalEndRunnable = endRunnable;
500                         String finalKey1 = key;
501                         postAnimation = () -> {
502                             mLogger.disappearAnimationEnded(finalKey1);
503                             if (finalEndRunnable != null) finalEndRunnable.run();
504                         };
505                     }
506                     long removeAnimationDelay = changingView.performRemoveAnimation(
507                             ANIMATION_DURATION_HEADS_UP_DISAPPEAR,
508                             0, 0.0f, true /* isHeadsUpAppear */, targetLocation,
509                             postAnimation, getGlobalAnimationFinishedListener());
510                     mAnimationProperties.delay += removeAnimationDelay;
511                 } else if (endRunnable != null) {
512                     endRunnable.run();
513                 }
514             }
515             mNewEvents.add(event);
516         }
517     }
518 
animateOverScrollToAmount(float targetAmount, final boolean onTop, final boolean isRubberbanded)519     public void animateOverScrollToAmount(float targetAmount, final boolean onTop,
520             final boolean isRubberbanded) {
521         final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop);
522         if (targetAmount == startOverScrollAmount) {
523             return;
524         }
525         cancelOverScrollAnimators(onTop);
526         ValueAnimator overScrollAnimator = ValueAnimator.ofFloat(startOverScrollAmount,
527                 targetAmount);
528         overScrollAnimator.setDuration(ANIMATION_DURATION_STANDARD);
529         overScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
530             @Override
531             public void onAnimationUpdate(ValueAnimator animation) {
532                 float currentOverScroll = (float) animation.getAnimatedValue();
533                 mHostLayout.setOverScrollAmount(
534                         currentOverScroll, onTop, false /* animate */, false /* cancelAnimators */,
535                         isRubberbanded);
536             }
537         });
538         overScrollAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
539         overScrollAnimator.addListener(new AnimatorListenerAdapter() {
540             @Override
541             public void onAnimationEnd(Animator animation) {
542                 if (onTop) {
543                     mTopOverScrollAnimator = null;
544                 } else {
545                     mBottomOverScrollAnimator = null;
546                 }
547             }
548         });
549         overScrollAnimator.start();
550         if (onTop) {
551             mTopOverScrollAnimator = overScrollAnimator;
552         } else {
553             mBottomOverScrollAnimator = overScrollAnimator;
554         }
555     }
556 
cancelOverScrollAnimators(boolean onTop)557     public void cancelOverScrollAnimators(boolean onTop) {
558         ValueAnimator currentAnimator = onTop ? mTopOverScrollAnimator : mBottomOverScrollAnimator;
559         if (currentAnimator != null) {
560             currentAnimator.cancel();
561         }
562     }
563 
setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom)564     public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) {
565         mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom;
566     }
567 
setShadeExpanded(boolean shadeExpanded)568     public void setShadeExpanded(boolean shadeExpanded) {
569         mShadeExpanded = shadeExpanded;
570     }
571 
setShelf(NotificationShelf shelf)572     public void setShelf(NotificationShelf shelf) {
573         mShelf = shelf;
574     }
575 }
576