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