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