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.ValueAnimator;
22import android.util.Property;
23import android.view.View;
24import android.view.ViewGroup;
25import android.view.animation.Interpolator;
26
27import com.android.systemui.Interpolators;
28import com.android.systemui.R;
29import com.android.systemui.statusbar.ExpandableNotificationRow;
30import com.android.systemui.statusbar.ExpandableView;
31import com.android.systemui.statusbar.NotificationShelf;
32
33import java.util.ArrayList;
34import java.util.HashSet;
35import java.util.Stack;
36
37/**
38 * An stack state animator which handles animations to new StackScrollStates
39 */
40public class StackStateAnimator {
41
42    public static final int ANIMATION_DURATION_STANDARD = 360;
43    public static final int ANIMATION_DURATION_WAKEUP = 200;
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 DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2;
54    public static final int ANIMATION_DELAY_HEADS_UP = 120;
55
56    private final int mGoToFullShadeAppearingTranslation;
57    private final ExpandableViewState mTmpState = new ExpandableViewState();
58    private final AnimationProperties mAnimationProperties;
59    public NotificationStackScrollLayout mHostLayout;
60    private ArrayList<NotificationStackScrollLayout.AnimationEvent> mNewEvents =
61            new ArrayList<>();
62    private ArrayList<View> mNewAddChildren = new ArrayList<>();
63    private HashSet<View> mHeadsUpAppearChildren = new HashSet<>();
64    private HashSet<View> mHeadsUpDisappearChildren = new HashSet<>();
65    private HashSet<Animator> mAnimatorSet = new HashSet<>();
66    private Stack<AnimatorListenerAdapter> mAnimationListenerPool = new Stack<>();
67    private AnimationFilter mAnimationFilter = new AnimationFilter();
68    private long mCurrentLength;
69    private long mCurrentAdditionalDelay;
70
71    /** The current index for the last child which was not added in this event set. */
72    private int mCurrentLastNotAddedIndex;
73    private ValueAnimator mTopOverScrollAnimator;
74    private ValueAnimator mBottomOverScrollAnimator;
75    private int mHeadsUpAppearHeightBottom;
76    private boolean mShadeExpanded;
77    private ArrayList<View> mChildrenToClearFromOverlay = new ArrayList<>();
78    private NotificationShelf mShelf;
79
80    public StackStateAnimator(NotificationStackScrollLayout hostLayout) {
81        mHostLayout = hostLayout;
82        mGoToFullShadeAppearingTranslation =
83                hostLayout.getContext().getResources().getDimensionPixelSize(
84                        R.dimen.go_to_full_shade_appearing_translation);
85        mAnimationProperties = new AnimationProperties() {
86            @Override
87            public AnimationFilter getAnimationFilter() {
88                return mAnimationFilter;
89            }
90
91            @Override
92            public AnimatorListenerAdapter getAnimationFinishListener() {
93                return getGlobalAnimationFinishedListener();
94            }
95
96            @Override
97            public boolean wasAdded(View view) {
98                return mNewAddChildren.contains(view);
99            }
100
101            @Override
102            public Interpolator getCustomInterpolator(View child, Property property) {
103                if (mHeadsUpAppearChildren.contains(child) && View.TRANSLATION_Y.equals(property)) {
104                    return Interpolators.HEADS_UP_APPEAR;
105                }
106                return null;
107            }
108        };
109    }
110
111    public boolean isRunning() {
112        return !mAnimatorSet.isEmpty();
113    }
114
115    public void startAnimationForEvents(
116            ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents,
117            StackScrollState finalState, long additionalDelay) {
118
119        processAnimationEvents(mAnimationEvents, finalState);
120
121        int childCount = mHostLayout.getChildCount();
122        mAnimationFilter.applyCombination(mNewEvents);
123        mCurrentAdditionalDelay = additionalDelay;
124        mCurrentLength = NotificationStackScrollLayout.AnimationEvent.combineLength(mNewEvents);
125        mCurrentLastNotAddedIndex = findLastNotAddedIndex(finalState);
126        for (int i = 0; i < childCount; i++) {
127            final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
128
129            ExpandableViewState viewState = finalState.getViewStateForView(child);
130            if (viewState == null || child.getVisibility() == View.GONE
131                    || applyWithoutAnimation(child, viewState, finalState)) {
132                continue;
133            }
134
135            initAnimationProperties(finalState, child, viewState);
136            viewState.animateTo(child, mAnimationProperties);
137        }
138        if (!isRunning()) {
139            // no child has preformed any animation, lets finish
140            onAnimationFinished();
141        }
142        mHeadsUpAppearChildren.clear();
143        mHeadsUpDisappearChildren.clear();
144        mNewEvents.clear();
145        mNewAddChildren.clear();
146    }
147
148    private void initAnimationProperties(StackScrollState finalState, ExpandableView child,
149            ExpandableViewState viewState) {
150        boolean wasAdded = mAnimationProperties.wasAdded(child);
151        mAnimationProperties.duration = mCurrentLength;
152        adaptDurationWhenGoingToFullShade(child, viewState, wasAdded);
153        mAnimationProperties.delay = 0;
154        if (wasAdded || mAnimationFilter.hasDelays
155                        && (viewState.yTranslation != child.getTranslationY()
156                        || viewState.zTranslation != child.getTranslationZ()
157                        || viewState.alpha != child.getAlpha()
158                        || viewState.height != child.getActualHeight()
159                        || viewState.clipTopAmount != child.getClipTopAmount()
160                        || viewState.dark != child.isDark()
161                        || viewState.shadowAlpha != child.getShadowAlpha())) {
162            mAnimationProperties.delay = mCurrentAdditionalDelay
163                    + calculateChildAnimationDelay(viewState, finalState);
164        }
165    }
166
167    private void adaptDurationWhenGoingToFullShade(ExpandableView child,
168            ExpandableViewState viewState, boolean wasAdded) {
169        if (wasAdded && mAnimationFilter.hasGoToFullShadeEvent) {
170            child.setTranslationY(child.getTranslationY() + mGoToFullShadeAppearingTranslation);
171            float longerDurationFactor = viewState.notGoneIndex - mCurrentLastNotAddedIndex;
172            longerDurationFactor = (float) Math.pow(longerDurationFactor, 0.7f);
173            mAnimationProperties.duration = ANIMATION_DURATION_APPEAR_DISAPPEAR + 50 +
174                    (long) (100 * longerDurationFactor);
175        }
176    }
177
178    /**
179     * Determines if a view should not perform an animation and applies it directly.
180     *
181     * @return true if no animation should be performed
182     */
183    private boolean applyWithoutAnimation(ExpandableView child, ExpandableViewState viewState,
184            StackScrollState finalState) {
185        if (mShadeExpanded) {
186            return false;
187        }
188        if (ViewState.isAnimatingY(child)) {
189            // A Y translation animation is running
190            return false;
191        }
192        if (mHeadsUpDisappearChildren.contains(child) || mHeadsUpAppearChildren.contains(child)) {
193            // This is a heads up animation
194            return false;
195        }
196        if (NotificationStackScrollLayout.isPinnedHeadsUp(child)) {
197            // This is another headsUp which might move. Let's animate!
198            return false;
199        }
200        viewState.applyToView(child);
201        return true;
202    }
203
204    private int findLastNotAddedIndex(StackScrollState finalState) {
205        int childCount = mHostLayout.getChildCount();
206        for (int i = childCount - 1; i >= 0; i--) {
207            final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
208
209            ExpandableViewState viewState = finalState.getViewStateForView(child);
210            if (viewState == null || child.getVisibility() == View.GONE) {
211                continue;
212            }
213            if (!mNewAddChildren.contains(child)) {
214                return viewState.notGoneIndex;
215            }
216        }
217        return -1;
218    }
219
220    private long calculateChildAnimationDelay(ExpandableViewState viewState,
221            StackScrollState finalState) {
222        if (mAnimationFilter.hasGoToFullShadeEvent) {
223            return calculateDelayGoToFullShade(viewState);
224        }
225        if (mAnimationFilter.hasHeadsUpDisappearClickEvent) {
226            return ANIMATION_DELAY_HEADS_UP;
227        }
228        long minDelay = 0;
229        for (NotificationStackScrollLayout.AnimationEvent event : mNewEvents) {
230            long delayPerElement = ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING;
231            switch (event.animationType) {
232                case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD: {
233                    int ownIndex = viewState.notGoneIndex;
234                    int changingIndex = finalState
235                            .getViewStateForView(event.changingView).notGoneIndex;
236                    int difference = Math.abs(ownIndex - changingIndex);
237                    difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
238                            difference - 1));
239                    long delay = (DELAY_EFFECT_MAX_INDEX_DIFFERENCE - difference) * delayPerElement;
240                    minDelay = Math.max(delay, minDelay);
241                    break;
242                }
243                case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT:
244                    delayPerElement = ANIMATION_DELAY_PER_ELEMENT_MANUAL;
245                case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE: {
246                    int ownIndex = viewState.notGoneIndex;
247                    boolean noNextView = event.viewAfterChangingView == null;
248                    View viewAfterChangingView = noNextView
249                            ? mHostLayout.getLastChildNotGone()
250                            : event.viewAfterChangingView;
251                    if (viewAfterChangingView == null) {
252                        // This can happen when the last view in the list is removed.
253                        // Since the shelf is still around and the only view, the code still goes
254                        // in here and tries to calculate the delay for it when case its properties
255                        // have changed.
256                        continue;
257                    }
258                    int nextIndex = finalState
259                            .getViewStateForView(viewAfterChangingView).notGoneIndex;
260                    if (ownIndex >= nextIndex) {
261                        // we only have the view afterwards
262                        ownIndex++;
263                    }
264                    int difference = Math.abs(ownIndex - nextIndex);
265                    difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
266                            difference - 1));
267                    long delay = difference * delayPerElement;
268                    minDelay = Math.max(delay, minDelay);
269                    break;
270                }
271                default:
272                    break;
273            }
274        }
275        return minDelay;
276    }
277
278    private long calculateDelayGoToFullShade(ExpandableViewState viewState) {
279        int shelfIndex = mShelf.getNotGoneIndex();
280        float index = viewState.notGoneIndex;
281        long result = 0;
282        if (index > shelfIndex) {
283            float diff = index - shelfIndex;
284            diff = (float) Math.pow(diff, 0.7f);
285            result += (long) (diff * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE * 0.25);
286            index = shelfIndex;
287        }
288        index = (float) Math.pow(index, 0.7f);
289        result += (long) (index * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE);
290        return result;
291    }
292
293    /**
294     * @return an adapter which ensures that onAnimationFinished is called once no animation is
295     *         running anymore
296     */
297    private AnimatorListenerAdapter getGlobalAnimationFinishedListener() {
298        if (!mAnimationListenerPool.empty()) {
299            return mAnimationListenerPool.pop();
300        }
301
302        // We need to create a new one, no reusable ones found
303        return new AnimatorListenerAdapter() {
304            private boolean mWasCancelled;
305
306            @Override
307            public void onAnimationEnd(Animator animation) {
308                mAnimatorSet.remove(animation);
309                if (mAnimatorSet.isEmpty() && !mWasCancelled) {
310                    onAnimationFinished();
311                }
312                mAnimationListenerPool.push(this);
313            }
314
315            @Override
316            public void onAnimationCancel(Animator animation) {
317                mWasCancelled = true;
318            }
319
320            @Override
321            public void onAnimationStart(Animator animation) {
322                mWasCancelled = false;
323                mAnimatorSet.add(animation);
324            }
325        };
326    }
327
328    private void onAnimationFinished() {
329        mHostLayout.onChildAnimationFinished();
330        for (View v : mChildrenToClearFromOverlay) {
331            removeFromOverlay(v);
332        }
333        mChildrenToClearFromOverlay.clear();
334    }
335
336    /**
337     * Process the animationEvents for a new animation
338     *
339     * @param animationEvents the animation events for the animation to perform
340     * @param finalState the final state to animate to
341     */
342    private void processAnimationEvents(
343            ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents,
344            StackScrollState finalState) {
345        for (NotificationStackScrollLayout.AnimationEvent event : animationEvents) {
346            final ExpandableView changingView = (ExpandableView) event.changingView;
347            if (event.animationType ==
348                    NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD) {
349
350                // This item is added, initialize it's properties.
351                ExpandableViewState viewState = finalState
352                        .getViewStateForView(changingView);
353                if (viewState == null) {
354                    // The position for this child was never generated, let's continue.
355                    continue;
356                }
357                viewState.applyToView(changingView);
358                mNewAddChildren.add(changingView);
359
360            } else if (event.animationType ==
361                    NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE) {
362                if (changingView.getVisibility() != View.VISIBLE) {
363                    removeFromOverlay(changingView);
364                    continue;
365                }
366
367                // Find the amount to translate up. This is needed in order to understand the
368                // direction of the remove animation (either downwards or upwards)
369                ExpandableViewState viewState = finalState
370                        .getViewStateForView(event.viewAfterChangingView);
371                int actualHeight = changingView.getActualHeight();
372                // upwards by default
373                float translationDirection = -1.0f;
374                if (viewState != null) {
375                    float ownPosition = changingView.getTranslationY();
376                    if (changingView instanceof ExpandableNotificationRow
377                            && event.viewAfterChangingView instanceof ExpandableNotificationRow) {
378                        ExpandableNotificationRow changingRow =
379                                (ExpandableNotificationRow) changingView;
380                        ExpandableNotificationRow nextRow =
381                                (ExpandableNotificationRow) event.viewAfterChangingView;
382                        if (changingRow.isRemoved()
383                                && changingRow.wasChildInGroupWhenRemoved()
384                                && !nextRow.isChildInGroup()) {
385                            // the next row isn't actually a child from a group! Let's
386                            // compare absolute positions!
387                            ownPosition = changingRow.getTranslationWhenRemoved();
388                        }
389                    }
390                    // there was a view after this one, Approximate the distance the next child
391                    // travelled
392                    translationDirection = ((viewState.yTranslation
393                            - (ownPosition + actualHeight / 2.0f)) * 2 /
394                            actualHeight);
395                    translationDirection = Math.max(Math.min(translationDirection, 1.0f),-1.0f);
396
397                }
398                changingView.performRemoveAnimation(ANIMATION_DURATION_APPEAR_DISAPPEAR,
399                        translationDirection, new Runnable() {
400                    @Override
401                    public void run() {
402                        // remove the temporary overlay
403                        removeFromOverlay(changingView);
404                    }
405                });
406            } else if (event.animationType ==
407                NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT) {
408                // A race condition can trigger the view to be added to the overlay even though
409                // it was fully swiped out. So let's remove it
410                mHostLayout.getOverlay().remove(changingView);
411                if (Math.abs(changingView.getTranslation()) == changingView.getWidth()
412                        && changingView.getTransientContainer() != null) {
413                    changingView.getTransientContainer().removeTransientView(changingView);
414                }
415            } else if (event.animationType == NotificationStackScrollLayout
416                    .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) {
417                ExpandableNotificationRow row = (ExpandableNotificationRow) event.changingView;
418                row.prepareExpansionChanged(finalState);
419            } else if (event.animationType == NotificationStackScrollLayout
420                    .AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) {
421                // This item is added, initialize it's properties.
422                ExpandableViewState viewState = finalState.getViewStateForView(changingView);
423                mTmpState.copyFrom(viewState);
424                if (event.headsUpFromBottom) {
425                    mTmpState.yTranslation = mHeadsUpAppearHeightBottom;
426                } else {
427                    mTmpState.yTranslation = -mTmpState.height;
428                }
429                mHeadsUpAppearChildren.add(changingView);
430                mTmpState.applyToView(changingView);
431            } else if (event.animationType == NotificationStackScrollLayout
432                            .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR ||
433                    event.animationType == NotificationStackScrollLayout
434                            .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK) {
435                mHeadsUpDisappearChildren.add(changingView);
436                if (changingView.getParent() == null) {
437                    // This notification was actually removed, so we need to add it to the overlay
438                    mHostLayout.getOverlay().add(changingView);
439                    mTmpState.initFrom(changingView);
440                    mTmpState.yTranslation = -changingView.getActualHeight();
441                    // We temporarily enable Y animations, the real filter will be combined
442                    // afterwards anyway
443                    mAnimationFilter.animateY = true;
444                    mAnimationProperties.delay =
445                            event.animationType == NotificationStackScrollLayout
446                                    .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
447                            ? ANIMATION_DELAY_HEADS_UP
448                            : 0;
449                    mAnimationProperties.duration = ANIMATION_DURATION_HEADS_UP_DISAPPEAR;
450                    mTmpState.animateTo(changingView, mAnimationProperties);
451                    mChildrenToClearFromOverlay.add(changingView);
452                }
453            }
454            mNewEvents.add(event);
455        }
456    }
457
458    public static void removeFromOverlay(View changingView) {
459        ViewGroup parent = (ViewGroup) changingView.getParent();
460        if (parent != null) {
461            parent.removeView(changingView);
462        }
463    }
464
465    public void animateOverScrollToAmount(float targetAmount, final boolean onTop,
466            final boolean isRubberbanded) {
467        final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop);
468        if (targetAmount == startOverScrollAmount) {
469            return;
470        }
471        cancelOverScrollAnimators(onTop);
472        ValueAnimator overScrollAnimator = ValueAnimator.ofFloat(startOverScrollAmount,
473                targetAmount);
474        overScrollAnimator.setDuration(ANIMATION_DURATION_STANDARD);
475        overScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
476            @Override
477            public void onAnimationUpdate(ValueAnimator animation) {
478                float currentOverScroll = (float) animation.getAnimatedValue();
479                mHostLayout.setOverScrollAmount(
480                        currentOverScroll, onTop, false /* animate */, false /* cancelAnimators */,
481                        isRubberbanded);
482            }
483        });
484        overScrollAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
485        overScrollAnimator.addListener(new AnimatorListenerAdapter() {
486            @Override
487            public void onAnimationEnd(Animator animation) {
488                if (onTop) {
489                    mTopOverScrollAnimator = null;
490                } else {
491                    mBottomOverScrollAnimator = null;
492                }
493            }
494        });
495        overScrollAnimator.start();
496        if (onTop) {
497            mTopOverScrollAnimator = overScrollAnimator;
498        } else {
499            mBottomOverScrollAnimator = overScrollAnimator;
500        }
501    }
502
503    public void cancelOverScrollAnimators(boolean onTop) {
504        ValueAnimator currentAnimator = onTop ? mTopOverScrollAnimator : mBottomOverScrollAnimator;
505        if (currentAnimator != null) {
506            currentAnimator.cancel();
507        }
508    }
509
510    public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) {
511        mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom;
512    }
513
514    public void setShadeExpanded(boolean shadeExpanded) {
515        mShadeExpanded = shadeExpanded;
516    }
517
518    public void setShelf(NotificationShelf shelf) {
519        mShelf = shelf;
520    }
521}
522