StackStateAnimator.java revision 708a6c120da6750d281195ef15a240a5627efed4
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
17package com.android.systemui.statusbar.stack;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.animation.PropertyValuesHolder;
23import android.animation.ValueAnimator;
24import android.view.View;
25import android.view.animation.AnimationUtils;
26import android.view.animation.Interpolator;
27
28import com.android.systemui.R;
29import com.android.systemui.statusbar.ExpandableView;
30
31import java.util.ArrayList;
32import java.util.HashSet;
33import java.util.Set;
34import java.util.Stack;
35
36/**
37 * An stack state animator which handles animations to new StackScrollStates
38 */
39public class StackStateAnimator {
40
41    public static final int ANIMATION_DURATION_STANDARD = 360;
42    public static final int ANIMATION_DURATION_APPEAR_DISAPPEAR = 464;
43    public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220;
44    public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80;
45    public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32;
46    private static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2;
47
48    private static final int TAG_ANIMATOR_TRANSLATION_Y = R.id.translation_y_animator_tag;
49    private static final int TAG_ANIMATOR_TRANSLATION_Z = R.id.translation_z_animator_tag;
50    private static final int TAG_ANIMATOR_SCALE = R.id.scale_animator_tag;
51    private static final int TAG_ANIMATOR_ALPHA = R.id.alpha_animator_tag;
52    private static final int TAG_ANIMATOR_HEIGHT = R.id.height_animator_tag;
53    private static final int TAG_ANIMATOR_TOP_INSET = R.id.top_inset_animator_tag;
54    private static final int TAG_END_TRANSLATION_Y = R.id.translation_y_animator_end_value_tag;
55    private static final int TAG_END_TRANSLATION_Z = R.id.translation_z_animator_end_value_tag;
56    private static final int TAG_END_SCALE = R.id.scale_animator_end_value_tag;
57    private static final int TAG_END_ALPHA = R.id.alpha_animator_end_value_tag;
58    private static final int TAG_END_HEIGHT = R.id.height_animator_end_value_tag;
59    private static final int TAG_END_TOP_INSET = R.id.top_inset_animator_end_value_tag;
60    private static final int TAG_START_TRANSLATION_Y = R.id.translation_y_animator_start_value_tag;
61    private static final int TAG_START_TRANSLATION_Z = R.id.translation_z_animator_start_value_tag;
62    private static final int TAG_START_SCALE = R.id.scale_animator_start_value_tag;
63    private static final int TAG_START_ALPHA = R.id.alpha_animator_start_value_tag;
64    private static final int TAG_START_HEIGHT = R.id.height_animator_start_value_tag;
65    private static final int TAG_START_TOP_INSET = R.id.top_inset_animator_start_value_tag;
66
67    private final Interpolator mFastOutSlowInInterpolator;
68    public NotificationStackScrollLayout mHostLayout;
69    private ArrayList<NotificationStackScrollLayout.AnimationEvent> mNewEvents =
70            new ArrayList<>();
71    private ArrayList<View> mNewAddChildren = new ArrayList<>();
72    private Set<Animator> mAnimatorSet = new HashSet<Animator>();
73    private Stack<AnimatorListenerAdapter> mAnimationListenerPool
74            = new Stack<AnimatorListenerAdapter>();
75    private AnimationFilter mAnimationFilter = new AnimationFilter();
76    private long mCurrentLength;
77
78    private ValueAnimator mTopOverScrollAnimator;
79    private ValueAnimator mBottomOverScrollAnimator;
80
81    public StackStateAnimator(NotificationStackScrollLayout hostLayout) {
82        mHostLayout = hostLayout;
83        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(hostLayout.getContext(),
84                android.R.interpolator.fast_out_slow_in);
85    }
86
87    public boolean isRunning() {
88        return !mAnimatorSet.isEmpty();
89    }
90
91    public void startAnimationForEvents(
92            ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents,
93            StackScrollState finalState) {
94
95        processAnimationEvents(mAnimationEvents, finalState);
96
97        int childCount = mHostLayout.getChildCount();
98        mAnimationFilter.applyCombination(mNewEvents);
99        mCurrentLength = NotificationStackScrollLayout.AnimationEvent.combineLength(mNewEvents);
100        for (int i = 0; i < childCount; i++) {
101            final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
102
103            StackScrollState.ViewState viewState = finalState.getViewStateForView(child);
104            if (viewState == null || child.getVisibility() == View.GONE) {
105                continue;
106            }
107
108            child.setClipBounds(null);
109            startAnimations(child, viewState, finalState);
110        }
111        if (!isRunning()) {
112            // no child has preformed any animation, lets finish
113            onAnimationFinished();
114        }
115        mNewEvents.clear();
116        mNewAddChildren.clear();
117    }
118
119    /**
120     * Start an animation to the given viewState
121     */
122    private void startAnimations(final ExpandableView child, StackScrollState.ViewState viewState,
123            StackScrollState finalState) {
124        int childVisibility = child.getVisibility();
125        boolean wasVisible = childVisibility == View.VISIBLE;
126        final float alpha = viewState.alpha;
127        if (!wasVisible && alpha != 0 && !viewState.gone) {
128            child.setVisibility(View.VISIBLE);
129        }
130
131        boolean yTranslationChanging = child.getTranslationY() != viewState.yTranslation;
132        boolean zTranslationChanging = child.getTranslationZ() != viewState.zTranslation;
133        boolean scaleChanging = child.getScaleX() != viewState.scale;
134        boolean alphaChanging = alpha != child.getAlpha();
135        boolean heightChanging = viewState.height != child.getActualHeight();
136        boolean topInsetChanging = viewState.clipTopAmount != child.getClipTopAmount();
137        boolean wasAdded = mNewAddChildren.contains(child);
138        boolean hasDelays = mAnimationFilter.hasDelays;
139        boolean isDelayRelevant = yTranslationChanging || zTranslationChanging || scaleChanging ||
140                alphaChanging || heightChanging || topInsetChanging;
141        long delay = 0;
142        if (hasDelays && isDelayRelevant || wasAdded) {
143            delay = calculateChildAnimationDelay(viewState, finalState);
144        }
145
146        // start translationY animation
147        if (yTranslationChanging) {
148            startYTranslationAnimation(child, viewState, delay);
149        }
150
151        // start translationZ animation
152        if (zTranslationChanging) {
153            startZTranslationAnimation(child, viewState, delay);
154        }
155
156        // start scale animation
157        if (scaleChanging) {
158            startScaleAnimation(child, viewState);
159        }
160
161        // start alpha animation
162        if (alphaChanging) {
163            startAlphaAnimation(child, viewState, delay);
164        }
165
166        // start height animation
167        if (heightChanging) {
168            startHeightAnimation(child, viewState, delay);
169        }
170
171        // start top inset animation
172        if (topInsetChanging) {
173            startInsetAnimation(child, viewState, delay);
174        }
175
176        // start dimmed animation
177        child.setDimmed(viewState.dimmed, mAnimationFilter.animateDimmed);
178
179        if (wasAdded) {
180            child.performAddAnimation(delay);
181        }
182    }
183
184    private long calculateChildAnimationDelay(StackScrollState.ViewState viewState,
185            StackScrollState finalState) {
186        long minDelay = 0;
187        for (NotificationStackScrollLayout.AnimationEvent event : mNewEvents) {
188            long delayPerElement = ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING;
189            switch (event.animationType) {
190                case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD: {
191                    int ownIndex = viewState.notGoneIndex;
192                    int changingIndex = finalState
193                            .getViewStateForView(event.changingView).notGoneIndex;
194                    int difference = Math.abs(ownIndex - changingIndex);
195                    difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
196                            difference - 1));
197                    long delay = (DELAY_EFFECT_MAX_INDEX_DIFFERENCE - difference) * delayPerElement;
198                    minDelay = Math.max(delay, minDelay);
199                    break;
200                }
201                case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT:
202                    delayPerElement = ANIMATION_DELAY_PER_ELEMENT_MANUAL;
203                case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE: {
204                    int ownIndex = viewState.notGoneIndex;
205                    boolean noNextView = event.viewAfterChangingView == null;
206                    View viewAfterChangingView = noNextView
207                            ? mHostLayout.getLastChildNotGone()
208                            : event.viewAfterChangingView;
209
210                    int nextIndex = finalState
211                            .getViewStateForView(viewAfterChangingView).notGoneIndex;
212                    if (ownIndex >= nextIndex) {
213                        // we only have the view afterwards
214                        ownIndex++;
215                    }
216                    int difference = Math.abs(ownIndex - nextIndex);
217                    difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
218                            difference - 1));
219                    long delay = difference * delayPerElement;
220                    minDelay = Math.max(delay, minDelay);
221                    break;
222                }
223                default:
224                    break;
225            }
226        }
227        return minDelay;
228    }
229
230    private void startHeightAnimation(final ExpandableView child,
231            StackScrollState.ViewState viewState, long delay) {
232        Integer previousStartValue = getChildTag(child, TAG_START_HEIGHT);
233        Integer previousEndValue = getChildTag(child, TAG_END_HEIGHT);
234        int newEndValue = viewState.height;
235        if (previousEndValue != null && previousEndValue == newEndValue) {
236            return;
237        }
238        ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_HEIGHT);
239        if (!mAnimationFilter.animateHeight) {
240            // just a local update was performed
241            if (previousAnimator != null) {
242                // we need to increase all animation keyframes of the previous animator by the
243                // relative change to the end value
244                PropertyValuesHolder[] values = previousAnimator.getValues();
245                int relativeDiff = newEndValue - previousEndValue;
246                int newStartValue = previousStartValue + relativeDiff;
247                values[0].setIntValues(newStartValue, newEndValue);
248                child.setTag(TAG_START_HEIGHT, newStartValue);
249                child.setTag(TAG_END_HEIGHT, newEndValue);
250                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
251                return;
252            } else {
253                // no new animation needed, let's just apply the value
254                child.setActualHeight(newEndValue, false);
255                return;
256            }
257        }
258
259        ValueAnimator animator = ValueAnimator.ofInt(child.getActualHeight(), newEndValue);
260        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
261            @Override
262            public void onAnimationUpdate(ValueAnimator animation) {
263                child.setActualHeight((int) animation.getAnimatedValue(),
264                        false /* notifyListeners */);
265            }
266        });
267        animator.setInterpolator(mFastOutSlowInInterpolator);
268        long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator);
269        animator.setDuration(newDuration);
270        if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
271            animator.setStartDelay(delay);
272        }
273        animator.addListener(getGlobalAnimationFinishedListener());
274        // remove the tag when the animation is finished
275        animator.addListener(new AnimatorListenerAdapter() {
276            @Override
277            public void onAnimationEnd(Animator animation) {
278                child.setTag(TAG_ANIMATOR_HEIGHT, null);
279                child.setTag(TAG_START_HEIGHT, null);
280                child.setTag(TAG_END_HEIGHT, null);
281            }
282        });
283        startAnimator(animator);
284        child.setTag(TAG_ANIMATOR_HEIGHT, animator);
285        child.setTag(TAG_START_HEIGHT, child.getActualHeight());
286        child.setTag(TAG_END_HEIGHT, newEndValue);
287    }
288
289    private void startInsetAnimation(final ExpandableView child,
290            StackScrollState.ViewState viewState, long delay) {
291        Integer previousStartValue = getChildTag(child, TAG_START_TOP_INSET);
292        Integer previousEndValue = getChildTag(child, TAG_END_TOP_INSET);
293        int newEndValue = viewState.clipTopAmount;
294        if (previousEndValue != null && previousEndValue == newEndValue) {
295            return;
296        }
297        ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TOP_INSET);
298        if (!mAnimationFilter.animateTopInset) {
299            // just a local update was performed
300            if (previousAnimator != null) {
301                // we need to increase all animation keyframes of the previous animator by the
302                // relative change to the end value
303                PropertyValuesHolder[] values = previousAnimator.getValues();
304                int relativeDiff = newEndValue - previousEndValue;
305                int newStartValue = previousStartValue + relativeDiff;
306                values[0].setIntValues(newStartValue, newEndValue);
307                child.setTag(TAG_START_TOP_INSET, newStartValue);
308                child.setTag(TAG_END_TOP_INSET, newEndValue);
309                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
310                return;
311            } else {
312                // no new animation needed, let's just apply the value
313                child.setClipTopAmount(newEndValue);
314                return;
315            }
316        }
317
318        ValueAnimator animator = ValueAnimator.ofInt(child.getClipTopAmount(), newEndValue);
319        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
320            @Override
321            public void onAnimationUpdate(ValueAnimator animation) {
322                child.setClipTopAmount((int) animation.getAnimatedValue());
323            }
324        });
325        animator.setInterpolator(mFastOutSlowInInterpolator);
326        long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator);
327        animator.setDuration(newDuration);
328        if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
329            animator.setStartDelay(delay);
330        }
331        animator.addListener(getGlobalAnimationFinishedListener());
332        // remove the tag when the animation is finished
333        animator.addListener(new AnimatorListenerAdapter() {
334            @Override
335            public void onAnimationEnd(Animator animation) {
336                child.setTag(TAG_ANIMATOR_TOP_INSET, null);
337                child.setTag(TAG_START_TOP_INSET, null);
338                child.setTag(TAG_END_TOP_INSET, null);
339            }
340        });
341        startAnimator(animator);
342        child.setTag(TAG_ANIMATOR_TOP_INSET, animator);
343        child.setTag(TAG_START_TOP_INSET, child.getClipTopAmount());
344        child.setTag(TAG_END_TOP_INSET, newEndValue);
345    }
346
347    private void startAlphaAnimation(final ExpandableView child,
348            final StackScrollState.ViewState viewState, long delay) {
349        Float previousStartValue = getChildTag(child,TAG_START_ALPHA);
350        Float previousEndValue = getChildTag(child,TAG_END_ALPHA);
351        final float newEndValue = viewState.alpha;
352        if (previousEndValue != null && previousEndValue == newEndValue) {
353            return;
354        }
355        ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_ALPHA);
356        if (!mAnimationFilter.animateAlpha) {
357            // just a local update was performed
358            if (previousAnimator != null) {
359                // we need to increase all animation keyframes of the previous animator by the
360                // relative change to the end value
361                PropertyValuesHolder[] values = previousAnimator.getValues();
362                float relativeDiff = newEndValue - previousEndValue;
363                float newStartValue = previousStartValue + relativeDiff;
364                values[0].setFloatValues(newStartValue, newEndValue);
365                child.setTag(TAG_START_ALPHA, newStartValue);
366                child.setTag(TAG_END_ALPHA, newEndValue);
367                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
368                return;
369            } else {
370                // no new animation needed, let's just apply the value
371                child.setAlpha(newEndValue);
372                if (newEndValue == 0) {
373                    child.setVisibility(View.INVISIBLE);
374                }
375            }
376        }
377
378        ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.ALPHA,
379                child.getAlpha(), newEndValue);
380        animator.setInterpolator(mFastOutSlowInInterpolator);
381        // Handle layer type
382        child.setLayerType(View.LAYER_TYPE_HARDWARE, null);
383        animator.addListener(new AnimatorListenerAdapter() {
384            public boolean mWasCancelled;
385
386            @Override
387            public void onAnimationEnd(Animator animation) {
388                child.setLayerType(View.LAYER_TYPE_NONE, null);
389                if (newEndValue == 0 && !mWasCancelled) {
390                    child.setVisibility(View.INVISIBLE);
391                }
392                child.setTag(TAG_ANIMATOR_ALPHA, null);
393                child.setTag(TAG_START_ALPHA, null);
394                child.setTag(TAG_END_ALPHA, null);
395            }
396
397            @Override
398            public void onAnimationCancel(Animator animation) {
399                mWasCancelled = true;
400            }
401
402            @Override
403            public void onAnimationStart(Animator animation) {
404                mWasCancelled = false;
405            }
406        });
407        long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator);
408        animator.setDuration(newDuration);
409        if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
410            animator.setStartDelay(delay);
411        }
412        animator.addListener(getGlobalAnimationFinishedListener());
413        // remove the tag when the animation is finished
414        animator.addListener(new AnimatorListenerAdapter() {
415            @Override
416            public void onAnimationEnd(Animator animation) {
417
418            }
419        });
420        startAnimator(animator);
421        child.setTag(TAG_ANIMATOR_ALPHA, animator);
422        child.setTag(TAG_START_ALPHA, child.getAlpha());
423        child.setTag(TAG_END_ALPHA, newEndValue);
424    }
425
426    private void startZTranslationAnimation(final ExpandableView child,
427            final StackScrollState.ViewState viewState, long delay) {
428        Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Z);
429        Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Z);
430        float newEndValue = viewState.zTranslation;
431        if (previousEndValue != null && previousEndValue == newEndValue) {
432            return;
433        }
434        ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Z);
435        if (!mAnimationFilter.animateZ) {
436            // just a local update was performed
437            if (previousAnimator != null) {
438                // we need to increase all animation keyframes of the previous animator by the
439                // relative change to the end value
440                PropertyValuesHolder[] values = previousAnimator.getValues();
441                float relativeDiff = newEndValue - previousEndValue;
442                float newStartValue = previousStartValue + relativeDiff;
443                values[0].setFloatValues(newStartValue, newEndValue);
444                child.setTag(TAG_START_TRANSLATION_Z, newStartValue);
445                child.setTag(TAG_END_TRANSLATION_Z, newEndValue);
446                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
447                return;
448            } else {
449                // no new animation needed, let's just apply the value
450                child.setTranslationZ(newEndValue);
451            }
452        }
453
454        ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Z,
455                child.getTranslationZ(), newEndValue);
456        animator.setInterpolator(mFastOutSlowInInterpolator);
457        long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator);
458        animator.setDuration(newDuration);
459        if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
460            animator.setStartDelay(delay);
461        }
462        animator.addListener(getGlobalAnimationFinishedListener());
463        // remove the tag when the animation is finished
464        animator.addListener(new AnimatorListenerAdapter() {
465            @Override
466            public void onAnimationEnd(Animator animation) {
467                child.setTag(TAG_ANIMATOR_TRANSLATION_Z, null);
468                child.setTag(TAG_START_TRANSLATION_Z, null);
469                child.setTag(TAG_END_TRANSLATION_Z, null);
470            }
471        });
472        startAnimator(animator);
473        child.setTag(TAG_ANIMATOR_TRANSLATION_Z, animator);
474        child.setTag(TAG_START_TRANSLATION_Z, child.getTranslationZ());
475        child.setTag(TAG_END_TRANSLATION_Z, newEndValue);
476    }
477
478    private void startYTranslationAnimation(final ExpandableView child,
479            StackScrollState.ViewState viewState, long delay) {
480        Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Y);
481        Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Y);
482        float newEndValue = viewState.yTranslation;
483        if (previousEndValue != null && previousEndValue == newEndValue) {
484            return;
485        }
486        ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Y);
487        if (!mAnimationFilter.animateY) {
488            // just a local update was performed
489            if (previousAnimator != null) {
490                // we need to increase all animation keyframes of the previous animator by the
491                // relative change to the end value
492                PropertyValuesHolder[] values = previousAnimator.getValues();
493                float relativeDiff = newEndValue - previousEndValue;
494                float newStartValue = previousStartValue + relativeDiff;
495                values[0].setFloatValues(newStartValue, newEndValue);
496                child.setTag(TAG_START_TRANSLATION_Y, newStartValue);
497                child.setTag(TAG_END_TRANSLATION_Y, newEndValue);
498                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
499                return;
500            } else {
501                // no new animation needed, let's just apply the value
502                child.setTranslationY(newEndValue);
503                return;
504            }
505        }
506
507        ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Y,
508                child.getTranslationY(), newEndValue);
509        animator.setInterpolator(mFastOutSlowInInterpolator);
510        long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator);
511        animator.setDuration(newDuration);
512        if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
513            animator.setStartDelay(delay);
514        }
515        animator.addListener(getGlobalAnimationFinishedListener());
516        // remove the tag when the animation is finished
517        animator.addListener(new AnimatorListenerAdapter() {
518            @Override
519            public void onAnimationEnd(Animator animation) {
520                child.setTag(TAG_ANIMATOR_TRANSLATION_Y, null);
521                child.setTag(TAG_START_TRANSLATION_Y, null);
522                child.setTag(TAG_END_TRANSLATION_Y, null);
523            }
524        });
525        startAnimator(animator);
526        child.setTag(TAG_ANIMATOR_TRANSLATION_Y, animator);
527        child.setTag(TAG_START_TRANSLATION_Y, child.getTranslationY());
528        child.setTag(TAG_END_TRANSLATION_Y, newEndValue);
529    }
530
531    private void startScaleAnimation(final ExpandableView child,
532            StackScrollState.ViewState viewState) {
533        Float previousStartValue = getChildTag(child, TAG_START_SCALE);
534        Float previousEndValue = getChildTag(child, TAG_END_SCALE);
535        float newEndValue = viewState.scale;
536        if (previousEndValue != null && previousEndValue == newEndValue) {
537            return;
538        }
539        ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_SCALE);
540        if (!mAnimationFilter.animateScale) {
541            // just a local update was performed
542            if (previousAnimator != null) {
543                // we need to increase all animation keyframes of the previous animator by the
544                // relative change to the end value
545                PropertyValuesHolder[] values = previousAnimator.getValues();
546                float relativeDiff = newEndValue - previousEndValue;
547                float newStartValue = previousStartValue + relativeDiff;
548                values[0].setFloatValues(newStartValue, newEndValue);
549                values[1].setFloatValues(newStartValue, newEndValue);
550                child.setTag(TAG_START_SCALE, newStartValue);
551                child.setTag(TAG_END_SCALE, newEndValue);
552                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
553                return;
554            } else {
555                // no new animation needed, let's just apply the value
556                child.setScaleX(newEndValue);
557                child.setScaleY(newEndValue);
558            }
559        }
560
561        PropertyValuesHolder holderX =
562                PropertyValuesHolder.ofFloat(View.SCALE_X, child.getScaleX(), newEndValue);
563        PropertyValuesHolder holderY =
564                PropertyValuesHolder.ofFloat(View.SCALE_Y, child.getScaleY(), newEndValue);
565        ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(child, holderX, holderY);
566        animator.setInterpolator(mFastOutSlowInInterpolator);
567        long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator);
568        animator.setDuration(newDuration);
569        animator.addListener(getGlobalAnimationFinishedListener());
570        // remove the tag when the animation is finished
571        animator.addListener(new AnimatorListenerAdapter() {
572            @Override
573            public void onAnimationEnd(Animator animation) {
574                child.setTag(TAG_ANIMATOR_SCALE, null);
575                child.setTag(TAG_START_SCALE, null);
576                child.setTag(TAG_END_SCALE, null);
577            }
578        });
579        startAnimator(animator);
580        child.setTag(TAG_ANIMATOR_SCALE, animator);
581        child.setTag(TAG_START_SCALE, child.getScaleX());
582        child.setTag(TAG_END_SCALE, newEndValue);
583    }
584
585    private void startAnimator(ValueAnimator animator) {
586        mAnimatorSet.add(animator);
587        animator.start();
588    }
589
590    /**
591     * @return an adapter which ensures that onAnimationFinished is called once no animation is
592     *         running anymore
593     */
594    private AnimatorListenerAdapter getGlobalAnimationFinishedListener() {
595        if (!mAnimationListenerPool.empty()) {
596            return mAnimationListenerPool.pop();
597        }
598
599        // We need to create a new one, no reusable ones found
600        return new AnimatorListenerAdapter() {
601            private boolean mWasCancelled;
602
603            @Override
604            public void onAnimationEnd(Animator animation) {
605                mAnimatorSet.remove(animation);
606                if (mAnimatorSet.isEmpty() && !mWasCancelled) {
607                    onAnimationFinished();
608                }
609                mAnimationListenerPool.push(this);
610            }
611
612            @Override
613            public void onAnimationCancel(Animator animation) {
614                mWasCancelled = true;
615            }
616
617            @Override
618            public void onAnimationStart(Animator animation) {
619                mWasCancelled = false;
620            }
621        };
622    }
623
624    private <T> T getChildTag(View child, int tag) {
625        return (T) child.getTag(tag);
626    }
627
628    /**
629     * Cancel the previous animator and get the duration of the new animation.
630     *
631     * @param previousAnimator the animator which was running before
632     * @return the new duration
633     */
634    private long cancelAnimatorAndGetNewDuration(ValueAnimator previousAnimator) {
635        long newDuration = mCurrentLength;
636        if (previousAnimator != null) {
637            // We take either the desired length of the new animation or the remaining time of
638            // the previous animator, whichever is longer.
639            newDuration = Math.max(previousAnimator.getDuration()
640                    - previousAnimator.getCurrentPlayTime(), newDuration);
641            previousAnimator.cancel();
642        }
643        return newDuration;
644    }
645
646    private void onAnimationFinished() {
647        mHostLayout.onChildAnimationFinished();
648    }
649
650    /**
651     * Process the animationEvents for a new animation
652     *
653     * @param animationEvents the animation events for the animation to perform
654     * @param finalState the final state to animate to
655     */
656    private void processAnimationEvents(
657            ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents,
658            StackScrollState finalState) {
659        for (NotificationStackScrollLayout.AnimationEvent event : animationEvents) {
660            final ExpandableView changingView = (ExpandableView) event.changingView;
661            if (event.animationType ==
662                    NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD) {
663
664                // This item is added, initialize it's properties.
665                StackScrollState.ViewState viewState = finalState
666                        .getViewStateForView(changingView);
667                if (viewState == null) {
668                    // The position for this child was never generated, let's continue.
669                    continue;
670                }
671                if (changingView.getVisibility() == View.GONE) {
672                    // The view was set to gone but the state never removed
673                    finalState.removeViewStateForView(changingView);
674                    continue;
675                }
676                changingView.setAlpha(viewState.alpha);
677                changingView.setTranslationY(viewState.yTranslation);
678                changingView.setTranslationZ(viewState.zTranslation);
679                changingView.setActualHeight(viewState.height, false);
680                mNewAddChildren.add(changingView);
681
682            } else if (event.animationType ==
683                    NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE) {
684                if (changingView.getVisibility() == View.GONE) {
685                    continue;
686                }
687
688                // Find the amount to translate up. This is needed in order to understand the
689                // direction of the remove animation (either downwards or upwards)
690                StackScrollState.ViewState viewState = finalState
691                        .getViewStateForView(event.viewAfterChangingView);
692                int actualHeight = changingView.getActualHeight();
693                // upwards by default
694                float translationDirection = -1.0f;
695                if (viewState != null) {
696                    // there was a view after this one, Approximate the distance the next child
697                    // travelled
698                    translationDirection = ((viewState.yTranslation
699                            - (changingView.getTranslationY() + actualHeight / 2.0f)) * 2 /
700                            actualHeight);
701                    translationDirection = Math.max(Math.min(translationDirection, 1.0f),-1.0f);
702
703                }
704                changingView.performRemoveAnimation(translationDirection, new Runnable() {
705                    @Override
706                    public void run() {
707                        // remove the temporary overlay
708                        mHostLayout.getOverlay().remove(changingView);
709                    }
710                });
711            }
712            mNewEvents.add(event);
713        }
714    }
715
716    public void animateOverScrollToAmount(float targetAmount, final boolean onTop) {
717        final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop);
718        cancelOverScrollAnimators(onTop);
719        ValueAnimator overScrollAnimator = ValueAnimator.ofFloat(startOverScrollAmount,
720                targetAmount);
721        overScrollAnimator.setDuration(ANIMATION_DURATION_STANDARD);
722        overScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
723            @Override
724            public void onAnimationUpdate(ValueAnimator animation) {
725                float currentOverScroll = (float) animation.getAnimatedValue();
726                mHostLayout.setOverScrollAmount(currentOverScroll, onTop, false /* animate */,
727                        false /* cancelAnimators */);
728            }
729        });
730        overScrollAnimator.setInterpolator(mFastOutSlowInInterpolator);
731        overScrollAnimator.start();
732        if (onTop) {
733            mTopOverScrollAnimator = overScrollAnimator;
734        } else {
735            mBottomOverScrollAnimator = overScrollAnimator;
736        }
737    }
738
739    public void cancelOverScrollAnimators(boolean onTop) {
740        ValueAnimator currentAnimator = onTop ? mTopOverScrollAnimator : mBottomOverScrollAnimator;
741        if (currentAnimator != null) {
742            currentAnimator.cancel();
743        }
744    }
745}
746