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