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