StackStateAnimator.java revision 475b21dfe517ec04f435f6b02f4a53083d040db4
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        // start dark animation
180        child.setDark(viewState.dark, mAnimationFilter.animateDark);
181
182        // apply scrimming
183        child.setScrimAmount(viewState.scrimAmount);
184
185        if (wasAdded) {
186            child.performAddAnimation(delay);
187        }
188    }
189
190    private long calculateChildAnimationDelay(StackScrollState.ViewState viewState,
191            StackScrollState finalState) {
192        long minDelay = 0;
193        for (NotificationStackScrollLayout.AnimationEvent event : mNewEvents) {
194            long delayPerElement = ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING;
195            switch (event.animationType) {
196                case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD: {
197                    int ownIndex = viewState.notGoneIndex;
198                    int changingIndex = finalState
199                            .getViewStateForView(event.changingView).notGoneIndex;
200                    int difference = Math.abs(ownIndex - changingIndex);
201                    difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
202                            difference - 1));
203                    long delay = (DELAY_EFFECT_MAX_INDEX_DIFFERENCE - difference) * delayPerElement;
204                    minDelay = Math.max(delay, minDelay);
205                    break;
206                }
207                case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT:
208                    delayPerElement = ANIMATION_DELAY_PER_ELEMENT_MANUAL;
209                case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE: {
210                    int ownIndex = viewState.notGoneIndex;
211                    boolean noNextView = event.viewAfterChangingView == null;
212                    View viewAfterChangingView = noNextView
213                            ? mHostLayout.getLastChildNotGone()
214                            : event.viewAfterChangingView;
215
216                    int nextIndex = finalState
217                            .getViewStateForView(viewAfterChangingView).notGoneIndex;
218                    if (ownIndex >= nextIndex) {
219                        // we only have the view afterwards
220                        ownIndex++;
221                    }
222                    int difference = Math.abs(ownIndex - nextIndex);
223                    difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
224                            difference - 1));
225                    long delay = difference * delayPerElement;
226                    minDelay = Math.max(delay, minDelay);
227                    break;
228                }
229                default:
230                    break;
231            }
232        }
233        return minDelay;
234    }
235
236    private void startHeightAnimation(final ExpandableView child,
237            StackScrollState.ViewState viewState, long delay) {
238        Integer previousStartValue = getChildTag(child, TAG_START_HEIGHT);
239        Integer previousEndValue = getChildTag(child, TAG_END_HEIGHT);
240        int newEndValue = viewState.height;
241        if (previousEndValue != null && previousEndValue == newEndValue) {
242            return;
243        }
244        ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_HEIGHT);
245        if (!mAnimationFilter.animateHeight) {
246            // just a local update was performed
247            if (previousAnimator != null) {
248                // we need to increase all animation keyframes of the previous animator by the
249                // relative change to the end value
250                PropertyValuesHolder[] values = previousAnimator.getValues();
251                int relativeDiff = newEndValue - previousEndValue;
252                int newStartValue = previousStartValue + relativeDiff;
253                values[0].setIntValues(newStartValue, newEndValue);
254                child.setTag(TAG_START_HEIGHT, newStartValue);
255                child.setTag(TAG_END_HEIGHT, newEndValue);
256                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
257                return;
258            } else {
259                // no new animation needed, let's just apply the value
260                child.setActualHeight(newEndValue, false);
261                return;
262            }
263        }
264
265        ValueAnimator animator = ValueAnimator.ofInt(child.getActualHeight(), newEndValue);
266        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
267            @Override
268            public void onAnimationUpdate(ValueAnimator animation) {
269                child.setActualHeight((int) animation.getAnimatedValue(),
270                        false /* notifyListeners */);
271            }
272        });
273        animator.setInterpolator(mFastOutSlowInInterpolator);
274        long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator);
275        animator.setDuration(newDuration);
276        if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
277            animator.setStartDelay(delay);
278        }
279        animator.addListener(getGlobalAnimationFinishedListener());
280        // remove the tag when the animation is finished
281        animator.addListener(new AnimatorListenerAdapter() {
282            @Override
283            public void onAnimationEnd(Animator animation) {
284                child.setTag(TAG_ANIMATOR_HEIGHT, null);
285                child.setTag(TAG_START_HEIGHT, null);
286                child.setTag(TAG_END_HEIGHT, null);
287            }
288        });
289        startAnimator(animator);
290        child.setTag(TAG_ANIMATOR_HEIGHT, animator);
291        child.setTag(TAG_START_HEIGHT, child.getActualHeight());
292        child.setTag(TAG_END_HEIGHT, newEndValue);
293    }
294
295    private void startInsetAnimation(final ExpandableView child,
296            StackScrollState.ViewState viewState, long delay) {
297        Integer previousStartValue = getChildTag(child, TAG_START_TOP_INSET);
298        Integer previousEndValue = getChildTag(child, TAG_END_TOP_INSET);
299        int newEndValue = viewState.clipTopAmount;
300        if (previousEndValue != null && previousEndValue == newEndValue) {
301            return;
302        }
303        ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TOP_INSET);
304        if (!mAnimationFilter.animateTopInset) {
305            // just a local update was performed
306            if (previousAnimator != null) {
307                // we need to increase all animation keyframes of the previous animator by the
308                // relative change to the end value
309                PropertyValuesHolder[] values = previousAnimator.getValues();
310                int relativeDiff = newEndValue - previousEndValue;
311                int newStartValue = previousStartValue + relativeDiff;
312                values[0].setIntValues(newStartValue, newEndValue);
313                child.setTag(TAG_START_TOP_INSET, newStartValue);
314                child.setTag(TAG_END_TOP_INSET, newEndValue);
315                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
316                return;
317            } else {
318                // no new animation needed, let's just apply the value
319                child.setClipTopAmount(newEndValue);
320                return;
321            }
322        }
323
324        ValueAnimator animator = ValueAnimator.ofInt(child.getClipTopAmount(), newEndValue);
325        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
326            @Override
327            public void onAnimationUpdate(ValueAnimator animation) {
328                child.setClipTopAmount((int) animation.getAnimatedValue());
329            }
330        });
331        animator.setInterpolator(mFastOutSlowInInterpolator);
332        long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator);
333        animator.setDuration(newDuration);
334        if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
335            animator.setStartDelay(delay);
336        }
337        animator.addListener(getGlobalAnimationFinishedListener());
338        // remove the tag when the animation is finished
339        animator.addListener(new AnimatorListenerAdapter() {
340            @Override
341            public void onAnimationEnd(Animator animation) {
342                child.setTag(TAG_ANIMATOR_TOP_INSET, null);
343                child.setTag(TAG_START_TOP_INSET, null);
344                child.setTag(TAG_END_TOP_INSET, null);
345            }
346        });
347        startAnimator(animator);
348        child.setTag(TAG_ANIMATOR_TOP_INSET, animator);
349        child.setTag(TAG_START_TOP_INSET, child.getClipTopAmount());
350        child.setTag(TAG_END_TOP_INSET, newEndValue);
351    }
352
353    private void startAlphaAnimation(final ExpandableView child,
354            final StackScrollState.ViewState viewState, long delay) {
355        Float previousStartValue = getChildTag(child,TAG_START_ALPHA);
356        Float previousEndValue = getChildTag(child,TAG_END_ALPHA);
357        final float newEndValue = viewState.alpha;
358        if (previousEndValue != null && previousEndValue == newEndValue) {
359            return;
360        }
361        ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_ALPHA);
362        if (!mAnimationFilter.animateAlpha) {
363            // just a local update was performed
364            if (previousAnimator != null) {
365                // we need to increase all animation keyframes of the previous animator by the
366                // relative change to the end value
367                PropertyValuesHolder[] values = previousAnimator.getValues();
368                float relativeDiff = newEndValue - previousEndValue;
369                float newStartValue = previousStartValue + relativeDiff;
370                values[0].setFloatValues(newStartValue, newEndValue);
371                child.setTag(TAG_START_ALPHA, newStartValue);
372                child.setTag(TAG_END_ALPHA, newEndValue);
373                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
374                return;
375            } else {
376                // no new animation needed, let's just apply the value
377                child.setAlpha(newEndValue);
378                if (newEndValue == 0) {
379                    child.setVisibility(View.INVISIBLE);
380                }
381            }
382        }
383
384        ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.ALPHA,
385                child.getAlpha(), newEndValue);
386        animator.setInterpolator(mFastOutSlowInInterpolator);
387        // Handle layer type
388        child.setLayerType(View.LAYER_TYPE_HARDWARE, null);
389        animator.addListener(new AnimatorListenerAdapter() {
390            public boolean mWasCancelled;
391
392            @Override
393            public void onAnimationEnd(Animator animation) {
394                child.setLayerType(View.LAYER_TYPE_NONE, null);
395                if (newEndValue == 0 && !mWasCancelled) {
396                    child.setVisibility(View.INVISIBLE);
397                }
398                child.setTag(TAG_ANIMATOR_ALPHA, null);
399                child.setTag(TAG_START_ALPHA, null);
400                child.setTag(TAG_END_ALPHA, null);
401            }
402
403            @Override
404            public void onAnimationCancel(Animator animation) {
405                mWasCancelled = true;
406            }
407
408            @Override
409            public void onAnimationStart(Animator animation) {
410                mWasCancelled = false;
411            }
412        });
413        long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator);
414        animator.setDuration(newDuration);
415        if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
416            animator.setStartDelay(delay);
417        }
418        animator.addListener(getGlobalAnimationFinishedListener());
419        // remove the tag when the animation is finished
420        animator.addListener(new AnimatorListenerAdapter() {
421            @Override
422            public void onAnimationEnd(Animator animation) {
423
424            }
425        });
426        startAnimator(animator);
427        child.setTag(TAG_ANIMATOR_ALPHA, animator);
428        child.setTag(TAG_START_ALPHA, child.getAlpha());
429        child.setTag(TAG_END_ALPHA, newEndValue);
430    }
431
432    private void startZTranslationAnimation(final ExpandableView child,
433            final StackScrollState.ViewState viewState, long delay) {
434        Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Z);
435        Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Z);
436        float newEndValue = viewState.zTranslation;
437        if (previousEndValue != null && previousEndValue == newEndValue) {
438            return;
439        }
440        ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Z);
441        if (!mAnimationFilter.animateZ) {
442            // just a local update was performed
443            if (previousAnimator != null) {
444                // we need to increase all animation keyframes of the previous animator by the
445                // relative change to the end value
446                PropertyValuesHolder[] values = previousAnimator.getValues();
447                float relativeDiff = newEndValue - previousEndValue;
448                float newStartValue = previousStartValue + relativeDiff;
449                values[0].setFloatValues(newStartValue, newEndValue);
450                child.setTag(TAG_START_TRANSLATION_Z, newStartValue);
451                child.setTag(TAG_END_TRANSLATION_Z, newEndValue);
452                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
453                return;
454            } else {
455                // no new animation needed, let's just apply the value
456                child.setTranslationZ(newEndValue);
457            }
458        }
459
460        ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Z,
461                child.getTranslationZ(), newEndValue);
462        animator.setInterpolator(mFastOutSlowInInterpolator);
463        long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator);
464        animator.setDuration(newDuration);
465        if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
466            animator.setStartDelay(delay);
467        }
468        animator.addListener(getGlobalAnimationFinishedListener());
469        // remove the tag when the animation is finished
470        animator.addListener(new AnimatorListenerAdapter() {
471            @Override
472            public void onAnimationEnd(Animator animation) {
473                child.setTag(TAG_ANIMATOR_TRANSLATION_Z, null);
474                child.setTag(TAG_START_TRANSLATION_Z, null);
475                child.setTag(TAG_END_TRANSLATION_Z, null);
476            }
477        });
478        startAnimator(animator);
479        child.setTag(TAG_ANIMATOR_TRANSLATION_Z, animator);
480        child.setTag(TAG_START_TRANSLATION_Z, child.getTranslationZ());
481        child.setTag(TAG_END_TRANSLATION_Z, newEndValue);
482    }
483
484    private void startYTranslationAnimation(final ExpandableView child,
485            StackScrollState.ViewState viewState, long delay) {
486        Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Y);
487        Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Y);
488        float newEndValue = viewState.yTranslation;
489        if (previousEndValue != null && previousEndValue == newEndValue) {
490            return;
491        }
492        ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Y);
493        if (!mAnimationFilter.animateY) {
494            // just a local update was performed
495            if (previousAnimator != null) {
496                // we need to increase all animation keyframes of the previous animator by the
497                // relative change to the end value
498                PropertyValuesHolder[] values = previousAnimator.getValues();
499                float relativeDiff = newEndValue - previousEndValue;
500                float newStartValue = previousStartValue + relativeDiff;
501                values[0].setFloatValues(newStartValue, newEndValue);
502                child.setTag(TAG_START_TRANSLATION_Y, newStartValue);
503                child.setTag(TAG_END_TRANSLATION_Y, newEndValue);
504                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
505                return;
506            } else {
507                // no new animation needed, let's just apply the value
508                child.setTranslationY(newEndValue);
509                return;
510            }
511        }
512
513        ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Y,
514                child.getTranslationY(), newEndValue);
515        animator.setInterpolator(mFastOutSlowInInterpolator);
516        long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator);
517        animator.setDuration(newDuration);
518        if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
519            animator.setStartDelay(delay);
520        }
521        animator.addListener(getGlobalAnimationFinishedListener());
522        // remove the tag when the animation is finished
523        animator.addListener(new AnimatorListenerAdapter() {
524            @Override
525            public void onAnimationEnd(Animator animation) {
526                child.setTag(TAG_ANIMATOR_TRANSLATION_Y, null);
527                child.setTag(TAG_START_TRANSLATION_Y, null);
528                child.setTag(TAG_END_TRANSLATION_Y, null);
529            }
530        });
531        startAnimator(animator);
532        child.setTag(TAG_ANIMATOR_TRANSLATION_Y, animator);
533        child.setTag(TAG_START_TRANSLATION_Y, child.getTranslationY());
534        child.setTag(TAG_END_TRANSLATION_Y, newEndValue);
535    }
536
537    private void startScaleAnimation(final ExpandableView child,
538            StackScrollState.ViewState viewState) {
539        Float previousStartValue = getChildTag(child, TAG_START_SCALE);
540        Float previousEndValue = getChildTag(child, TAG_END_SCALE);
541        float newEndValue = viewState.scale;
542        if (previousEndValue != null && previousEndValue == newEndValue) {
543            return;
544        }
545        ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_SCALE);
546        if (!mAnimationFilter.animateScale) {
547            // just a local update was performed
548            if (previousAnimator != null) {
549                // we need to increase all animation keyframes of the previous animator by the
550                // relative change to the end value
551                PropertyValuesHolder[] values = previousAnimator.getValues();
552                float relativeDiff = newEndValue - previousEndValue;
553                float newStartValue = previousStartValue + relativeDiff;
554                values[0].setFloatValues(newStartValue, newEndValue);
555                values[1].setFloatValues(newStartValue, newEndValue);
556                child.setTag(TAG_START_SCALE, newStartValue);
557                child.setTag(TAG_END_SCALE, newEndValue);
558                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
559                return;
560            } else {
561                // no new animation needed, let's just apply the value
562                child.setScaleX(newEndValue);
563                child.setScaleY(newEndValue);
564            }
565        }
566
567        PropertyValuesHolder holderX =
568                PropertyValuesHolder.ofFloat(View.SCALE_X, child.getScaleX(), newEndValue);
569        PropertyValuesHolder holderY =
570                PropertyValuesHolder.ofFloat(View.SCALE_Y, child.getScaleY(), newEndValue);
571        ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(child, holderX, holderY);
572        animator.setInterpolator(mFastOutSlowInInterpolator);
573        long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator);
574        animator.setDuration(newDuration);
575        animator.addListener(getGlobalAnimationFinishedListener());
576        // remove the tag when the animation is finished
577        animator.addListener(new AnimatorListenerAdapter() {
578            @Override
579            public void onAnimationEnd(Animator animation) {
580                child.setTag(TAG_ANIMATOR_SCALE, null);
581                child.setTag(TAG_START_SCALE, null);
582                child.setTag(TAG_END_SCALE, null);
583            }
584        });
585        startAnimator(animator);
586        child.setTag(TAG_ANIMATOR_SCALE, animator);
587        child.setTag(TAG_START_SCALE, child.getScaleX());
588        child.setTag(TAG_END_SCALE, newEndValue);
589    }
590
591    private void startAnimator(ValueAnimator animator) {
592        mAnimatorSet.add(animator);
593        animator.start();
594    }
595
596    /**
597     * @return an adapter which ensures that onAnimationFinished is called once no animation is
598     *         running anymore
599     */
600    private AnimatorListenerAdapter getGlobalAnimationFinishedListener() {
601        if (!mAnimationListenerPool.empty()) {
602            return mAnimationListenerPool.pop();
603        }
604
605        // We need to create a new one, no reusable ones found
606        return new AnimatorListenerAdapter() {
607            private boolean mWasCancelled;
608
609            @Override
610            public void onAnimationEnd(Animator animation) {
611                mAnimatorSet.remove(animation);
612                if (mAnimatorSet.isEmpty() && !mWasCancelled) {
613                    onAnimationFinished();
614                }
615                mAnimationListenerPool.push(this);
616            }
617
618            @Override
619            public void onAnimationCancel(Animator animation) {
620                mWasCancelled = true;
621            }
622
623            @Override
624            public void onAnimationStart(Animator animation) {
625                mWasCancelled = false;
626            }
627        };
628    }
629
630    private <T> T getChildTag(View child, int tag) {
631        return (T) child.getTag(tag);
632    }
633
634    /**
635     * Cancel the previous animator and get the duration of the new animation.
636     *
637     * @param previousAnimator the animator which was running before
638     * @return the new duration
639     */
640    private long cancelAnimatorAndGetNewDuration(ValueAnimator previousAnimator) {
641        long newDuration = mCurrentLength;
642        if (previousAnimator != null) {
643            // We take either the desired length of the new animation or the remaining time of
644            // the previous animator, whichever is longer.
645            newDuration = Math.max(previousAnimator.getDuration()
646                    - previousAnimator.getCurrentPlayTime(), newDuration);
647            previousAnimator.cancel();
648        }
649        return newDuration;
650    }
651
652    private void onAnimationFinished() {
653        mHostLayout.onChildAnimationFinished();
654    }
655
656    /**
657     * Process the animationEvents for a new animation
658     *
659     * @param animationEvents the animation events for the animation to perform
660     * @param finalState the final state to animate to
661     */
662    private void processAnimationEvents(
663            ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents,
664            StackScrollState finalState) {
665        for (NotificationStackScrollLayout.AnimationEvent event : animationEvents) {
666            final ExpandableView changingView = (ExpandableView) event.changingView;
667            if (event.animationType ==
668                    NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD) {
669
670                // This item is added, initialize it's properties.
671                StackScrollState.ViewState viewState = finalState
672                        .getViewStateForView(changingView);
673                if (viewState == null) {
674                    // The position for this child was never generated, let's continue.
675                    continue;
676                }
677                if (changingView.getVisibility() == View.GONE) {
678                    // The view was set to gone but the state never removed
679                    finalState.removeViewStateForView(changingView);
680                    continue;
681                }
682                changingView.setAlpha(viewState.alpha);
683                changingView.setTranslationY(viewState.yTranslation);
684                changingView.setTranslationZ(viewState.zTranslation);
685                changingView.setActualHeight(viewState.height, false);
686                mNewAddChildren.add(changingView);
687
688            } else if (event.animationType ==
689                    NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE) {
690                if (changingView.getVisibility() == View.GONE) {
691                    continue;
692                }
693
694                // Find the amount to translate up. This is needed in order to understand the
695                // direction of the remove animation (either downwards or upwards)
696                StackScrollState.ViewState viewState = finalState
697                        .getViewStateForView(event.viewAfterChangingView);
698                int actualHeight = changingView.getActualHeight();
699                // upwards by default
700                float translationDirection = -1.0f;
701                if (viewState != null) {
702                    // there was a view after this one, Approximate the distance the next child
703                    // travelled
704                    translationDirection = ((viewState.yTranslation
705                            - (changingView.getTranslationY() + actualHeight / 2.0f)) * 2 /
706                            actualHeight);
707                    translationDirection = Math.max(Math.min(translationDirection, 1.0f),-1.0f);
708
709                }
710                changingView.performRemoveAnimation(translationDirection, new Runnable() {
711                    @Override
712                    public void run() {
713                        // remove the temporary overlay
714                        mHostLayout.getOverlay().remove(changingView);
715                    }
716                });
717            }
718            mNewEvents.add(event);
719        }
720    }
721
722    public void animateOverScrollToAmount(float targetAmount, final boolean onTop,
723            final boolean isRubberbanded) {
724        final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop);
725        if (targetAmount == startOverScrollAmount) {
726            return;
727        }
728        cancelOverScrollAnimators(onTop);
729        ValueAnimator overScrollAnimator = ValueAnimator.ofFloat(startOverScrollAmount,
730                targetAmount);
731        overScrollAnimator.setDuration(ANIMATION_DURATION_STANDARD);
732        overScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
733            @Override
734            public void onAnimationUpdate(ValueAnimator animation) {
735                float currentOverScroll = (float) animation.getAnimatedValue();
736                mHostLayout.setOverScrollAmount(
737                        currentOverScroll, onTop, false /* animate */, false /* cancelAnimators */,
738                        isRubberbanded);
739            }
740        });
741        overScrollAnimator.setInterpolator(mFastOutSlowInInterpolator);
742        overScrollAnimator.addListener(new AnimatorListenerAdapter() {
743            @Override
744            public void onAnimationEnd(Animator animation) {
745                if (onTop) {
746                    mTopOverScrollAnimator = null;
747                } else {
748                    mBottomOverScrollAnimator = null;
749                }
750            }
751        });
752        overScrollAnimator.start();
753        if (onTop) {
754            mTopOverScrollAnimator = overScrollAnimator;
755        } else {
756            mBottomOverScrollAnimator = overScrollAnimator;
757        }
758    }
759
760    public void cancelOverScrollAnimators(boolean onTop) {
761        ValueAnimator currentAnimator = onTop ? mTopOverScrollAnimator : mBottomOverScrollAnimator;
762        if (currentAnimator != null) {
763            currentAnimator.cancel();
764        }
765    }
766}
767