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