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