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