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.notification.stack;
18 
19 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR;
20 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_CYCLING_IN;
21 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_CYCLING_OUT;
22 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR;
23 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK;
24 
25 import android.animation.Animator;
26 import android.animation.AnimatorListenerAdapter;
27 import android.animation.ValueAnimator;
28 import android.content.Context;
29 import android.util.Property;
30 import android.view.View;
31 
32 import com.android.app.animation.Interpolators;
33 import com.android.internal.annotations.VisibleForTesting;
34 import com.android.systemui.res.R;
35 import com.android.systemui.shared.clocks.AnimatableClockView;
36 import com.android.systemui.statusbar.NotificationShelf;
37 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
38 import com.android.systemui.statusbar.notification.row.ExpandableView;
39 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
40 import com.android.systemui.statusbar.notification.shared.NotificationHeadsUpCycling;
41 import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation;
42 
43 import java.util.ArrayList;
44 import java.util.HashSet;
45 import java.util.Stack;
46 
47 /**
48  * An stack state animator which handles animations to new StackScrollStates
49  */
50 public class StackStateAnimator {
51 
52     public static final int ANIMATION_DURATION_STANDARD = 360;
53     public static final int ANIMATION_DURATION_CORNER_RADIUS = 200;
54     public static final int ANIMATION_DURATION_WAKEUP = 500;
55     public static final int ANIMATION_DURATION_WAKEUP_SCRIM = 667;
56     public static final int ANIMATION_DURATION_GO_TO_FULL_SHADE = 448;
57     public static final int ANIMATION_DURATION_APPEAR_DISAPPEAR = 464;
58     public static final int ANIMATION_DURATION_SWIPE = 200;
59     public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220;
60     public static final int ANIMATION_DURATION_CLOSE_REMOTE_INPUT = 150;
61     public static final int ANIMATION_DURATION_HEADS_UP_APPEAR = 400;
62     public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 400;
63     public static final int ANIMATION_DURATION_HEADS_UP_CYCLING = 400;
64     public static final int ANIMATION_DURATION_FOLD_TO_AOD =
65             AnimatableClockView.ANIMATION_DURATION_FOLD_TO_AOD;
66     public static final int ANIMATION_DURATION_PRIORITY_CHANGE = 500;
67     public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80;
68     public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32;
69     public static final int ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE = 48;
70     public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2;
71     private static final int MAX_STAGGER_COUNT = 5;
72 
73     @VisibleForTesting int mGoToFullShadeAppearingTranslation;
74     @VisibleForTesting float mHeadsUpAppearStartAboveScreen;
75     // Padding between the old and new heads up notifications for the hun cycling animation
76     private float mHeadsUpCyclingPadding;
77     private final ExpandableViewState mTmpState = new ExpandableViewState();
78     private final AnimationProperties mAnimationProperties;
79     public NotificationStackScrollLayout mHostLayout;
80     private ArrayList<NotificationStackScrollLayout.AnimationEvent> mNewEvents =
81             new ArrayList<>();
82     private ArrayList<View> mNewAddChildren = new ArrayList<>();
83     private HashSet<View> mHeadsUpAppearChildren = new HashSet<>();
84     private HashSet<View> mHeadsUpDisappearChildren = new HashSet<>();
85     private HashSet<Animator> mAnimatorSet = new HashSet<>();
86     private Stack<AnimatorListenerAdapter> mAnimationListenerPool = new Stack<>();
87     private AnimationFilter mAnimationFilter = new AnimationFilter();
88     private long mCurrentLength;
89     private long mCurrentAdditionalDelay;
90 
91     private ValueAnimator mTopOverScrollAnimator;
92     private ValueAnimator mBottomOverScrollAnimator;
93     private int mHeadsUpAppearHeightBottom;
94     private int mStackTopMargin;
95     private boolean mShadeExpanded;
96     private ArrayList<ExpandableView> mTransientViewsToRemove = new ArrayList<>();
97     private NotificationShelf mShelf;
98     private StackStateLogger mLogger;
99 
StackStateAnimator(Context context, NotificationStackScrollLayout hostLayout)100     public StackStateAnimator(Context context, NotificationStackScrollLayout hostLayout) {
101         mHostLayout = hostLayout;
102         initView(context);
103         mAnimationProperties = new AnimationProperties() {
104             @Override
105             public AnimationFilter getAnimationFilter() {
106                 return mAnimationFilter;
107             }
108 
109             @Override
110             public AnimatorListenerAdapter getAnimationFinishListener(Property property) {
111                 return getGlobalAnimationFinishedListener();
112             }
113 
114             @Override
115             public boolean wasAdded(View view) {
116                 return mNewAddChildren.contains(view);
117             }
118         };
119     }
120 
121     /**
122      * Needs to be called on configuration changes, to update cached resource values.
123      */
initView(Context context)124     public void initView(Context context) {
125         updateResources(context);
126     }
127 
updateResources(Context context)128     private void updateResources(Context context) {
129         mGoToFullShadeAppearingTranslation =
130                 context.getResources().getDimensionPixelSize(
131                         R.dimen.go_to_full_shade_appearing_translation);
132         mHeadsUpAppearStartAboveScreen = context.getResources()
133                 .getDimensionPixelSize(R.dimen.heads_up_appear_y_above_screen);
134         mHeadsUpCyclingPadding = context.getResources()
135                 .getDimensionPixelSize(R.dimen.heads_up_cycling_padding);
136     }
137 
setLogger(StackStateLogger logger)138     protected void setLogger(StackStateLogger logger) {
139         mLogger = logger;
140     }
141 
isRunning()142     public boolean isRunning() {
143         return !mAnimatorSet.isEmpty();
144     }
145 
startAnimationForEvents( ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents, long additionalDelay)146     public void startAnimationForEvents(
147             ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents,
148             long additionalDelay) {
149 
150         // Animation events might generate custom animations, which are started async
151         boolean anyCustomAnimationCreated = processAnimationEvents(mAnimationEvents);
152 
153         int childCount = mHostLayout.getChildCount();
154         mAnimationFilter.applyCombination(mNewEvents);
155         mCurrentAdditionalDelay = additionalDelay;
156         mCurrentLength = NotificationStackScrollLayout.AnimationEvent.combineLength(mNewEvents);
157         // Used to stagger concurrent animations' delays and durations for visual effect
158         int animationStaggerCount = 0;
159         for (int i = 0; i < childCount; i++) {
160             final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
161 
162             ExpandableViewState viewState = child.getViewState();
163             if (viewState == null || child.getVisibility() == View.GONE
164                     || applyWithoutAnimation(child, viewState)) {
165                 continue;
166             }
167 
168             if (mAnimationProperties.wasAdded(child) && animationStaggerCount < MAX_STAGGER_COUNT) {
169                 animationStaggerCount++;
170             }
171             initAnimationProperties(child, viewState, animationStaggerCount);
172             viewState.animateTo(child, mAnimationProperties);
173         }
174         if (!isRunning() && !anyCustomAnimationCreated) {
175             // no child has performed any animation or is about to animate, lets finish
176             onAnimationFinished();
177         }
178         mHeadsUpAppearChildren.clear();
179         mHeadsUpDisappearChildren.clear();
180         mNewEvents.clear();
181         mNewAddChildren.clear();
182         if (NotificationsImprovedHunAnimation.isEnabled()
183                 || NotificationHeadsUpCycling.isEnabled()) {
184             mAnimationProperties.resetCustomInterpolators();
185         }
186     }
187 
initAnimationProperties(ExpandableView child, ExpandableViewState viewState, int animationStaggerCount)188     private void initAnimationProperties(ExpandableView child,
189             ExpandableViewState viewState, int animationStaggerCount) {
190         boolean wasAdded = mAnimationProperties.wasAdded(child);
191         mAnimationProperties.duration = mCurrentLength;
192         adaptDurationWhenGoingToFullShade(child, viewState, wasAdded, animationStaggerCount);
193         mAnimationProperties.delay = 0;
194         if (wasAdded || mAnimationFilter.hasDelays
195                         && (viewState.getYTranslation() != child.getTranslationY()
196                         || viewState.getZTranslation() != child.getTranslationZ()
197                         || viewState.getAlpha() != child.getAlpha()
198                         || viewState.height != child.getActualHeight()
199                         || viewState.clipTopAmount != child.getClipTopAmount())) {
200             mAnimationProperties.delay = mCurrentAdditionalDelay
201                     + calculateChildAnimationDelay(viewState, animationStaggerCount);
202         }
203     }
204 
adaptDurationWhenGoingToFullShade(ExpandableView child, ExpandableViewState viewState, boolean wasAdded, int animationStaggerCount)205     private void adaptDurationWhenGoingToFullShade(ExpandableView child,
206             ExpandableViewState viewState, boolean wasAdded, int animationStaggerCount) {
207         boolean isDecorView = child instanceof StackScrollerDecorView;
208         boolean needsAdjustment = wasAdded || isDecorView;
209         if (needsAdjustment && mAnimationFilter.hasGoToFullShadeEvent) {
210             int startOffset = 0;
211             if (!isDecorView) {
212                 startOffset = mGoToFullShadeAppearingTranslation;
213                 float longerDurationFactor = (float) Math.pow(animationStaggerCount, 0.7f);
214                 mAnimationProperties.duration = ANIMATION_DURATION_APPEAR_DISAPPEAR + 50
215                         + (long) (100 * longerDurationFactor);
216             }
217             child.setTranslationY(viewState.getYTranslation() + startOffset);
218         }
219     }
220 
221     /**
222      * Determines if a view should not perform an animation and applies it directly.
223      *
224      * @return true if no animation should be performed
225      */
applyWithoutAnimation(ExpandableView child, ExpandableViewState viewState)226     private boolean applyWithoutAnimation(ExpandableView child, ExpandableViewState viewState) {
227         if (mShadeExpanded) {
228             return false;
229         }
230         if (ViewState.isAnimatingY(child)) {
231             // A Y translation animation is running
232             return false;
233         }
234         if (mHeadsUpDisappearChildren.contains(child) || mHeadsUpAppearChildren.contains(child)) {
235             // This is a heads up animation
236             return false;
237         }
238         if (NotificationStackScrollLayout.isPinnedHeadsUp(child)) {
239             // This is another headsUp which might move. Let's animate!
240             return false;
241         }
242         viewState.applyToView(child);
243         return true;
244     }
245 
calculateChildAnimationDelay(ExpandableViewState viewState, int animationStaggerCount)246     private long calculateChildAnimationDelay(ExpandableViewState viewState,
247             int animationStaggerCount) {
248         if (mAnimationFilter.hasGoToFullShadeEvent) {
249             return calculateDelayGoToFullShade(viewState, animationStaggerCount);
250         }
251         if (mAnimationFilter.customDelay != AnimationFilter.NO_DELAY) {
252             return mAnimationFilter.customDelay;
253         }
254         long minDelay = 0;
255         for (NotificationStackScrollLayout.AnimationEvent event : mNewEvents) {
256             long delayPerElement = ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING;
257             switch (event.animationType) {
258                 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD: {
259                     int ownIndex = viewState.notGoneIndex;
260                     int changingIndex =
261                             ((ExpandableView) (event.mChangingView)).getViewState().notGoneIndex;
262                     int difference = Math.abs(ownIndex - changingIndex);
263                     difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
264                             difference - 1));
265                     long delay = (DELAY_EFFECT_MAX_INDEX_DIFFERENCE - difference) * delayPerElement;
266                     minDelay = Math.max(delay, minDelay);
267                     break;
268                 }
269                 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT:
270                     delayPerElement = ANIMATION_DELAY_PER_ELEMENT_MANUAL;
271                 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE: {
272                     int ownIndex = viewState.notGoneIndex;
273                     boolean noNextView = event.viewAfterChangingView == null;
274                     ExpandableView viewAfterChangingView = noNextView
275                             ? mHostLayout.getLastChildNotGone()
276                             : (ExpandableView) event.viewAfterChangingView;
277                     if (viewAfterChangingView == null) {
278                         // This can happen when the last view in the list is removed.
279                         // Since the shelf is still around and the only view, the code still goes
280                         // in here and tries to calculate the delay for it when case its properties
281                         // have changed.
282                         continue;
283                     }
284                     int nextIndex = viewAfterChangingView.getViewState().notGoneIndex;
285                     if (ownIndex >= nextIndex) {
286                         // we only have the view afterwards
287                         ownIndex++;
288                     }
289                     int difference = Math.abs(ownIndex - nextIndex);
290                     difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
291                             difference - 1));
292                     long delay = difference * delayPerElement;
293                     minDelay = Math.max(delay, minDelay);
294                     break;
295                 }
296                 default:
297                     break;
298             }
299         }
300         return minDelay;
301     }
302 
calculateDelayGoToFullShade(ExpandableViewState viewState, int animationStaggerCount)303     private long calculateDelayGoToFullShade(ExpandableViewState viewState,
304             int animationStaggerCount) {
305         int shelfIndex = mShelf.getNotGoneIndex();
306         float index = viewState.notGoneIndex;
307         long result = 0;
308         if (index > shelfIndex) {
309             float diff = (float) Math.pow(animationStaggerCount, 0.7f);
310             result += (long) (diff * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE * 0.25);
311             index = shelfIndex;
312         }
313         index = (float) Math.pow(index, 0.7f);
314         result += (long) (index * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE);
315         return result;
316     }
317 
318     /**
319      * @return an adapter which ensures that onAnimationFinished is called once no animation is
320      *         running anymore
321      */
getGlobalAnimationFinishedListener()322     private AnimatorListenerAdapter getGlobalAnimationFinishedListener() {
323         if (!mAnimationListenerPool.empty()) {
324             return mAnimationListenerPool.pop();
325         }
326 
327         // We need to create a new one, no reusable ones found
328         return new AnimatorListenerAdapter() {
329             private boolean mWasCancelled;
330 
331             @Override
332             public void onAnimationEnd(Animator animation) {
333                 mAnimatorSet.remove(animation);
334                 if (mAnimatorSet.isEmpty() && !mWasCancelled) {
335                     onAnimationFinished();
336                 }
337                 mAnimationListenerPool.push(this);
338             }
339 
340             @Override
341             public void onAnimationCancel(Animator animation) {
342                 mWasCancelled = true;
343             }
344 
345             @Override
346             public void onAnimationStart(Animator animation) {
347                 mWasCancelled = false;
348                 mAnimatorSet.add(animation);
349             }
350         };
351     }
352 
onAnimationFinished()353     private void onAnimationFinished() {
354         mHostLayout.onChildAnimationFinished();
355 
356         for (ExpandableView transientViewToRemove : mTransientViewsToRemove) {
357             transientViewToRemove.removeFromTransientContainer();
358         }
359         mTransientViewsToRemove.clear();
360     }
361 
362     /**
363      * Process the animationEvents for a new animation. Here is the place to do something custom,
364      * like to modify the ViewState or to create a custom animation for an event.
365      *
366      *  @param animationEvents the animation events for the animation to perform
367      * @return true if any custom animation was created
368      */
processAnimationEvents( ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents)369     private boolean processAnimationEvents(
370             ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents) {
371         boolean needsCustomAnimation = false;
372         for (NotificationStackScrollLayout.AnimationEvent event : animationEvents) {
373             final ExpandableView changingView = event.mChangingView;
374             boolean loggable = false;
375             boolean isHeadsUp = false;
376             String key = null;
377             if (changingView instanceof ExpandableNotificationRow && mLogger != null) {
378                 loggable = true;
379                 isHeadsUp = ((ExpandableNotificationRow) changingView).isHeadsUp();
380                 key = ((ExpandableNotificationRow) changingView).getEntry().getKey();
381             }
382             if (event.animationType ==
383                     NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD) {
384 
385                 // This item is added, initialize its properties.
386                 ExpandableViewState viewState = changingView.getViewState();
387                 if (viewState == null || viewState.gone) {
388                     // The position for this child was never generated, let's continue.
389                     continue;
390                 }
391                 if (loggable && isHeadsUp) {
392                     mLogger.logHUNViewAppearingWithAddEvent(key);
393                 }
394                 viewState.applyToView(changingView);
395                 mNewAddChildren.add(changingView);
396 
397             } else if (event.animationType ==
398                     NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE) {
399                 int changingViewVisibility = changingView.getVisibility();
400                 if (loggable) {
401                     mLogger.processAnimationEventsRemoval(key, changingViewVisibility, isHeadsUp);
402                 }
403                 if (changingViewVisibility != View.VISIBLE) {
404                     changingView.removeFromTransientContainer();
405                     continue;
406                 }
407 
408                 // Find the amount to translate up. This is needed in order to understand the
409                 // direction of the remove animation (either downwards or upwards)
410                 // upwards by default
411                 float translationDirection = -1.0f;
412                 if (event.viewAfterChangingView != null) {
413                     float ownPosition = changingView.getTranslationY();
414                     if (changingView instanceof ExpandableNotificationRow
415                             && event.viewAfterChangingView instanceof ExpandableNotificationRow) {
416                         ExpandableNotificationRow changingRow =
417                                 (ExpandableNotificationRow) changingView;
418                         ExpandableNotificationRow nextRow =
419                                 (ExpandableNotificationRow) event.viewAfterChangingView;
420                         if (changingRow.isRemoved()
421                                 && changingRow.wasChildInGroupWhenRemoved()
422                                 && !nextRow.isChildInGroup()) {
423                             // the next row isn't actually a child from a group! Let's
424                             // compare absolute positions!
425                             ownPosition = changingRow.getTranslationWhenRemoved();
426                         }
427                     }
428                     int actualHeight = changingView.getActualHeight();
429                     // there was a view after this one, Approximate the distance the next child
430                     // travelled
431                     ExpandableViewState viewState =
432                             ((ExpandableView) event.viewAfterChangingView).getViewState();
433                     translationDirection = ((viewState.getYTranslation()
434                             - (ownPosition + actualHeight / 2.0f)) * 2 /
435                             actualHeight);
436                     translationDirection = Math.max(Math.min(translationDirection, 1.0f),-1.0f);
437 
438                 }
439                 Runnable postAnimation;
440                 Runnable startAnimation;
441                 if (loggable) {
442                     String finalKey = key;
443                     final boolean finalIsHeadsHp = isHeadsUp;
444                     startAnimation = () -> {
445                         mLogger.animationStart(finalKey, "ANIMATION_TYPE_REMOVE", finalIsHeadsHp);
446                         changingView.setInRemovalAnimation(true);
447                     };
448                     postAnimation = () -> {
449                         mLogger.animationEnd(finalKey, "ANIMATION_TYPE_REMOVE", finalIsHeadsHp);
450                         changingView.setInRemovalAnimation(false);
451                         changingView.removeFromTransientContainer();
452                     };
453                 } else {
454                     startAnimation = ()-> {
455                         changingView.setInRemovalAnimation(true);
456                     };
457                     postAnimation = () -> {
458                         changingView.setInRemovalAnimation(false);
459                         changingView.removeFromTransientContainer();
460                     };
461                 }
462                 changingView.performRemoveAnimation(ANIMATION_DURATION_APPEAR_DISAPPEAR,
463                         0 /* delay */, translationDirection, false /* isHeadsUpAppear */,
464                         startAnimation, postAnimation, getGlobalAnimationFinishedListener(),
465                         ExpandableView.ClipSide.BOTTOM);
466                 needsCustomAnimation = true;
467             } else if (event.animationType ==
468                 NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT) {
469                 boolean isFullySwipedOut = mHostLayout.isFullySwipedOut(changingView);
470                 if (loggable) {
471                     mLogger.processAnimationEventsRemoveSwipeOut(key, isFullySwipedOut, isHeadsUp);
472                 }
473                 if (isFullySwipedOut) {
474                     changingView.removeFromTransientContainer();
475                 }
476             } else if (event.animationType == NotificationStackScrollLayout
477                     .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) {
478                 ExpandableNotificationRow row = (ExpandableNotificationRow) event.mChangingView;
479                 row.prepareExpansionChanged();
480             } else if (event.animationType == ANIMATION_TYPE_HEADS_UP_CYCLING_IN) {
481                 mHeadsUpAppearChildren.add(changingView);
482 
483                 mTmpState.copyFrom(changingView.getViewState());
484                 mTmpState.setYTranslation(changingView.getViewState().getYTranslation()
485                         + getHeadsUpCyclingInYTranslationStart(event.headsUpFromBottom));
486                 mTmpState.applyToView(changingView);
487 
488                 // TODO(b/339519404): use a different interpolator
489                 Runnable onAnimationEnd = null;
490                 if (loggable) {
491                     // This only captures HEADS_UP_APPEAR animations, but HUNs can appear with
492                     // normal ADD animations, which would not be logged here.
493                     String finalKey = key;
494                     mLogger.logHUNViewAppearing(key);
495                     onAnimationEnd = () -> {
496                         mLogger.appearAnimationEnded(finalKey);
497                     };
498                 }
499                 changingView.performAddAnimation(0, ANIMATION_DURATION_HEADS_UP_CYCLING,
500                         /* isHeadsUpAppear= */ true, onAnimationEnd);
501             } else if (NotificationsImprovedHunAnimation.isEnabled()
502                     && (event.animationType == ANIMATION_TYPE_HEADS_UP_APPEAR)) {
503                 mHeadsUpAppearChildren.add(changingView);
504 
505                 mTmpState.copyFrom(changingView.getViewState());
506                 // translate the HUN in from the top, or the bottom of the screen
507                 mTmpState.setYTranslation(getHeadsUpYTranslationStart(event.headsUpFromBottom));
508                 // set the height and the initial position
509                 mTmpState.applyToView(changingView);
510                 mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y,
511                         Interpolators.FAST_OUT_SLOW_IN);
512 
513                 Runnable onAnimationEnd = null;
514                 if (loggable) {
515                     // This only captures HEADS_UP_APPEAR animations, but HUNs can appear with
516                     // normal ADD animations, which would not be logged here.
517                     String finalKey = key;
518                     mLogger.logHUNViewAppearing(key);
519                     onAnimationEnd = () -> mLogger.appearAnimationEnded(finalKey);
520                 }
521                 changingView.performAddAnimation(0, ANIMATION_DURATION_HEADS_UP_APPEAR,
522                         /* isHeadsUpAppear= */ true, onAnimationEnd);
523             } else if (event.animationType == ANIMATION_TYPE_HEADS_UP_CYCLING_OUT) {
524                 mHeadsUpDisappearChildren.add(changingView);
525                 Runnable endRunnable = null;
526                 mTmpState.copyFrom(changingView.getViewState());
527 
528                 if (changingView.getParent() == null) {
529                     // This notification was actually removed, so we need to add it
530                     // transiently
531                     mHostLayout.addTransientView(changingView, 0);
532                     changingView.setTransientContainer(mHostLayout);
533                     // TODO(b/316404716): remove the hard-coded height
534                     // StackScrollAlgorithm cannot find this view because it has been removed
535                     // from the NSSL. To correctly translate the view to the top or bottom of
536                     // the screen (where it animated from), we need to update its translation.
537                     mTmpState.setYTranslation(
538                             mTmpState.getYTranslation() + 10
539                     );
540                     endRunnable = changingView::removeFromTransientContainer;
541                 }
542 
543                 boolean needsAnimation = true;
544                 if (changingView instanceof ExpandableNotificationRow) {
545                     ExpandableNotificationRow row =
546                             (ExpandableNotificationRow) changingView;
547                     if (row.isDismissed()) {
548                         needsAnimation = false;
549                     }
550                 }
551                 if (needsAnimation) {
552                     // We need to add the global animation listener, since once no animations are
553                     // running anymore, the panel will instantly hide itself. We need to wait until
554                     // the animation is fully finished for this though.
555                     final Runnable tmpEndRunnable = endRunnable;
556                     Runnable postAnimation;
557                     Runnable startAnimation;
558                     if (loggable) {
559                         String finalKey1 = key;
560                         final boolean finalIsHeadsUp = isHeadsUp;
561                         final String type = "ANIMATION_TYPE_HEADS_UP_CYCLING_OUT";
562                         startAnimation = () -> {
563                             mLogger.animationStart(finalKey1, type, finalIsHeadsUp);
564                             changingView.setInRemovalAnimation(true);
565                         };
566                         postAnimation = () -> {
567                             mLogger.animationEnd(finalKey1, type, finalIsHeadsUp);
568                             changingView.setInRemovalAnimation(false);
569                             if (tmpEndRunnable != null) {
570                                 tmpEndRunnable.run();
571                             }
572 
573                         };
574                     } else {
575                         postAnimation = () -> {
576                             changingView.setInRemovalAnimation(false);
577                             if (tmpEndRunnable != null) {
578                                 tmpEndRunnable.run();
579                             }
580                         };
581                         startAnimation = () -> {
582                             changingView.setInRemovalAnimation(true);
583                         };
584                     }
585                     long removeAnimationDelay = changingView.performRemoveAnimation(
586                             ANIMATION_DURATION_HEADS_UP_CYCLING,
587                             /* delay= */ 0,
588                             // It's a shame that translationDirection isn't where we do the y
589                             // translation, the actual translation is in StackScrollAlgorithm.
590                             /* translationDirection= */ 0.0f,
591                             /* isHeadsUpAnimation= */ true,
592                             startAnimation, postAnimation,
593                             getGlobalAnimationFinishedListener(), ExpandableView.ClipSide.TOP);
594                     mAnimationProperties.delay += removeAnimationDelay;
595                     mAnimationProperties.duration = ANIMATION_DURATION_HEADS_UP_CYCLING;
596                     mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y,
597                             Interpolators.LINEAR);
598                     mAnimationProperties.getAnimationFilter().animateY = true;
599                     mTmpState.animateTo(changingView, mAnimationProperties);
600                     mAnimationProperties.resetCustomInterpolators();
601                 } else if (endRunnable != null) {
602                     endRunnable.run();
603                 }
604                 needsCustomAnimation |= needsAnimation;
605             } else if (event.animationType == ANIMATION_TYPE_HEADS_UP_APPEAR) {
606                 NotificationsImprovedHunAnimation.assertInLegacyMode();
607                 // This item is added, initialize its properties.
608                 ExpandableViewState viewState = changingView.getViewState();
609                 mTmpState.copyFrom(viewState);
610                 if (event.headsUpFromBottom) {
611                     mTmpState.setYTranslation(mHeadsUpAppearHeightBottom);
612                 } else {
613                     Runnable onAnimationEnd = null;
614                     if (loggable) {
615                         String finalKey = key;
616                         onAnimationEnd = () -> mLogger.appearAnimationEnded(finalKey);
617                     }
618                     changingView.performAddAnimation(0, ANIMATION_DURATION_HEADS_UP_APPEAR,
619                             true /* isHeadsUpAppear */, onAnimationEnd);
620                 }
621                 mHeadsUpAppearChildren.add(changingView);
622                 // this only captures HEADS_UP_APPEAR animations, but HUNs can appear with normal
623                 // ADD animations, which would not be logged here.
624                 if (loggable) {
625                     mLogger.logHUNViewAppearing(key);
626                 }
627 
628                 mTmpState.applyToView(changingView);
629             } else if (event.animationType == ANIMATION_TYPE_HEADS_UP_DISAPPEAR
630                     || event.animationType == ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK) {
631                 mHeadsUpDisappearChildren.add(changingView);
632                 Runnable endRunnable = null;
633                 mTmpState.copyFrom(changingView.getViewState());
634                 if (changingView.getParent() == null) {
635                     // This notification was actually removed, so we need to add it
636                     // transiently
637                     mHostLayout.addTransientView(changingView, 0);
638                     changingView.setTransientContainer(mHostLayout);
639                     if (NotificationsImprovedHunAnimation.isEnabled()) {
640                         // StackScrollAlgorithm cannot find this view because it has been removed
641                         // from the NSSL. To correctly translate the view to the top or bottom of
642                         // the screen (where it animated from), we need to update its translation.
643                         mTmpState.setYTranslation(
644                                 getHeadsUpYTranslationStart(event.headsUpFromBottom)
645                         );
646                     }
647                     endRunnable = changingView::removeFromTransientContainer;
648                 }
649 
650                 boolean needsAnimation = true;
651                 if (changingView instanceof ExpandableNotificationRow) {
652                     ExpandableNotificationRow row =
653                             (ExpandableNotificationRow) changingView;
654                     if (row.isDismissed()) {
655                         needsAnimation = false;
656                     }
657                 }
658                 if (needsAnimation) {
659                     // We need to add the global animation listener, since once no animations are
660                     // running anymore, the panel will instantly hide itself. We need to wait until
661                     // the animation is fully finished for this though.
662                     final Runnable tmpEndRunnable = endRunnable;
663                     Runnable postAnimation;
664                     Runnable startAnimation;
665                     if (loggable) {
666                         String finalKey1 = key;
667                         final boolean finalIsHeadsUp = isHeadsUp;
668                         final String type =
669                                 event.animationType == ANIMATION_TYPE_HEADS_UP_DISAPPEAR
670                                         ? "ANIMATION_TYPE_HEADS_UP_DISAPPEAR"
671                                         : "ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK";
672                         startAnimation = () -> {
673                             mLogger.animationStart(finalKey1, type, finalIsHeadsUp);
674                             changingView.setInRemovalAnimation(true);
675                         };
676                         postAnimation = () -> {
677                             mLogger.animationEnd(finalKey1, type, finalIsHeadsUp);
678                             changingView.setInRemovalAnimation(false);
679                             if (tmpEndRunnable != null) {
680                                 tmpEndRunnable.run();
681                             }
682                         };
683                     } else {
684                         startAnimation = () -> {
685                             changingView.setInRemovalAnimation(true);
686                         };
687                         postAnimation = () -> {
688                             changingView.setInRemovalAnimation(false);
689                             if (tmpEndRunnable != null) {
690                                 tmpEndRunnable.run();
691                             }
692                         };
693                     }
694                     long removeAnimationDelay = changingView.performRemoveAnimation(
695                             ANIMATION_DURATION_HEADS_UP_DISAPPEAR,
696                             0, 0.0f, true /* isHeadsUpAppear */,
697                             startAnimation, postAnimation,
698                             getGlobalAnimationFinishedListener(), ExpandableView.ClipSide.BOTTOM);
699                     mAnimationProperties.delay += removeAnimationDelay;
700                     if (NotificationsImprovedHunAnimation.isEnabled()) {
701                         mAnimationProperties.duration = ANIMATION_DURATION_HEADS_UP_DISAPPEAR;
702                         mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y,
703                                 Interpolators.FAST_OUT_SLOW_IN_REVERSE);
704                         mAnimationProperties.getAnimationFilter().animateY = true;
705                         mTmpState.animateTo(changingView, mAnimationProperties);
706                         mAnimationProperties.resetCustomInterpolators();
707                     }
708                 } else if (endRunnable != null) {
709                     endRunnable.run();
710                 }
711                 needsCustomAnimation |= needsAnimation;
712             }
713             mNewEvents.add(event);
714         }
715         return needsCustomAnimation;
716     }
717 
getHeadsUpYTranslationStart(boolean headsUpFromBottom)718     private float getHeadsUpYTranslationStart(boolean headsUpFromBottom) {
719         if (headsUpFromBottom) {
720             // start from the bottom of the screen
721             return mHeadsUpAppearHeightBottom + mHeadsUpAppearStartAboveScreen;
722         }
723         // start from the top of the screen
724         return -mStackTopMargin - mHeadsUpAppearStartAboveScreen;
725     }
726 
727     /**
728      * @param headsUpFromBottom Whether we are showing the HUNs at the bottom of the screen
729      * @return The start y translation of the HUN cycling in animation
730      */
getHeadsUpCyclingInYTranslationStart(boolean headsUpFromBottom)731     private float getHeadsUpCyclingInYTranslationStart(boolean headsUpFromBottom) {
732         if (headsUpFromBottom) {
733             // start from the bottom of the screen
734             return mHeadsUpAppearHeightBottom + mHeadsUpCyclingPadding;
735         }
736         // start from the top of the screen
737         return -mHeadsUpCyclingPadding;
738     }
739 
740     /**
741      * @param headsUpFromBottom Whether we are showing the HUNs at the bottom of the screen
742      * @param oldHunHeight Height of the old HUN
743      * @param newHunHeight Height of the new HUN
744      * @return The y translation target value of the HUN cycling out animation
745      */
getHeadsUpCyclingOutYTranslation( boolean headsUpFromBottom, int oldHunHeight, int newHunHeight )746     private float getHeadsUpCyclingOutYTranslation(
747             boolean headsUpFromBottom,
748             int oldHunHeight,
749             int newHunHeight
750     ) {
751         final float translationDistance = mHeadsUpCyclingPadding + newHunHeight - oldHunHeight;
752         if (headsUpFromBottom) {
753             // start from the bottom of the screen
754             return mHeadsUpAppearHeightBottom - translationDistance;
755         }
756         return translationDistance;
757     }
758 
animateOverScrollToAmount(float targetAmount, final boolean onTop, final boolean isRubberbanded)759     public void animateOverScrollToAmount(float targetAmount, final boolean onTop,
760             final boolean isRubberbanded) {
761         final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop);
762         if (targetAmount == startOverScrollAmount) {
763             return;
764         }
765         cancelOverScrollAnimators(onTop);
766         ValueAnimator overScrollAnimator = ValueAnimator.ofFloat(startOverScrollAmount,
767                 targetAmount);
768         overScrollAnimator.setDuration(ANIMATION_DURATION_STANDARD);
769         overScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
770             @Override
771             public void onAnimationUpdate(ValueAnimator animation) {
772                 float currentOverScroll = (float) animation.getAnimatedValue();
773                 mHostLayout.setOverScrollAmount(
774                         currentOverScroll, onTop, false /* animate */, false /* cancelAnimators */,
775                         isRubberbanded);
776             }
777         });
778         overScrollAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
779         overScrollAnimator.addListener(new AnimatorListenerAdapter() {
780             @Override
781             public void onAnimationEnd(Animator animation) {
782                 if (onTop) {
783                     mTopOverScrollAnimator = null;
784                 } else {
785                     mBottomOverScrollAnimator = null;
786                 }
787             }
788         });
789         overScrollAnimator.start();
790         if (onTop) {
791             mTopOverScrollAnimator = overScrollAnimator;
792         } else {
793             mBottomOverScrollAnimator = overScrollAnimator;
794         }
795     }
796 
cancelOverScrollAnimators(boolean onTop)797     public void cancelOverScrollAnimators(boolean onTop) {
798         ValueAnimator currentAnimator = onTop ? mTopOverScrollAnimator : mBottomOverScrollAnimator;
799         if (currentAnimator != null) {
800             currentAnimator.cancel();
801         }
802     }
803 
setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom)804     public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) {
805         mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom;
806     }
807 
setStackTopMargin(int stackTopMargin)808     public void setStackTopMargin(int stackTopMargin) {
809         mStackTopMargin = stackTopMargin;
810     }
811 
setShadeExpanded(boolean shadeExpanded)812     public void setShadeExpanded(boolean shadeExpanded) {
813         mShadeExpanded = shadeExpanded;
814     }
815 
setShelf(NotificationShelf shelf)816     public void setShelf(NotificationShelf shelf) {
817         mShelf = shelf;
818     }
819 }
820