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.ObjectAnimator;
22 import android.animation.PropertyValuesHolder;
23 import android.animation.ValueAnimator;
24 import android.view.View;
25 import android.view.animation.AnimationUtils;
26 import android.view.animation.Interpolator;
27 
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.SpeedBumpView;
32 import com.android.systemui.statusbar.policy.HeadsUpManager;
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_GO_TO_FULL_SHADE = 448;
45     public static final int ANIMATION_DURATION_APPEAR_DISAPPEAR = 464;
46     public static final int ANIMATION_DURATION_EXPAND_CLICKED = 360;
47     public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220;
48     public static final int ANIMATION_DURATION_HEADS_UP_APPEAR = 650;
49     public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 230;
50     public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80;
51     public static final int ANIMATION_DELAY_PER_ELEMENT_EXPAND_CHILDREN = 54;
52     public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32;
53     public static final int ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE = 48;
54     public static final int ANIMATION_DELAY_PER_ELEMENT_DARK = 24;
55     public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2;
56     public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE_CHILDREN = 3;
57 
58     private static final int TAG_ANIMATOR_TRANSLATION_Y = R.id.translation_y_animator_tag;
59     private static final int TAG_ANIMATOR_TRANSLATION_Z = R.id.translation_z_animator_tag;
60     private static final int TAG_ANIMATOR_SCALE = R.id.scale_animator_tag;
61     private static final int TAG_ANIMATOR_ALPHA = R.id.alpha_animator_tag;
62     private static final int TAG_ANIMATOR_HEIGHT = R.id.height_animator_tag;
63     private static final int TAG_ANIMATOR_TOP_INSET = R.id.top_inset_animator_tag;
64     private static final int TAG_END_TRANSLATION_Y = R.id.translation_y_animator_end_value_tag;
65     private static final int TAG_END_TRANSLATION_Z = R.id.translation_z_animator_end_value_tag;
66     private static final int TAG_END_SCALE = R.id.scale_animator_end_value_tag;
67     private static final int TAG_END_ALPHA = R.id.alpha_animator_end_value_tag;
68     private static final int TAG_END_HEIGHT = R.id.height_animator_end_value_tag;
69     private static final int TAG_END_TOP_INSET = R.id.top_inset_animator_end_value_tag;
70     private static final int TAG_START_TRANSLATION_Y = R.id.translation_y_animator_start_value_tag;
71     private static final int TAG_START_TRANSLATION_Z = R.id.translation_z_animator_start_value_tag;
72     private static final int TAG_START_SCALE = R.id.scale_animator_start_value_tag;
73     private static final int TAG_START_ALPHA = R.id.alpha_animator_start_value_tag;
74     private static final int TAG_START_HEIGHT = R.id.height_animator_start_value_tag;
75     private static final int TAG_START_TOP_INSET = R.id.top_inset_animator_start_value_tag;
76 
77     private final Interpolator mFastOutSlowInInterpolator;
78     private final Interpolator mHeadsUpAppearInterpolator;
79     private final int mGoToFullShadeAppearingTranslation;
80     private final StackViewState mTmpState = new StackViewState();
81     public NotificationStackScrollLayout mHostLayout;
82     private ArrayList<NotificationStackScrollLayout.AnimationEvent> mNewEvents =
83             new ArrayList<>();
84     private ArrayList<View> mNewAddChildren = new ArrayList<>();
85     private HashSet<View> mHeadsUpAppearChildren = new HashSet<>();
86     private HashSet<View> mHeadsUpDisappearChildren = new HashSet<>();
87     private HashSet<Animator> mAnimatorSet = new HashSet<>();
88     private Stack<AnimatorListenerAdapter> mAnimationListenerPool = new Stack<>();
89     private AnimationFilter mAnimationFilter = new AnimationFilter();
90     private long mCurrentLength;
91     private long mCurrentAdditionalDelay;
92 
93     /** The current index for the last child which was not added in this event set. */
94     private int mCurrentLastNotAddedIndex;
95     private ValueAnimator mTopOverScrollAnimator;
96     private ValueAnimator mBottomOverScrollAnimator;
97     private ExpandableNotificationRow mChildExpandingView;
98     private int mHeadsUpAppearHeightBottom;
99     private boolean mShadeExpanded;
100     private ArrayList<View> mChildrenToClearFromOverlay = new ArrayList<>();
101 
StackStateAnimator(NotificationStackScrollLayout hostLayout)102     public StackStateAnimator(NotificationStackScrollLayout hostLayout) {
103         mHostLayout = hostLayout;
104         mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(hostLayout.getContext(),
105                 android.R.interpolator.fast_out_slow_in);
106         mGoToFullShadeAppearingTranslation =
107                 hostLayout.getContext().getResources().getDimensionPixelSize(
108                         R.dimen.go_to_full_shade_appearing_translation);
109         mHeadsUpAppearInterpolator = new HeadsUpAppearInterpolator();
110     }
111 
isRunning()112     public boolean isRunning() {
113         return !mAnimatorSet.isEmpty();
114     }
115 
startAnimationForEvents( ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents, StackScrollState finalState, long additionalDelay)116     public void startAnimationForEvents(
117             ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents,
118             StackScrollState finalState, long additionalDelay) {
119 
120         processAnimationEvents(mAnimationEvents, finalState);
121 
122         int childCount = mHostLayout.getChildCount();
123         mAnimationFilter.applyCombination(mNewEvents);
124         mCurrentAdditionalDelay = additionalDelay;
125         mCurrentLength = NotificationStackScrollLayout.AnimationEvent.combineLength(mNewEvents);
126         mCurrentLastNotAddedIndex = findLastNotAddedIndex(finalState);
127         for (int i = 0; i < childCount; i++) {
128             final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
129 
130             StackViewState viewState = finalState.getViewStateForView(child);
131             if (viewState == null || child.getVisibility() == View.GONE
132                     || applyWithoutAnimation(child, viewState, finalState)) {
133                 continue;
134             }
135 
136             child.setClipTopOptimization(0);
137             startStackAnimations(child, viewState, finalState, i, -1 /* fixedDelay */);
138         }
139         if (!isRunning()) {
140             // no child has preformed any animation, lets finish
141             onAnimationFinished();
142         }
143         mHeadsUpAppearChildren.clear();
144         mHeadsUpDisappearChildren.clear();
145         mNewEvents.clear();
146         mNewAddChildren.clear();
147         mChildExpandingView = null;
148     }
149 
150     /**
151      * Determines if a view should not perform an animation and applies it directly.
152      *
153      * @return true if no animation should be performed
154      */
applyWithoutAnimation(ExpandableView child, StackViewState viewState, StackScrollState finalState)155     private boolean applyWithoutAnimation(ExpandableView child, StackViewState viewState,
156             StackScrollState finalState) {
157         if (mShadeExpanded) {
158             return false;
159         }
160         if (getChildTag(child, TAG_ANIMATOR_TRANSLATION_Y) != null) {
161             // A Y translation animation is running
162             return false;
163         }
164         if (mHeadsUpDisappearChildren.contains(child) || mHeadsUpAppearChildren.contains(child)) {
165             // This is a heads up animation
166             return false;
167         }
168         if (NotificationStackScrollLayout.isPinnedHeadsUp(child)) {
169             // This is another headsUp which might move. Let's animate!
170             return false;
171         }
172         finalState.applyState(child, viewState);
173         return true;
174     }
175 
findLastNotAddedIndex(StackScrollState finalState)176     private int findLastNotAddedIndex(StackScrollState finalState) {
177         int childCount = mHostLayout.getChildCount();
178         for (int i = childCount - 1; i >= 0; i--) {
179             final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
180 
181             StackViewState viewState = finalState.getViewStateForView(child);
182             if (viewState == null || child.getVisibility() == View.GONE) {
183                 continue;
184             }
185             if (!mNewAddChildren.contains(child)) {
186                 return viewState.notGoneIndex;
187             }
188         }
189         return -1;
190     }
191 
192 
193     /**
194      * Start an animation to the given  {@link StackViewState}.
195      *
196      * @param child the child to start the animation on
197      * @param viewState the {@link StackViewState} of the view to animate to
198      * @param finalState the final state after the animation
199      * @param i the index of the view; only relevant if the view is the speed bump and is
200      *          ignored otherwise
201      * @param fixedDelay a fixed delay if desired or -1 if the delay should be calculated
202      */
startStackAnimations(final ExpandableView child, StackViewState viewState, StackScrollState finalState, int i, long fixedDelay)203     public void startStackAnimations(final ExpandableView child, StackViewState viewState,
204             StackScrollState finalState, int i, long fixedDelay) {
205         final float alpha = viewState.alpha;
206         boolean wasAdded = mNewAddChildren.contains(child);
207         long duration = mCurrentLength;
208         if (wasAdded && mAnimationFilter.hasGoToFullShadeEvent) {
209             child.setTranslationY(child.getTranslationY() + mGoToFullShadeAppearingTranslation);
210             float longerDurationFactor = viewState.notGoneIndex - mCurrentLastNotAddedIndex;
211             longerDurationFactor = (float) Math.pow(longerDurationFactor, 0.7f);
212             duration = ANIMATION_DURATION_APPEAR_DISAPPEAR + 50 +
213                     (long) (100 * longerDurationFactor);
214         }
215         boolean yTranslationChanging = child.getTranslationY() != viewState.yTranslation;
216         boolean zTranslationChanging = child.getTranslationZ() != viewState.zTranslation;
217         boolean scaleChanging = child.getScaleX() != viewState.scale;
218         boolean alphaChanging = alpha != child.getAlpha();
219         boolean heightChanging = viewState.height != child.getActualHeight();
220         boolean darkChanging = viewState.dark != child.isDark();
221         boolean topInsetChanging = viewState.clipTopAmount != child.getClipTopAmount();
222         boolean hasDelays = mAnimationFilter.hasDelays;
223         boolean isDelayRelevant = yTranslationChanging || zTranslationChanging || scaleChanging ||
224                 alphaChanging || heightChanging || topInsetChanging || darkChanging;
225         long delay = 0;
226         if (fixedDelay != -1) {
227             delay = fixedDelay;
228         } else if (hasDelays && isDelayRelevant || wasAdded) {
229             delay = mCurrentAdditionalDelay + calculateChildAnimationDelay(viewState, finalState);
230         }
231 
232         startViewAnimations(child, viewState, delay, duration);
233 
234         // start height animation
235         if (heightChanging && child.getActualHeight() != 0) {
236             startHeightAnimation(child, viewState, duration, delay);
237         }
238 
239         // start top inset animation
240         if (topInsetChanging) {
241             startInsetAnimation(child, viewState, duration, delay);
242         }
243 
244         // start dimmed animation
245         child.setDimmed(viewState.dimmed, mAnimationFilter.animateDimmed);
246 
247         // apply speed bump state
248         child.setBelowSpeedBump(viewState.belowSpeedBump);
249 
250         // start hiding sensitive animation
251         child.setHideSensitive(viewState.hideSensitive, mAnimationFilter.animateHideSensitive,
252                 delay, duration);
253 
254         // start dark animation
255         child.setDark(viewState.dark, mAnimationFilter.animateDark, delay);
256 
257         if (wasAdded) {
258             child.performAddAnimation(delay, mCurrentLength);
259         }
260         if (child instanceof SpeedBumpView) {
261             finalState.performSpeedBumpAnimation(i, (SpeedBumpView) child, viewState,
262                     delay + duration);
263         } else if (child instanceof ExpandableNotificationRow) {
264             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
265             row.startChildAnimation(finalState, this, child == mChildExpandingView, delay,
266                     duration);
267         }
268     }
269 
270     /**
271      * Start an animation to a new {@link ViewState}.
272      *
273      * @param child the child to start the animation on
274      * @param viewState the  {@link StackViewState} of the view to animate to
275      * @param delay a fixed delay
276      * @param duration the duration of the animation
277      */
startViewAnimations(View child, ViewState viewState, long delay, long duration)278     public void startViewAnimations(View child, ViewState viewState, long delay, long duration) {
279         boolean wasVisible = child.getVisibility() == View.VISIBLE;
280         final float alpha = viewState.alpha;
281         if (!wasVisible && alpha != 0 && !viewState.gone) {
282             child.setVisibility(View.VISIBLE);
283         }
284         boolean yTranslationChanging = child.getTranslationY() != viewState.yTranslation;
285         boolean zTranslationChanging = child.getTranslationZ() != viewState.zTranslation;
286         boolean scaleChanging = child.getScaleX() != viewState.scale;
287         float childAlpha = child.getVisibility() == View.INVISIBLE ? 0.0f : child.getAlpha();
288         boolean alphaChanging = viewState.alpha != childAlpha;
289         if (child instanceof ExpandableView) {
290             // We don't want views to change visibility when they are animating to GONE
291             alphaChanging &= !((ExpandableView) child).willBeGone();
292         }
293 
294         // start translationY animation
295         if (yTranslationChanging) {
296             startYTranslationAnimation(child, viewState, duration, delay);
297         }
298 
299         // start translationZ animation
300         if (zTranslationChanging) {
301             startZTranslationAnimation(child, viewState, duration, delay);
302         }
303 
304         // start scale animation
305         if (scaleChanging) {
306             startScaleAnimation(child, viewState, duration);
307         }
308 
309         // start alpha animation
310         if (alphaChanging && child.getTranslationX() == 0) {
311             startAlphaAnimation(child, viewState, duration, delay);
312         }
313     }
314 
calculateChildAnimationDelay(StackViewState viewState, StackScrollState finalState)315     private long calculateChildAnimationDelay(StackViewState viewState,
316             StackScrollState finalState) {
317         if (mAnimationFilter.hasDarkEvent) {
318             return calculateDelayDark(viewState);
319         }
320         if (mAnimationFilter.hasGoToFullShadeEvent) {
321             return calculateDelayGoToFullShade(viewState);
322         }
323         long minDelay = 0;
324         for (NotificationStackScrollLayout.AnimationEvent event : mNewEvents) {
325             long delayPerElement = ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING;
326             switch (event.animationType) {
327                 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD: {
328                     int ownIndex = viewState.notGoneIndex;
329                     int changingIndex = finalState
330                             .getViewStateForView(event.changingView).notGoneIndex;
331                     int difference = Math.abs(ownIndex - changingIndex);
332                     difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
333                             difference - 1));
334                     long delay = (DELAY_EFFECT_MAX_INDEX_DIFFERENCE - difference) * delayPerElement;
335                     minDelay = Math.max(delay, minDelay);
336                     break;
337                 }
338                 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT:
339                     delayPerElement = ANIMATION_DELAY_PER_ELEMENT_MANUAL;
340                 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE: {
341                     int ownIndex = viewState.notGoneIndex;
342                     boolean noNextView = event.viewAfterChangingView == null;
343                     View viewAfterChangingView = noNextView
344                             ? mHostLayout.getLastChildNotGone()
345                             : event.viewAfterChangingView;
346 
347                     int nextIndex = finalState
348                             .getViewStateForView(viewAfterChangingView).notGoneIndex;
349                     if (ownIndex >= nextIndex) {
350                         // we only have the view afterwards
351                         ownIndex++;
352                     }
353                     int difference = Math.abs(ownIndex - nextIndex);
354                     difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
355                             difference - 1));
356                     long delay = difference * delayPerElement;
357                     minDelay = Math.max(delay, minDelay);
358                     break;
359                 }
360                 default:
361                     break;
362             }
363         }
364         return minDelay;
365     }
366 
calculateDelayDark(StackViewState viewState)367     private long calculateDelayDark(StackViewState viewState) {
368         int referenceIndex;
369         if (mAnimationFilter.darkAnimationOriginIndex ==
370                 NotificationStackScrollLayout.AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_ABOVE) {
371             referenceIndex = 0;
372         } else if (mAnimationFilter.darkAnimationOriginIndex ==
373                 NotificationStackScrollLayout.AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_BELOW) {
374             referenceIndex = mHostLayout.getNotGoneChildCount() - 1;
375         } else {
376             referenceIndex = mAnimationFilter.darkAnimationOriginIndex;
377         }
378         return Math.abs(referenceIndex - viewState.notGoneIndex) * ANIMATION_DELAY_PER_ELEMENT_DARK;
379     }
380 
calculateDelayGoToFullShade(StackViewState viewState)381     private long calculateDelayGoToFullShade(StackViewState viewState) {
382         float index = viewState.notGoneIndex;
383         index = (float) Math.pow(index, 0.7f);
384         return (long) (index * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE);
385     }
386 
startHeightAnimation(final ExpandableView child, StackViewState viewState, long duration, long delay)387     private void startHeightAnimation(final ExpandableView child,
388             StackViewState viewState, long duration, long delay) {
389         Integer previousStartValue = getChildTag(child, TAG_START_HEIGHT);
390         Integer previousEndValue = getChildTag(child, TAG_END_HEIGHT);
391         int newEndValue = viewState.height;
392         if (previousEndValue != null && previousEndValue == newEndValue) {
393             return;
394         }
395         ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_HEIGHT);
396         if (!mAnimationFilter.animateHeight) {
397             // just a local update was performed
398             if (previousAnimator != null) {
399                 // we need to increase all animation keyframes of the previous animator by the
400                 // relative change to the end value
401                 PropertyValuesHolder[] values = previousAnimator.getValues();
402                 int relativeDiff = newEndValue - previousEndValue;
403                 int newStartValue = previousStartValue + relativeDiff;
404                 values[0].setIntValues(newStartValue, newEndValue);
405                 child.setTag(TAG_START_HEIGHT, newStartValue);
406                 child.setTag(TAG_END_HEIGHT, newEndValue);
407                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
408                 return;
409             } else {
410                 // no new animation needed, let's just apply the value
411                 child.setActualHeight(newEndValue, false);
412                 return;
413             }
414         }
415 
416         ValueAnimator animator = ValueAnimator.ofInt(child.getActualHeight(), newEndValue);
417         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
418             @Override
419             public void onAnimationUpdate(ValueAnimator animation) {
420                 child.setActualHeight((int) animation.getAnimatedValue(),
421                         false /* notifyListeners */);
422             }
423         });
424         animator.setInterpolator(mFastOutSlowInInterpolator);
425         long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
426         animator.setDuration(newDuration);
427         if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
428             animator.setStartDelay(delay);
429         }
430         animator.addListener(getGlobalAnimationFinishedListener());
431         // remove the tag when the animation is finished
432         animator.addListener(new AnimatorListenerAdapter() {
433             @Override
434             public void onAnimationEnd(Animator animation) {
435                 child.setTag(TAG_ANIMATOR_HEIGHT, null);
436                 child.setTag(TAG_START_HEIGHT, null);
437                 child.setTag(TAG_END_HEIGHT, null);
438             }
439         });
440         startAnimator(animator);
441         child.setTag(TAG_ANIMATOR_HEIGHT, animator);
442         child.setTag(TAG_START_HEIGHT, child.getActualHeight());
443         child.setTag(TAG_END_HEIGHT, newEndValue);
444     }
445 
startInsetAnimation(final ExpandableView child, StackViewState viewState, long duration, long delay)446     private void startInsetAnimation(final ExpandableView child,
447             StackViewState viewState, long duration, long delay) {
448         Integer previousStartValue = getChildTag(child, TAG_START_TOP_INSET);
449         Integer previousEndValue = getChildTag(child, TAG_END_TOP_INSET);
450         int newEndValue = viewState.clipTopAmount;
451         if (previousEndValue != null && previousEndValue == newEndValue) {
452             return;
453         }
454         ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TOP_INSET);
455         if (!mAnimationFilter.animateTopInset) {
456             // just a local update was performed
457             if (previousAnimator != null) {
458                 // we need to increase all animation keyframes of the previous animator by the
459                 // relative change to the end value
460                 PropertyValuesHolder[] values = previousAnimator.getValues();
461                 int relativeDiff = newEndValue - previousEndValue;
462                 int newStartValue = previousStartValue + relativeDiff;
463                 values[0].setIntValues(newStartValue, newEndValue);
464                 child.setTag(TAG_START_TOP_INSET, newStartValue);
465                 child.setTag(TAG_END_TOP_INSET, newEndValue);
466                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
467                 return;
468             } else {
469                 // no new animation needed, let's just apply the value
470                 child.setClipTopAmount(newEndValue);
471                 return;
472             }
473         }
474 
475         ValueAnimator animator = ValueAnimator.ofInt(child.getClipTopAmount(), newEndValue);
476         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
477             @Override
478             public void onAnimationUpdate(ValueAnimator animation) {
479                 child.setClipTopAmount((int) animation.getAnimatedValue());
480             }
481         });
482         animator.setInterpolator(mFastOutSlowInInterpolator);
483         long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
484         animator.setDuration(newDuration);
485         if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
486             animator.setStartDelay(delay);
487         }
488         animator.addListener(getGlobalAnimationFinishedListener());
489         // remove the tag when the animation is finished
490         animator.addListener(new AnimatorListenerAdapter() {
491             @Override
492             public void onAnimationEnd(Animator animation) {
493                 child.setTag(TAG_ANIMATOR_TOP_INSET, null);
494                 child.setTag(TAG_START_TOP_INSET, null);
495                 child.setTag(TAG_END_TOP_INSET, null);
496             }
497         });
498         startAnimator(animator);
499         child.setTag(TAG_ANIMATOR_TOP_INSET, animator);
500         child.setTag(TAG_START_TOP_INSET, child.getClipTopAmount());
501         child.setTag(TAG_END_TOP_INSET, newEndValue);
502     }
503 
startAlphaAnimation(final View child, final ViewState viewState, long duration, long delay)504     private void startAlphaAnimation(final View child,
505             final ViewState viewState, long duration, long delay) {
506         Float previousStartValue = getChildTag(child,TAG_START_ALPHA);
507         Float previousEndValue = getChildTag(child,TAG_END_ALPHA);
508         final float newEndValue = viewState.alpha;
509         if (previousEndValue != null && previousEndValue == newEndValue) {
510             return;
511         }
512         ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_ALPHA);
513         if (!mAnimationFilter.animateAlpha) {
514             // just a local update was performed
515             if (previousAnimator != null) {
516                 // we need to increase all animation keyframes of the previous animator by the
517                 // relative change to the end value
518                 PropertyValuesHolder[] values = previousAnimator.getValues();
519                 float relativeDiff = newEndValue - previousEndValue;
520                 float newStartValue = previousStartValue + relativeDiff;
521                 values[0].setFloatValues(newStartValue, newEndValue);
522                 child.setTag(TAG_START_ALPHA, newStartValue);
523                 child.setTag(TAG_END_ALPHA, newEndValue);
524                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
525                 return;
526             } else {
527                 // no new animation needed, let's just apply the value
528                 child.setAlpha(newEndValue);
529                 if (newEndValue == 0) {
530                     child.setVisibility(View.INVISIBLE);
531                 }
532             }
533         }
534 
535         ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.ALPHA,
536                 child.getAlpha(), newEndValue);
537         animator.setInterpolator(mFastOutSlowInInterpolator);
538         // Handle layer type
539         child.setLayerType(View.LAYER_TYPE_HARDWARE, null);
540         animator.addListener(new AnimatorListenerAdapter() {
541             public boolean mWasCancelled;
542 
543             @Override
544             public void onAnimationEnd(Animator animation) {
545                 child.setLayerType(View.LAYER_TYPE_NONE, null);
546                 if (newEndValue == 0 && !mWasCancelled) {
547                     child.setVisibility(View.INVISIBLE);
548                 }
549                 // remove the tag when the animation is finished
550                 child.setTag(TAG_ANIMATOR_ALPHA, null);
551                 child.setTag(TAG_START_ALPHA, null);
552                 child.setTag(TAG_END_ALPHA, null);
553             }
554 
555             @Override
556             public void onAnimationCancel(Animator animation) {
557                 mWasCancelled = true;
558             }
559 
560             @Override
561             public void onAnimationStart(Animator animation) {
562                 mWasCancelled = false;
563             }
564         });
565         long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
566         animator.setDuration(newDuration);
567         if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
568             animator.setStartDelay(delay);
569         }
570         animator.addListener(getGlobalAnimationFinishedListener());
571 
572         startAnimator(animator);
573         child.setTag(TAG_ANIMATOR_ALPHA, animator);
574         child.setTag(TAG_START_ALPHA, child.getAlpha());
575         child.setTag(TAG_END_ALPHA, newEndValue);
576     }
577 
startZTranslationAnimation(final View child, final ViewState viewState, long duration, long delay)578     private void startZTranslationAnimation(final View child,
579             final ViewState viewState, long duration, long delay) {
580         Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Z);
581         Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Z);
582         float newEndValue = viewState.zTranslation;
583         if (previousEndValue != null && previousEndValue == newEndValue) {
584             return;
585         }
586         ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Z);
587         if (!mAnimationFilter.animateZ) {
588             // just a local update was performed
589             if (previousAnimator != null) {
590                 // we need to increase all animation keyframes of the previous animator by the
591                 // relative change to the end value
592                 PropertyValuesHolder[] values = previousAnimator.getValues();
593                 float relativeDiff = newEndValue - previousEndValue;
594                 float newStartValue = previousStartValue + relativeDiff;
595                 values[0].setFloatValues(newStartValue, newEndValue);
596                 child.setTag(TAG_START_TRANSLATION_Z, newStartValue);
597                 child.setTag(TAG_END_TRANSLATION_Z, newEndValue);
598                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
599                 return;
600             } else {
601                 // no new animation needed, let's just apply the value
602                 child.setTranslationZ(newEndValue);
603             }
604         }
605 
606         ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Z,
607                 child.getTranslationZ(), newEndValue);
608         animator.setInterpolator(mFastOutSlowInInterpolator);
609         long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
610         animator.setDuration(newDuration);
611         if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
612             animator.setStartDelay(delay);
613         }
614         animator.addListener(getGlobalAnimationFinishedListener());
615         // remove the tag when the animation is finished
616         animator.addListener(new AnimatorListenerAdapter() {
617             @Override
618             public void onAnimationEnd(Animator animation) {
619                 child.setTag(TAG_ANIMATOR_TRANSLATION_Z, null);
620                 child.setTag(TAG_START_TRANSLATION_Z, null);
621                 child.setTag(TAG_END_TRANSLATION_Z, null);
622             }
623         });
624         startAnimator(animator);
625         child.setTag(TAG_ANIMATOR_TRANSLATION_Z, animator);
626         child.setTag(TAG_START_TRANSLATION_Z, child.getTranslationZ());
627         child.setTag(TAG_END_TRANSLATION_Z, newEndValue);
628     }
629 
startYTranslationAnimation(final View child, ViewState viewState, long duration, long delay)630     private void startYTranslationAnimation(final View child,
631             ViewState viewState, long duration, long delay) {
632         Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Y);
633         Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Y);
634         float newEndValue = viewState.yTranslation;
635         if (previousEndValue != null && previousEndValue == newEndValue) {
636             return;
637         }
638         ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Y);
639         if (!mAnimationFilter.animateY) {
640             // just a local update was performed
641             if (previousAnimator != null) {
642                 // we need to increase all animation keyframes of the previous animator by the
643                 // relative change to the end value
644                 PropertyValuesHolder[] values = previousAnimator.getValues();
645                 float relativeDiff = newEndValue - previousEndValue;
646                 float newStartValue = previousStartValue + relativeDiff;
647                 values[0].setFloatValues(newStartValue, newEndValue);
648                 child.setTag(TAG_START_TRANSLATION_Y, newStartValue);
649                 child.setTag(TAG_END_TRANSLATION_Y, newEndValue);
650                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
651                 return;
652             } else {
653                 // no new animation needed, let's just apply the value
654                 child.setTranslationY(newEndValue);
655                 return;
656             }
657         }
658 
659         ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Y,
660                 child.getTranslationY(), newEndValue);
661         Interpolator interpolator = mHeadsUpAppearChildren.contains(child) ?
662                 mHeadsUpAppearInterpolator :mFastOutSlowInInterpolator;
663         animator.setInterpolator(interpolator);
664         long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
665         animator.setDuration(newDuration);
666         if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
667             animator.setStartDelay(delay);
668         }
669         animator.addListener(getGlobalAnimationFinishedListener());
670         // remove the tag when the animation is finished
671         animator.addListener(new AnimatorListenerAdapter() {
672             @Override
673             public void onAnimationEnd(Animator animation) {
674                 HeadsUpManager.setIsClickedNotification(child, false);
675                 child.setTag(TAG_ANIMATOR_TRANSLATION_Y, null);
676                 child.setTag(TAG_START_TRANSLATION_Y, null);
677                 child.setTag(TAG_END_TRANSLATION_Y, null);
678             }
679         });
680         startAnimator(animator);
681         child.setTag(TAG_ANIMATOR_TRANSLATION_Y, animator);
682         child.setTag(TAG_START_TRANSLATION_Y, child.getTranslationY());
683         child.setTag(TAG_END_TRANSLATION_Y, newEndValue);
684     }
685 
startScaleAnimation(final View child, ViewState viewState, long duration)686     private void startScaleAnimation(final View child,
687             ViewState viewState, long duration) {
688         Float previousStartValue = getChildTag(child, TAG_START_SCALE);
689         Float previousEndValue = getChildTag(child, TAG_END_SCALE);
690         float newEndValue = viewState.scale;
691         if (previousEndValue != null && previousEndValue == newEndValue) {
692             return;
693         }
694         ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_SCALE);
695         if (!mAnimationFilter.animateScale) {
696             // just a local update was performed
697             if (previousAnimator != null) {
698                 // we need to increase all animation keyframes of the previous animator by the
699                 // relative change to the end value
700                 PropertyValuesHolder[] values = previousAnimator.getValues();
701                 float relativeDiff = newEndValue - previousEndValue;
702                 float newStartValue = previousStartValue + relativeDiff;
703                 values[0].setFloatValues(newStartValue, newEndValue);
704                 values[1].setFloatValues(newStartValue, newEndValue);
705                 child.setTag(TAG_START_SCALE, newStartValue);
706                 child.setTag(TAG_END_SCALE, newEndValue);
707                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
708                 return;
709             } else {
710                 // no new animation needed, let's just apply the value
711                 child.setScaleX(newEndValue);
712                 child.setScaleY(newEndValue);
713             }
714         }
715 
716         PropertyValuesHolder holderX =
717                 PropertyValuesHolder.ofFloat(View.SCALE_X, child.getScaleX(), newEndValue);
718         PropertyValuesHolder holderY =
719                 PropertyValuesHolder.ofFloat(View.SCALE_Y, child.getScaleY(), newEndValue);
720         ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(child, holderX, holderY);
721         animator.setInterpolator(mFastOutSlowInInterpolator);
722         long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
723         animator.setDuration(newDuration);
724         animator.addListener(getGlobalAnimationFinishedListener());
725         // remove the tag when the animation is finished
726         animator.addListener(new AnimatorListenerAdapter() {
727             @Override
728             public void onAnimationEnd(Animator animation) {
729                 child.setTag(TAG_ANIMATOR_SCALE, null);
730                 child.setTag(TAG_START_SCALE, null);
731                 child.setTag(TAG_END_SCALE, null);
732             }
733         });
734         startAnimator(animator);
735         child.setTag(TAG_ANIMATOR_SCALE, animator);
736         child.setTag(TAG_START_SCALE, child.getScaleX());
737         child.setTag(TAG_END_SCALE, newEndValue);
738     }
739 
startAnimator(ValueAnimator animator)740     private void startAnimator(ValueAnimator animator) {
741         mAnimatorSet.add(animator);
742         animator.start();
743     }
744 
745     /**
746      * @return an adapter which ensures that onAnimationFinished is called once no animation is
747      *         running anymore
748      */
getGlobalAnimationFinishedListener()749     private AnimatorListenerAdapter getGlobalAnimationFinishedListener() {
750         if (!mAnimationListenerPool.empty()) {
751             return mAnimationListenerPool.pop();
752         }
753 
754         // We need to create a new one, no reusable ones found
755         return new AnimatorListenerAdapter() {
756             private boolean mWasCancelled;
757 
758             @Override
759             public void onAnimationEnd(Animator animation) {
760                 mAnimatorSet.remove(animation);
761                 if (mAnimatorSet.isEmpty() && !mWasCancelled) {
762                     onAnimationFinished();
763                 }
764                 mAnimationListenerPool.push(this);
765             }
766 
767             @Override
768             public void onAnimationCancel(Animator animation) {
769                 mWasCancelled = true;
770             }
771 
772             @Override
773             public void onAnimationStart(Animator animation) {
774                 mWasCancelled = false;
775             }
776         };
777     }
778 
getChildTag(View child, int tag)779     public static <T> T getChildTag(View child, int tag) {
780         return (T) child.getTag(tag);
781     }
782 
783     /**
784      * Cancel the previous animator and get the duration of the new animation.
785      *
786      * @param duration the new duration
787      * @param previousAnimator the animator which was running before
788      * @return the new duration
789      */
cancelAnimatorAndGetNewDuration(long duration, ValueAnimator previousAnimator)790     private long cancelAnimatorAndGetNewDuration(long duration, ValueAnimator previousAnimator) {
791         long newDuration = duration;
792         if (previousAnimator != null) {
793             // We take either the desired length of the new animation or the remaining time of
794             // the previous animator, whichever is longer.
795             newDuration = Math.max(previousAnimator.getDuration()
796                     - previousAnimator.getCurrentPlayTime(), newDuration);
797             previousAnimator.cancel();
798         }
799         return newDuration;
800     }
801 
onAnimationFinished()802     private void onAnimationFinished() {
803         mHostLayout.onChildAnimationFinished();
804         for (View v : mChildrenToClearFromOverlay) {
805             mHostLayout.getOverlay().remove(v);
806         }
807         mChildrenToClearFromOverlay.clear();
808     }
809 
810     /**
811      * Process the animationEvents for a new animation
812      *
813      * @param animationEvents the animation events for the animation to perform
814      * @param finalState the final state to animate to
815      */
processAnimationEvents( ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents, StackScrollState finalState)816     private void processAnimationEvents(
817             ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents,
818             StackScrollState finalState) {
819         for (NotificationStackScrollLayout.AnimationEvent event : animationEvents) {
820             final ExpandableView changingView = (ExpandableView) event.changingView;
821             if (event.animationType ==
822                     NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD) {
823 
824                 // This item is added, initialize it's properties.
825                 StackViewState viewState = finalState
826                         .getViewStateForView(changingView);
827                 if (viewState == null) {
828                     // The position for this child was never generated, let's continue.
829                     continue;
830                 }
831                 if (changingView.getVisibility() == View.GONE) {
832                     // The view was set to gone but the state never removed
833                     finalState.removeViewStateForView(changingView);
834                     continue;
835                 }
836                 finalState.applyState(changingView, viewState);
837                 mNewAddChildren.add(changingView);
838 
839             } else if (event.animationType ==
840                     NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE) {
841                 if (changingView.getVisibility() == View.GONE) {
842                     mHostLayout.getOverlay().remove(changingView);
843                     continue;
844                 }
845 
846                 // Find the amount to translate up. This is needed in order to understand the
847                 // direction of the remove animation (either downwards or upwards)
848                 StackViewState viewState = finalState
849                         .getViewStateForView(event.viewAfterChangingView);
850                 int actualHeight = changingView.getActualHeight();
851                 // upwards by default
852                 float translationDirection = -1.0f;
853                 if (viewState != null) {
854                     // there was a view after this one, Approximate the distance the next child
855                     // travelled
856                     translationDirection = ((viewState.yTranslation
857                             - (changingView.getTranslationY() + actualHeight / 2.0f)) * 2 /
858                             actualHeight);
859                     translationDirection = Math.max(Math.min(translationDirection, 1.0f),-1.0f);
860 
861                 }
862                 changingView.performRemoveAnimation(ANIMATION_DURATION_APPEAR_DISAPPEAR,
863                         translationDirection, new Runnable() {
864                     @Override
865                     public void run() {
866                         // remove the temporary overlay
867                         mHostLayout.getOverlay().remove(changingView);
868                     }
869                 });
870             } else if (event.animationType ==
871                 NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT) {
872                 // A race condition can trigger the view to be added to the overlay even though
873                 // it is swiped out. So let's remove it
874                 mHostLayout.getOverlay().remove(changingView);
875             } else if (event.animationType == NotificationStackScrollLayout
876                     .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) {
877                 ExpandableNotificationRow row = (ExpandableNotificationRow) event.changingView;
878                 row.prepareExpansionChanged(finalState);
879                 mChildExpandingView = row;
880             } else if (event.animationType == NotificationStackScrollLayout
881                     .AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) {
882                 // This item is added, initialize it's properties.
883                 StackViewState viewState = finalState.getViewStateForView(changingView);
884                 mTmpState.copyFrom(viewState);
885                 if (event.headsUpFromBottom) {
886                     mTmpState.yTranslation = mHeadsUpAppearHeightBottom;
887                 } else {
888                     mTmpState.yTranslation = -mTmpState.height;
889                 }
890                 mHeadsUpAppearChildren.add(changingView);
891                 finalState.applyState(changingView, mTmpState);
892             } else if (event.animationType == NotificationStackScrollLayout
893                     .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR) {
894                 mHeadsUpDisappearChildren.add(changingView);
895                 if (mHostLayout.indexOfChild(changingView) == -1) {
896                     // This notification was actually removed, so we need to add it to the overlay
897                     mHostLayout.getOverlay().add(changingView);
898                     mTmpState.initFrom(changingView);
899                     mTmpState.yTranslation = -changingView.getActualHeight();
900                     // We temporarily enable Y animations, the real filter will be combined
901                     // afterwards anyway
902                     mAnimationFilter.animateY = true;
903                     startViewAnimations(changingView, mTmpState, 0,
904                             ANIMATION_DURATION_HEADS_UP_DISAPPEAR);
905                     mChildrenToClearFromOverlay.add(changingView);
906                 }
907             }
908             mNewEvents.add(event);
909         }
910     }
911 
animateOverScrollToAmount(float targetAmount, final boolean onTop, final boolean isRubberbanded)912     public void animateOverScrollToAmount(float targetAmount, final boolean onTop,
913             final boolean isRubberbanded) {
914         final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop);
915         if (targetAmount == startOverScrollAmount) {
916             return;
917         }
918         cancelOverScrollAnimators(onTop);
919         ValueAnimator overScrollAnimator = ValueAnimator.ofFloat(startOverScrollAmount,
920                 targetAmount);
921         overScrollAnimator.setDuration(ANIMATION_DURATION_STANDARD);
922         overScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
923             @Override
924             public void onAnimationUpdate(ValueAnimator animation) {
925                 float currentOverScroll = (float) animation.getAnimatedValue();
926                 mHostLayout.setOverScrollAmount(
927                         currentOverScroll, onTop, false /* animate */, false /* cancelAnimators */,
928                         isRubberbanded);
929             }
930         });
931         overScrollAnimator.setInterpolator(mFastOutSlowInInterpolator);
932         overScrollAnimator.addListener(new AnimatorListenerAdapter() {
933             @Override
934             public void onAnimationEnd(Animator animation) {
935                 if (onTop) {
936                     mTopOverScrollAnimator = null;
937                 } else {
938                     mBottomOverScrollAnimator = null;
939                 }
940             }
941         });
942         overScrollAnimator.start();
943         if (onTop) {
944             mTopOverScrollAnimator = overScrollAnimator;
945         } else {
946             mBottomOverScrollAnimator = overScrollAnimator;
947         }
948     }
949 
cancelOverScrollAnimators(boolean onTop)950     public void cancelOverScrollAnimators(boolean onTop) {
951         ValueAnimator currentAnimator = onTop ? mTopOverScrollAnimator : mBottomOverScrollAnimator;
952         if (currentAnimator != null) {
953             currentAnimator.cancel();
954         }
955     }
956 
957     /**
958      * Get the end value of the height animation running on a view or the actualHeight
959      * if no animation is running.
960      */
getFinalActualHeight(ExpandableView view)961     public static int getFinalActualHeight(ExpandableView view) {
962         if (view == null) {
963             return 0;
964         }
965         ValueAnimator heightAnimator = getChildTag(view, TAG_ANIMATOR_HEIGHT);
966         if (heightAnimator == null) {
967             return view.getActualHeight();
968         } else {
969             return getChildTag(view, TAG_END_HEIGHT);
970         }
971     }
972 
setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom)973     public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) {
974         mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom;
975     }
976 
setShadeExpanded(boolean shadeExpanded)977     public void setShadeExpanded(boolean shadeExpanded) {
978         mShadeExpanded = shadeExpanded;
979     }
980 }
981