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