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 
17 package com.android.systemui.statusbar.stack;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ValueAnimator;
22 import android.util.Property;
23 import android.view.View;
24 import android.view.ViewGroup;
25 import android.view.animation.Interpolator;
26 
27 import com.android.systemui.Interpolators;
28 import com.android.systemui.R;
29 import com.android.systemui.statusbar.ExpandableNotificationRow;
30 import com.android.systemui.statusbar.ExpandableView;
31 import com.android.systemui.statusbar.NotificationShelf;
32 
33 import java.util.ArrayList;
34 import java.util.HashSet;
35 import java.util.Stack;
36 
37 /**
38  * An stack state animator which handles animations to new StackScrollStates
39  */
40 public 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 
StackStateAnimator(NotificationStackScrollLayout hostLayout)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 
isRunning()111     public boolean isRunning() {
112         return !mAnimatorSet.isEmpty();
113     }
114 
startAnimationForEvents( ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents, StackScrollState finalState, long additionalDelay)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 
initAnimationProperties(StackScrollState finalState, ExpandableView child, ExpandableViewState viewState)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 
adaptDurationWhenGoingToFullShade(ExpandableView child, ExpandableViewState viewState, boolean wasAdded)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      */
applyWithoutAnimation(ExpandableView child, ExpandableViewState viewState, StackScrollState finalState)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 
findLastNotAddedIndex(StackScrollState finalState)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 
calculateChildAnimationDelay(ExpandableViewState viewState, StackScrollState finalState)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 
calculateDelayGoToFullShade(ExpandableViewState viewState)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      */
getGlobalAnimationFinishedListener()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 
onAnimationFinished()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      */
processAnimationEvents( ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents, StackScrollState finalState)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 
removeFromOverlay(View changingView)458     public static void removeFromOverlay(View changingView) {
459         ViewGroup parent = (ViewGroup) changingView.getParent();
460         if (parent != null) {
461             parent.removeView(changingView);
462         }
463     }
464 
animateOverScrollToAmount(float targetAmount, final boolean onTop, final boolean isRubberbanded)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 
cancelOverScrollAnimators(boolean onTop)503     public void cancelOverScrollAnimators(boolean onTop) {
504         ValueAnimator currentAnimator = onTop ? mTopOverScrollAnimator : mBottomOverScrollAnimator;
505         if (currentAnimator != null) {
506             currentAnimator.cancel();
507         }
508     }
509 
setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom)510     public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) {
511         mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom;
512     }
513 
setShadeExpanded(boolean shadeExpanded)514     public void setShadeExpanded(boolean shadeExpanded) {
515         mShadeExpanded = shadeExpanded;
516     }
517 
setShelf(NotificationShelf shelf)518     public void setShelf(NotificationShelf shelf) {
519         mShelf = shelf;
520     }
521 }
522