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.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.ViewGroup;
25 import android.view.animation.Interpolator;
26 
27 import com.android.systemui.Interpolators;
28 import com.android.systemui.R;
29 import com.android.systemui.statusbar.ExpandableNotificationRow;
30 import com.android.systemui.statusbar.ExpandableView;
31 import com.android.systemui.statusbar.NotificationShelf;
32 import com.android.systemui.statusbar.StatusBarIconView;
33 
34 import java.util.ArrayList;
35 import java.util.HashSet;
36 import java.util.Stack;
37 
38 /**
39  * An stack state animator which handles animations to new StackScrollStates
40  */
41 public class StackStateAnimator {
42 
43     public static final int ANIMATION_DURATION_STANDARD = 360;
44     public static final int ANIMATION_DURATION_WAKEUP = 500;
45     public static final int ANIMATION_DURATION_GO_TO_FULL_SHADE = 448;
46     public static final int ANIMATION_DURATION_APPEAR_DISAPPEAR = 464;
47     public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220;
48     public static final int ANIMATION_DURATION_CLOSE_REMOTE_INPUT = 150;
49     public static final int ANIMATION_DURATION_HEADS_UP_APPEAR = 550;
50     public static final int ANIMATION_DURATION_HEADS_UP_APPEAR_CLOSED
51             = (int) (ANIMATION_DURATION_HEADS_UP_APPEAR
52                     * HeadsUpAppearInterpolator.getFractionUntilOvershoot());
53     public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 300;
54     public static final int ANIMATION_DURATION_BLOCKING_HELPER_FADE = 240;
55     public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80;
56     public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32;
57     public static final int ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE = 48;
58     public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2;
59     public static final int ANIMATION_DELAY_HEADS_UP = 120;
60     public static final int ANIMATION_DELAY_HEADS_UP_CLICKED= 120;
61 
62     private final int mGoToFullShadeAppearingTranslation;
63     private final int mPulsingAppearingTranslation;
64     private final ExpandableViewState mTmpState = new ExpandableViewState();
65     private final AnimationProperties mAnimationProperties;
66     public NotificationStackScrollLayout mHostLayout;
67     private ArrayList<NotificationStackScrollLayout.AnimationEvent> mNewEvents =
68             new ArrayList<>();
69     private ArrayList<View> mNewAddChildren = new ArrayList<>();
70     private HashSet<View> mHeadsUpAppearChildren = new HashSet<>();
71     private HashSet<View> mHeadsUpDisappearChildren = new HashSet<>();
72     private HashSet<Animator> mAnimatorSet = new HashSet<>();
73     private Stack<AnimatorListenerAdapter> mAnimationListenerPool = new Stack<>();
74     private AnimationFilter mAnimationFilter = new AnimationFilter();
75     private long mCurrentLength;
76     private long mCurrentAdditionalDelay;
77 
78     /** The current index for the last child which was not added in this event set. */
79     private int mCurrentLastNotAddedIndex;
80     private ValueAnimator mTopOverScrollAnimator;
81     private ValueAnimator mBottomOverScrollAnimator;
82     private int mHeadsUpAppearHeightBottom;
83     private boolean mShadeExpanded;
84     private ArrayList<ExpandableView> mTransientViewsToRemove = new ArrayList<>();
85     private NotificationShelf mShelf;
86     private float mStatusBarIconLocation;
87     private int[] mTmpLocation = new int[2];
88 
StackStateAnimator(NotificationStackScrollLayout hostLayout)89     public StackStateAnimator(NotificationStackScrollLayout hostLayout) {
90         mHostLayout = hostLayout;
91         mGoToFullShadeAppearingTranslation =
92                 hostLayout.getContext().getResources().getDimensionPixelSize(
93                         R.dimen.go_to_full_shade_appearing_translation);
94         mPulsingAppearingTranslation =
95                 hostLayout.getContext().getResources().getDimensionPixelSize(
96                         R.dimen.pulsing_notification_appear_translation);
97         mAnimationProperties = new AnimationProperties() {
98             @Override
99             public AnimationFilter getAnimationFilter() {
100                 return mAnimationFilter;
101             }
102 
103             @Override
104             public AnimatorListenerAdapter getAnimationFinishListener() {
105                 return getGlobalAnimationFinishedListener();
106             }
107 
108             @Override
109             public boolean wasAdded(View view) {
110                 return mNewAddChildren.contains(view);
111             }
112 
113             @Override
114             public Interpolator getCustomInterpolator(View child, Property property) {
115                 if (mHeadsUpAppearChildren.contains(child) && View.TRANSLATION_Y.equals(property)) {
116                     return Interpolators.HEADS_UP_APPEAR;
117                 }
118                 return null;
119             }
120         };
121     }
122 
isRunning()123     public boolean isRunning() {
124         return !mAnimatorSet.isEmpty();
125     }
126 
startAnimationForEvents( ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents, StackScrollState finalState, long additionalDelay)127     public void startAnimationForEvents(
128             ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents,
129             StackScrollState finalState, long additionalDelay) {
130 
131         processAnimationEvents(mAnimationEvents, finalState);
132 
133         int childCount = mHostLayout.getChildCount();
134         mAnimationFilter.applyCombination(mNewEvents);
135         mCurrentAdditionalDelay = additionalDelay;
136         mCurrentLength = NotificationStackScrollLayout.AnimationEvent.combineLength(mNewEvents);
137         mCurrentLastNotAddedIndex = findLastNotAddedIndex(finalState);
138         for (int i = 0; i < childCount; i++) {
139             final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
140 
141             ExpandableViewState viewState = finalState.getViewStateForView(child);
142             if (viewState == null || child.getVisibility() == View.GONE
143                     || applyWithoutAnimation(child, viewState, finalState)) {
144                 continue;
145             }
146 
147             initAnimationProperties(finalState, child, viewState);
148             viewState.animateTo(child, mAnimationProperties);
149         }
150         if (!isRunning()) {
151             // no child has preformed any animation, lets finish
152             onAnimationFinished();
153         }
154         mHeadsUpAppearChildren.clear();
155         mHeadsUpDisappearChildren.clear();
156         mNewEvents.clear();
157         mNewAddChildren.clear();
158     }
159 
initAnimationProperties(StackScrollState finalState, ExpandableView child, ExpandableViewState viewState)160     private void initAnimationProperties(StackScrollState finalState, ExpandableView child,
161             ExpandableViewState viewState) {
162         boolean wasAdded = mAnimationProperties.wasAdded(child);
163         mAnimationProperties.duration = mCurrentLength;
164         adaptDurationWhenGoingToFullShade(child, viewState, wasAdded);
165         mAnimationProperties.delay = 0;
166         if (wasAdded || mAnimationFilter.hasDelays
167                         && (viewState.yTranslation != child.getTranslationY()
168                         || viewState.zTranslation != child.getTranslationZ()
169                         || viewState.alpha != child.getAlpha()
170                         || viewState.height != child.getActualHeight()
171                         || viewState.clipTopAmount != child.getClipTopAmount()
172                         || viewState.dark != child.isDark()
173                         || viewState.shadowAlpha != child.getShadowAlpha())) {
174             mAnimationProperties.delay = mCurrentAdditionalDelay
175                     + calculateChildAnimationDelay(viewState, finalState);
176         }
177     }
178 
adaptDurationWhenGoingToFullShade(ExpandableView child, ExpandableViewState viewState, boolean wasAdded)179     private void adaptDurationWhenGoingToFullShade(ExpandableView child,
180             ExpandableViewState viewState, boolean wasAdded) {
181         if (wasAdded && mAnimationFilter.hasGoToFullShadeEvent) {
182             child.setTranslationY(child.getTranslationY() + mGoToFullShadeAppearingTranslation);
183             float longerDurationFactor = viewState.notGoneIndex - mCurrentLastNotAddedIndex;
184             longerDurationFactor = (float) Math.pow(longerDurationFactor, 0.7f);
185             mAnimationProperties.duration = ANIMATION_DURATION_APPEAR_DISAPPEAR + 50 +
186                     (long) (100 * longerDurationFactor);
187         }
188     }
189 
190     /**
191      * Determines if a view should not perform an animation and applies it directly.
192      *
193      * @return true if no animation should be performed
194      */
applyWithoutAnimation(ExpandableView child, ExpandableViewState viewState, StackScrollState finalState)195     private boolean applyWithoutAnimation(ExpandableView child, ExpandableViewState viewState,
196             StackScrollState finalState) {
197         if (mShadeExpanded) {
198             return false;
199         }
200         if (ViewState.isAnimatingY(child)) {
201             // A Y translation animation is running
202             return false;
203         }
204         if (mHeadsUpDisappearChildren.contains(child) || mHeadsUpAppearChildren.contains(child)) {
205             // This is a heads up animation
206             return false;
207         }
208         if (NotificationStackScrollLayout.isPinnedHeadsUp(child)) {
209             // This is another headsUp which might move. Let's animate!
210             return false;
211         }
212         viewState.applyToView(child);
213         return true;
214     }
215 
findLastNotAddedIndex(StackScrollState finalState)216     private int findLastNotAddedIndex(StackScrollState finalState) {
217         int childCount = mHostLayout.getChildCount();
218         for (int i = childCount - 1; i >= 0; i--) {
219             final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
220 
221             ExpandableViewState viewState = finalState.getViewStateForView(child);
222             if (viewState == null || child.getVisibility() == View.GONE) {
223                 continue;
224             }
225             if (!mNewAddChildren.contains(child)) {
226                 return viewState.notGoneIndex;
227             }
228         }
229         return -1;
230     }
231 
calculateChildAnimationDelay(ExpandableViewState viewState, StackScrollState finalState)232     private long calculateChildAnimationDelay(ExpandableViewState viewState,
233             StackScrollState finalState) {
234         if (mAnimationFilter.hasGoToFullShadeEvent) {
235             return calculateDelayGoToFullShade(viewState);
236         }
237         if (mAnimationFilter.customDelay != AnimationFilter.NO_DELAY) {
238             return mAnimationFilter.customDelay;
239         }
240         long minDelay = 0;
241         for (NotificationStackScrollLayout.AnimationEvent event : mNewEvents) {
242             long delayPerElement = ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING;
243             switch (event.animationType) {
244                 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD: {
245                     int ownIndex = viewState.notGoneIndex;
246                     int changingIndex = finalState
247                             .getViewStateForView(event.changingView).notGoneIndex;
248                     int difference = Math.abs(ownIndex - changingIndex);
249                     difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
250                             difference - 1));
251                     long delay = (DELAY_EFFECT_MAX_INDEX_DIFFERENCE - difference) * delayPerElement;
252                     minDelay = Math.max(delay, minDelay);
253                     break;
254                 }
255                 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT:
256                     delayPerElement = ANIMATION_DELAY_PER_ELEMENT_MANUAL;
257                 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE: {
258                     int ownIndex = viewState.notGoneIndex;
259                     boolean noNextView = event.viewAfterChangingView == null;
260                     View viewAfterChangingView = noNextView
261                             ? mHostLayout.getLastChildNotGone()
262                             : event.viewAfterChangingView;
263                     if (viewAfterChangingView == null) {
264                         // This can happen when the last view in the list is removed.
265                         // Since the shelf is still around and the only view, the code still goes
266                         // in here and tries to calculate the delay for it when case its properties
267                         // have changed.
268                         continue;
269                     }
270                     int nextIndex = finalState
271                             .getViewStateForView(viewAfterChangingView).notGoneIndex;
272                     if (ownIndex >= nextIndex) {
273                         // we only have the view afterwards
274                         ownIndex++;
275                     }
276                     int difference = Math.abs(ownIndex - nextIndex);
277                     difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
278                             difference - 1));
279                     long delay = difference * delayPerElement;
280                     minDelay = Math.max(delay, minDelay);
281                     break;
282                 }
283                 default:
284                     break;
285             }
286         }
287         return minDelay;
288     }
289 
calculateDelayGoToFullShade(ExpandableViewState viewState)290     private long calculateDelayGoToFullShade(ExpandableViewState viewState) {
291         int shelfIndex = mShelf.getNotGoneIndex();
292         float index = viewState.notGoneIndex;
293         long result = 0;
294         if (index > shelfIndex) {
295             float diff = index - shelfIndex;
296             diff = (float) Math.pow(diff, 0.7f);
297             result += (long) (diff * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE * 0.25);
298             index = shelfIndex;
299         }
300         index = (float) Math.pow(index, 0.7f);
301         result += (long) (index * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE);
302         return result;
303     }
304 
305     /**
306      * @return an adapter which ensures that onAnimationFinished is called once no animation is
307      *         running anymore
308      */
getGlobalAnimationFinishedListener()309     private AnimatorListenerAdapter getGlobalAnimationFinishedListener() {
310         if (!mAnimationListenerPool.empty()) {
311             return mAnimationListenerPool.pop();
312         }
313 
314         // We need to create a new one, no reusable ones found
315         return new AnimatorListenerAdapter() {
316             private boolean mWasCancelled;
317 
318             @Override
319             public void onAnimationEnd(Animator animation) {
320                 mAnimatorSet.remove(animation);
321                 if (mAnimatorSet.isEmpty() && !mWasCancelled) {
322                     onAnimationFinished();
323                 }
324                 mAnimationListenerPool.push(this);
325             }
326 
327             @Override
328             public void onAnimationCancel(Animator animation) {
329                 mWasCancelled = true;
330             }
331 
332             @Override
333             public void onAnimationStart(Animator animation) {
334                 mWasCancelled = false;
335                 mAnimatorSet.add(animation);
336             }
337         };
338     }
339 
onAnimationFinished()340     private void onAnimationFinished() {
341         mHostLayout.onChildAnimationFinished();
342 
343         for (ExpandableView transientViewsToRemove : mTransientViewsToRemove) {
344             transientViewsToRemove.getTransientContainer()
345                     .removeTransientView(transientViewsToRemove);
346         }
347         mTransientViewsToRemove.clear();
348     }
349 
350     /**
351      * Process the animationEvents for a new animation
352      *
353      * @param animationEvents the animation events for the animation to perform
354      * @param finalState the final state to animate to
355      */
processAnimationEvents( ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents, StackScrollState finalState)356     private void processAnimationEvents(
357             ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents,
358             StackScrollState finalState) {
359         for (NotificationStackScrollLayout.AnimationEvent event : animationEvents) {
360             final ExpandableView changingView = (ExpandableView) event.changingView;
361             if (event.animationType ==
362                     NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD) {
363 
364                 // This item is added, initialize it's properties.
365                 ExpandableViewState viewState = finalState
366                         .getViewStateForView(changingView);
367                 if (viewState == null || viewState.gone) {
368                     // The position for this child was never generated, let's continue.
369                     continue;
370                 }
371                 viewState.applyToView(changingView);
372                 mNewAddChildren.add(changingView);
373 
374             } else if (event.animationType ==
375                     NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE) {
376                 if (changingView.getVisibility() != View.VISIBLE) {
377                     removeTransientView(changingView);
378                     continue;
379                 }
380 
381                 // Find the amount to translate up. This is needed in order to understand the
382                 // direction of the remove animation (either downwards or upwards)
383                 ExpandableViewState viewState = finalState
384                         .getViewStateForView(event.viewAfterChangingView);
385                 int actualHeight = changingView.getActualHeight();
386                 // upwards by default
387                 float translationDirection = -1.0f;
388                 if (viewState != null) {
389                     float ownPosition = changingView.getTranslationY();
390                     if (changingView instanceof ExpandableNotificationRow
391                             && event.viewAfterChangingView instanceof ExpandableNotificationRow) {
392                         ExpandableNotificationRow changingRow =
393                                 (ExpandableNotificationRow) changingView;
394                         ExpandableNotificationRow nextRow =
395                                 (ExpandableNotificationRow) event.viewAfterChangingView;
396                         if (changingRow.isRemoved()
397                                 && changingRow.wasChildInGroupWhenRemoved()
398                                 && !nextRow.isChildInGroup()) {
399                             // the next row isn't actually a child from a group! Let's
400                             // compare absolute positions!
401                             ownPosition = changingRow.getTranslationWhenRemoved();
402                         }
403                     }
404                     // there was a view after this one, Approximate the distance the next child
405                     // travelled
406                     translationDirection = ((viewState.yTranslation
407                             - (ownPosition + actualHeight / 2.0f)) * 2 /
408                             actualHeight);
409                     translationDirection = Math.max(Math.min(translationDirection, 1.0f),-1.0f);
410 
411                 }
412                 changingView.performRemoveAnimation(ANIMATION_DURATION_APPEAR_DISAPPEAR,
413                         0 /* delay */, translationDirection,  false /* isHeadsUpAppear */,
414                         0, new Runnable() {
415                     @Override
416                     public void run() {
417                         removeTransientView(changingView);
418                     }
419                 }, null);
420             } else if (event.animationType ==
421                 NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT) {
422                 if (Math.abs(changingView.getTranslation()) == changingView.getWidth()
423                         && changingView.getTransientContainer() != null) {
424                     changingView.getTransientContainer().removeTransientView(changingView);
425                 }
426             } else if (event.animationType == NotificationStackScrollLayout
427                     .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) {
428                 ExpandableNotificationRow row = (ExpandableNotificationRow) event.changingView;
429                 row.prepareExpansionChanged(finalState);
430             } else if (event.animationType == NotificationStackScrollLayout
431                     .AnimationEvent.ANIMATION_TYPE_PULSE_APPEAR) {
432                 ExpandableViewState viewState = finalState.getViewStateForView(changingView);
433                 mTmpState.copyFrom(viewState);
434                 mTmpState.yTranslation += mPulsingAppearingTranslation;
435                 mTmpState.alpha = 0;
436                 mTmpState.applyToView(changingView);
437             } else if (event.animationType == NotificationStackScrollLayout
438                     .AnimationEvent.ANIMATION_TYPE_PULSE_DISAPPEAR) {
439                 ExpandableViewState viewState = finalState.getViewStateForView(changingView);
440                 viewState.yTranslation += mPulsingAppearingTranslation;
441                 viewState.alpha = 0;
442             } else if (event.animationType == NotificationStackScrollLayout
443                     .AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) {
444                 // This item is added, initialize it's properties.
445                 ExpandableViewState viewState = finalState.getViewStateForView(changingView);
446                 mTmpState.copyFrom(viewState);
447                 if (event.headsUpFromBottom) {
448                     mTmpState.yTranslation = mHeadsUpAppearHeightBottom;
449                 } else {
450                     mTmpState.yTranslation = 0;
451                     changingView.performAddAnimation(0, ANIMATION_DURATION_HEADS_UP_APPEAR_CLOSED,
452                             true /* isHeadsUpAppear */);
453                 }
454                 mHeadsUpAppearChildren.add(changingView);
455                 mTmpState.applyToView(changingView);
456             } else if (event.animationType == NotificationStackScrollLayout
457                             .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR ||
458                     event.animationType == NotificationStackScrollLayout
459                             .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK) {
460                 mHeadsUpDisappearChildren.add(changingView);
461                 Runnable endRunnable = null;
462                 // We need some additional delay in case we were removed to make sure we're not
463                 // lagging
464                 int extraDelay = event.animationType == NotificationStackScrollLayout
465                         .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
466                         ? ANIMATION_DELAY_HEADS_UP_CLICKED
467                         : 0;
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                     mTmpState.yTranslation = 0;
474                     // We temporarily enable Y animations, the real filter will be combined
475                     // afterwards anyway
476                     mAnimationFilter.animateY = true;
477                     mAnimationProperties.delay = extraDelay + ANIMATION_DELAY_HEADS_UP;
478                     mAnimationProperties.duration = ANIMATION_DURATION_HEADS_UP_DISAPPEAR;
479                     mTmpState.animateTo(changingView, mAnimationProperties);
480                     endRunnable = () -> removeTransientView(changingView);
481                 }
482                 float targetLocation = 0;
483                 boolean needsAnimation = true;
484                 if (changingView instanceof ExpandableNotificationRow) {
485                     ExpandableNotificationRow row = (ExpandableNotificationRow) changingView;
486                     if (row.isDismissed()) {
487                         needsAnimation = false;
488                     }
489                     StatusBarIconView icon = row.getEntry().icon;
490                     if (icon.getParent() != null) {
491                         icon.getLocationOnScreen(mTmpLocation);
492                         float iconPosition = mTmpLocation[0] - icon.getTranslationX()
493                                 + ViewState.getFinalTranslationX(icon) + icon.getWidth() * 0.25f;
494                         mHostLayout.getLocationOnScreen(mTmpLocation);
495                         targetLocation = iconPosition - mTmpLocation[0];
496                     }
497                 }
498 
499                 if (needsAnimation) {
500                     // We need to add the global animation listener, since once no animations are
501                     // running anymore, the panel will instantly hide itself. We need to wait until
502                     // the animation is fully finished for this though.
503                     changingView.performRemoveAnimation(ANIMATION_DURATION_HEADS_UP_DISAPPEAR
504                                     + ANIMATION_DELAY_HEADS_UP, extraDelay, 0.0f,
505                             true /* isHeadsUpAppear */, targetLocation, endRunnable,
506                             getGlobalAnimationFinishedListener());
507                 } else if (endRunnable != null) {
508                     endRunnable.run();
509                 }
510             }
511             mNewEvents.add(event);
512         }
513     }
514 
removeTransientView(ExpandableView viewToRemove)515     public static void removeTransientView(ExpandableView viewToRemove) {
516         if (viewToRemove.getTransientContainer() != null) {
517             viewToRemove.getTransientContainer().removeTransientView(viewToRemove);
518         }
519     }
520 
animateOverScrollToAmount(float targetAmount, final boolean onTop, final boolean isRubberbanded)521     public void animateOverScrollToAmount(float targetAmount, final boolean onTop,
522             final boolean isRubberbanded) {
523         final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop);
524         if (targetAmount == startOverScrollAmount) {
525             return;
526         }
527         cancelOverScrollAnimators(onTop);
528         ValueAnimator overScrollAnimator = ValueAnimator.ofFloat(startOverScrollAmount,
529                 targetAmount);
530         overScrollAnimator.setDuration(ANIMATION_DURATION_STANDARD);
531         overScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
532             @Override
533             public void onAnimationUpdate(ValueAnimator animation) {
534                 float currentOverScroll = (float) animation.getAnimatedValue();
535                 mHostLayout.setOverScrollAmount(
536                         currentOverScroll, onTop, false /* animate */, false /* cancelAnimators */,
537                         isRubberbanded);
538             }
539         });
540         overScrollAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
541         overScrollAnimator.addListener(new AnimatorListenerAdapter() {
542             @Override
543             public void onAnimationEnd(Animator animation) {
544                 if (onTop) {
545                     mTopOverScrollAnimator = null;
546                 } else {
547                     mBottomOverScrollAnimator = null;
548                 }
549             }
550         });
551         overScrollAnimator.start();
552         if (onTop) {
553             mTopOverScrollAnimator = overScrollAnimator;
554         } else {
555             mBottomOverScrollAnimator = overScrollAnimator;
556         }
557     }
558 
cancelOverScrollAnimators(boolean onTop)559     public void cancelOverScrollAnimators(boolean onTop) {
560         ValueAnimator currentAnimator = onTop ? mTopOverScrollAnimator : mBottomOverScrollAnimator;
561         if (currentAnimator != null) {
562             currentAnimator.cancel();
563         }
564     }
565 
setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom)566     public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) {
567         mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom;
568     }
569 
setShadeExpanded(boolean shadeExpanded)570     public void setShadeExpanded(boolean shadeExpanded) {
571         mShadeExpanded = shadeExpanded;
572     }
573 
setShelf(NotificationShelf shelf)574     public void setShelf(NotificationShelf shelf) {
575         mShelf = shelf;
576     }
577 }
578