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