diff options
| author | 2024-04-15 17:43:17 +0000 | |
|---|---|---|
| committer | 2024-05-09 21:27:04 +0000 | |
| commit | 6b8f56f801c4680caea3ca55f5fb899c05a6772b (patch) | |
| tree | 78f178f03b8a7ae06252680f4011e6a11bb531bd | |
| parent | 80f02617dc6324f8fd97cd0b25794ab0a70c8ee4 (diff) | |
Heads up cycling animation
Introduce a new type of hun intro and outro animation - the hun
cycling animation. This animation is for the transition from the old
hun to the new hun when the avalanche notification feature is enabled,
and at least two huns are posted within a short period of time.
Bug: 316404716
Test: manual
Flag: ACONFIG notification_heads_up_cycling DEVELOPMENT
Change-Id: I9d1596075af2bef2b50e6b5ff07503647ee1b0ba
12 files changed, 379 insertions, 47 deletions
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index a1daebd7513e..5857692cdaa9 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -285,6 +285,9 @@ the amount by the view is positioned above the screen before the animation starts. --> <dimen name="heads_up_appear_y_above_screen">32dp</dimen> + <!-- padding between the old and new heads up notifications for the hun cycling animation --> + <dimen name="heads_up_cycling_padding">8dp</dimen> + <!-- padding between the heads up and the statusbar --> <dimen name="heads_up_status_bar_padding">8dp</dimen> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index 61cdea190a43..a85ad04e8d58 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.notification.row; import static com.android.systemui.Flags.notificationBackgroundTintOptimization; +import static com.android.systemui.statusbar.notification.row.ExpandableView.ClipSide.BOTTOM; +import static com.android.systemui.statusbar.notification.row.ExpandableView.ClipSide.TOP; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -42,6 +44,7 @@ import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.notification.FakeShadowView; import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.SourceType; +import com.android.systemui.statusbar.notification.shared.NotificationHeadsUpCycling; import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; @@ -353,12 +356,13 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView @Override public long performRemoveAnimation(long duration, long delay, float translationDirection, boolean isHeadsUpAnimation, Runnable onStartedRunnable, Runnable onFinishedRunnable, - AnimatorListenerAdapter animationListener) { + AnimatorListenerAdapter animationListener, ClipSide clipSide) { enableAppearDrawing(true); mIsHeadsUpAnimation = isHeadsUpAnimation; if (mDrawingAppearAnimation) { startAppearAnimation(false /* isAppearing */, translationDirection, - delay, duration, onStartedRunnable, onFinishedRunnable, animationListener); + delay, duration, onStartedRunnable, onFinishedRunnable, animationListener, + clipSide); } else { if (onStartedRunnable != null) { onStartedRunnable.run(); @@ -377,13 +381,13 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView mIsHeadsUpAnimation = isHeadsUpAppear; if (mDrawingAppearAnimation) { startAppearAnimation(true /* isAppearing */, isHeadsUpAppear ? 0.0f : -1.0f, delay, - duration, null, null, null); + duration, null, null, null, ClipSide.BOTTOM); } } private void startAppearAnimation(boolean isAppearing, float translationDirection, long delay, long duration, final Runnable onStartedRunnable, final Runnable onFinishedRunnable, - AnimatorListenerAdapter animationListener) { + AnimatorListenerAdapter animationListener, ClipSide clipSide) { mAnimationTranslationY = translationDirection * getActualHeight(); cancelAppearAnimation(); if (mAppearAnimationFraction == -1.0f) { @@ -405,9 +409,16 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView mCurrentAppearInterpolator = Interpolators.FAST_OUT_SLOW_IN_REVERSE; targetValue = 0.0f; } + + if (NotificationHeadsUpCycling.isEnabled()) { + // TODO(b/316404716): add avalanche filtering + mCurrentAppearInterpolator = Interpolators.LINEAR; + } + mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction, targetValue); - if (NotificationsImprovedHunAnimation.isEnabled()) { + if (NotificationsImprovedHunAnimation.isEnabled() + || NotificationHeadsUpCycling.isEnabled()) { mAppearAnimator.setInterpolator(mCurrentAppearInterpolator); } else { mAppearAnimator.setInterpolator(Interpolators.LINEAR); @@ -417,7 +428,12 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView mAppearAnimator.addUpdateListener(animation -> { mAppearAnimationFraction = (float) animation.getAnimatedValue(); updateAppearAnimationAlpha(); - updateAppearRect(); + if (NotificationHeadsUpCycling.isEnabled()) { + // For cycling out, we want the HUN to be clipped from the top. + updateAppearRect(clipSide); + } else { + updateAppearRect(); + } invalidate(); }); if (animationListener != null) { @@ -425,7 +441,11 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView } // we need to apply the initial state already to avoid drawn frames in the wrong state updateAppearAnimationAlpha(); - updateAppearRect(); + if (NotificationHeadsUpCycling.isEnabled()) { + updateAppearRect(clipSide); + } else { + updateAppearRect(); + } mAppearAnimator.addListener(new AnimatorListenerAdapter() { private boolean mRunWithoutInterruptions; @@ -507,14 +527,18 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView enableAppearDrawing(false); } - private void updateAppearRect() { + /** + * Update the View's Rect clipping to fit the appear animation + * @param clipSide Which side if view we want to clip from + */ + private void updateAppearRect(ClipSide clipSide) { float interpolatedFraction = - NotificationsImprovedHunAnimation.isEnabled() ? mAppearAnimationFraction + NotificationsImprovedHunAnimation.isEnabled() + || NotificationHeadsUpCycling.isEnabled() ? mAppearAnimationFraction : mCurrentAppearInterpolator.getInterpolation(mAppearAnimationFraction); mAppearAnimationTranslation = (1.0f - interpolatedFraction) * mAnimationTranslationY; - final int actualHeight = getActualHeight(); - float bottom = actualHeight * interpolatedFraction; - + final int fullHeight = getActualHeight(); + float height = fullHeight * interpolatedFraction; if (mTargetPoint != null) { int width = getWidth(); float fraction = 1 - mAppearAnimationFraction; @@ -523,13 +547,26 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView mAnimationTranslationY + (mAnimationTranslationY - mTargetPoint.y) * fraction, width - (width - mTargetPoint.x) * fraction, - actualHeight - (actualHeight - mTargetPoint.y) * fraction); + fullHeight - (fullHeight - mTargetPoint.y) * fraction); } else { - setOutlineRect(0, mAppearAnimationTranslation, getWidth(), - bottom + mAppearAnimationTranslation); + if (clipSide == TOP) { + setOutlineRect( + 0, + /* top= */ fullHeight - height, + getWidth(), + /* bottom= */ fullHeight + ); + } else if (clipSide == BOTTOM) { + setOutlineRect(0, mAppearAnimationTranslation, getWidth(), + height + mAppearAnimationTranslation); + } } } + private void updateAppearRect() { + updateAppearRect(ClipSide.BOTTOM); + } + private float getInterpolatedAppearAnimationFraction() { if (mAppearAnimationFraction >= 0) { @@ -539,11 +576,36 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView } private void updateAppearAnimationAlpha() { - float contentAlphaProgress = MathUtils.constrain(mAppearAnimationFraction, - ALPHA_APPEAR_START_FRACTION, ALPHA_APPEAR_END_FRACTION); - float range = ALPHA_APPEAR_END_FRACTION - ALPHA_APPEAR_START_FRACTION; - float alpha = (contentAlphaProgress - ALPHA_APPEAR_START_FRACTION) / range; - setContentAlpha(Interpolators.ALPHA_IN.getInterpolation(alpha)); + updateAppearAnimationContentAlpha( + mAppearAnimationFraction, + ALPHA_APPEAR_START_FRACTION, + ALPHA_APPEAR_END_FRACTION, + Interpolators.ALPHA_IN + ); + } + + /** + * Update the alpha value of the content view during the appear animation. We suppose that the + * content alpha changes from 0 to 1 during some part of the appear animation. + * @param appearFraction the current appearFraction, should be in the range of [0, 1], where + * 1 represents fully appeared + * @param startFraction the appear fraction when the content view should be + * * fully transparent + * @param endFraction the appear fraction when the content view should be + * fully in-transparent, should be greater or equals to startFraction + * @param interpolator the interpolator to update the alpha + */ + private void updateAppearAnimationContentAlpha( + float appearFraction, + float startFraction, + float endFraction, + Interpolator interpolator + ) { + float contentAlphaProgress = MathUtils.constrain(appearFraction, startFraction, + endFraction); + float range = endFraction - startFraction; + float alpha = (contentAlphaProgress - startFraction) / range; + setContentAlpha(interpolator.getInterpolation(alpha)); } private void setContentAlpha(float contentAlpha) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 5e3df7b5e60f..472362e907e1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -3069,7 +3069,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView boolean isHeadsUpAnimation, Runnable onStartedRunnable, Runnable onFinishedRunnable, - AnimatorListenerAdapter animationListener) { + AnimatorListenerAdapter animationListener, ClipSide clipSide) { if (mMenuRow != null && mMenuRow.isMenuVisible()) { Animator anim = getTranslateViewAnimator(0f, null /* listener */); if (anim != null) { @@ -3085,7 +3085,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView public void onAnimationEnd(Animator animation) { ExpandableNotificationRow.super.performRemoveAnimation( duration, delay, translationDirection, isHeadsUpAnimation, - null, onFinishedRunnable, animationListener); + null, onFinishedRunnable, animationListener, ClipSide.BOTTOM); } }); anim.start(); @@ -3093,7 +3093,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } return super.performRemoveAnimation(duration, delay, translationDirection, - isHeadsUpAnimation, onStartedRunnable, onFinishedRunnable, animationListener); + isHeadsUpAnimation, onStartedRunnable, onFinishedRunnable, animationListener, + clipSide); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java index 05e8717d0005..2af119f98f4a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java @@ -362,17 +362,17 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro /** * Perform a remove animation on this view. - * @param duration The duration of the remove animation. - * @param delay The delay of the animation + * + * @param duration The duration of the remove animation. + * @param delay The delay of the animation * @param translationDirection The direction value from [-1 ... 1] indicating in which the * animation should be performed. A value of -1 means that The * remove animation should be performed upwards, * such that the child appears to be going away to the top. 1 * Should mean the opposite. - * @param isHeadsUpAnimation Is this a headsUp animation. - * @param onFinishedRunnable A runnable which should be run when the animation is finished. - * @param animationListener An animation listener to add to the animation. - * + * @param isHeadsUpAnimation Is this a headsUp animation. + * @param onFinishedRunnable A runnable which should be run when the animation is finished. + * @param animationListener An animation listener to add to the animation. * @return The additional delay, in milliseconds, that this view needs to add before the * animation starts. */ @@ -380,7 +380,12 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro long delay, float translationDirection, boolean isHeadsUpAnimation, Runnable onStartedRunnable, Runnable onFinishedRunnable, - AnimatorListenerAdapter animationListener); + AnimatorListenerAdapter animationListener, ClipSide clipSide); + + public enum ClipSide { + TOP, + BOTTOM + } public void performAddAnimation(long delay, long duration, boolean isHeadsUpAppear) { performAddAnimation(delay, duration, isHeadsUpAppear, null); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java index 162e8af47394..291dc132686b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java @@ -252,7 +252,7 @@ public abstract class StackScrollerDecorView extends ExpandableView { float translationDirection, boolean isHeadsUpAnimation, Runnable onStartedRunnable, Runnable onFinishedRunnable, - AnimatorListenerAdapter animationListener) { + AnimatorListenerAdapter animationListener, ClipSide clipSide) { // TODO: Use duration if (onStartedRunnable != null) { onStartedRunnable.run(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationHeadsUpCycling.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationHeadsUpCycling.kt index 0344b32dd6ad..d4f8ea385667 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationHeadsUpCycling.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationHeadsUpCycling.kt @@ -33,7 +33,12 @@ object NotificationHeadsUpCycling { /** Is the heads-up cycling animation enabled */ @JvmStatic inline val isEnabled - get() = Flags.notificationContentAlphaOptimization() + get() = Flags.notificationHeadsUpCycling() + + /** Whether to animate the bottom line when transiting from a tall HUN to a short HUN */ + @JvmStatic + inline val animateTallToShort + get() = false /** * Called to ensure code is only run when the flag is enabled. This protects users from the diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java index e520957975f3..5f4e832f31a3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java @@ -293,6 +293,8 @@ public class AmbientState implements Dumpable { } String getAvalancheShowingHunKey() { + // If we don't have a previous showing hun, we don't consider the showing hun as avalanche + if (isNullAvalancheKey(getAvalanchePreviousHunKey())) return ""; return mAvalancheController.getShowingHunKey(); } @@ -300,6 +302,11 @@ public class AmbientState implements Dumpable { return mAvalancheController.getPreviousHunKey(); } + boolean isNullAvalancheKey(String key) { + if (key == null || key.isEmpty()) return true; + return key.equals("HeadsUpEntry null") || key.equals("HeadsUpEntry.mEntry null"); + } + void setOverExpansion(float overExpansion) { mOverExpansion = overExpansion; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt index 5551ab46262c..bd7bd596438a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt @@ -70,13 +70,14 @@ class MediaContainerView(context: Context, attrs: AttributeSet?) : ExpandableVie } override fun performRemoveAnimation( - duration: Long, - delay: Long, - translationDirection: Float, - isHeadsUpAnimation: Boolean, - onStartedRunnable: Runnable?, - onFinishedRunnable: Runnable?, - animationListener: AnimatorListenerAdapter? + duration: Long, + delay: Long, + translationDirection: Float, + isHeadsUpAnimation: Boolean, + onStartedRunnable: Runnable?, + onFinishedRunnable: Runnable?, + animationListener: AnimatorListenerAdapter?, + clipSide: ClipSide ): Long { return 0 } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 773a6bf752a6..6aacd1c04fdf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -112,6 +112,7 @@ import com.android.systemui.statusbar.notification.row.ActivatableNotificationVi import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.StackScrollerDecorView; +import com.android.systemui.statusbar.notification.shared.NotificationHeadsUpCycling; import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor; import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation; import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor; @@ -151,7 +152,6 @@ import java.util.function.Consumer; public class NotificationStackScrollLayout extends ViewGroup implements Dumpable, NotificationScrollView { - public static final float BACKGROUND_ALPHA_DIMMED = 0.7f; private static final String TAG = "StackScroller"; private static final boolean SPEW = Log.isLoggable(TAG, Log.VERBOSE); @@ -3143,6 +3143,11 @@ public class NotificationStackScrollLayout type = row.wasJustClicked() ? AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK : AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR; + if (NotificationHeadsUpCycling.isEnabled()) { + if (mStackScrollAlgorithm.isCyclingOut(row, mAmbientState)) { + type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_CYCLING_OUT; + } + } if (row.isChildInGroup()) { // We can otherwise get stuck in there if it was just isolated row.setHeadsUpAnimatingAway(false); @@ -3163,6 +3168,11 @@ public class NotificationStackScrollLayout if (pinnedAndClosed || shouldHunAppearFromTheBottom) { // Our custom add animation type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR; + if (NotificationHeadsUpCycling.isEnabled()) { + if (mStackScrollAlgorithm.isCyclingIn(row, mAmbientState)) { + type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_CYCLING_IN; + } + } } else { // Normal add animation type = AnimationEvent.ANIMATION_TYPE_ADD; @@ -6134,6 +6144,22 @@ public class NotificationStackScrollLayout .animateTopInset() .animateY() .animateZ(), + + // ANIMATION_TYPE_HEADS_UP_CYCLING_OUT + new AnimationFilter() + .animateHeight() + .animateTopInset() + .animateY() + .animateZ() + .hasDelays(), + + // ANIMATION_TYPE_HEADS_UP_CYCLING_IN + new AnimationFilter() + .animateHeight() + .animateTopInset() + .animateY() + .animateZ() + .hasDelays(), }; static int[] LENGTHS = new int[]{ @@ -6185,6 +6211,12 @@ public class NotificationStackScrollLayout // ANIMATION_TYPE_EVERYTHING StackStateAnimator.ANIMATION_DURATION_STANDARD, + + // ANIMATION_TYPE_HEADS_UP_CYCLING_OUT + StackStateAnimator.ANIMATION_DURATION_HEADS_UP_CYCLING, + + // ANIMATION_TYPE_HEADS_UP_CYCLING_IN + StackStateAnimator.ANIMATION_DURATION_HEADS_UP_CYCLING, }; static final int ANIMATION_TYPE_ADD = 0; @@ -6203,6 +6235,8 @@ public class NotificationStackScrollLayout static final int ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK = 13; static final int ANIMATION_TYPE_HEADS_UP_OTHER = 14; static final int ANIMATION_TYPE_EVERYTHING = 15; + static final int ANIMATION_TYPE_HEADS_UP_CYCLING_OUT = 16; + static final int ANIMATION_TYPE_HEADS_UP_CYCLING_IN = 17; final long eventStartTime; final ExpandableView mChangingView; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index d0cebae40c5a..0fcfc4b4b2c8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -38,6 +38,7 @@ import com.android.systemui.statusbar.notification.footer.ui.view.FooterView; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; +import com.android.systemui.statusbar.notification.shared.NotificationHeadsUpCycling; import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation; import java.util.ArrayList; @@ -75,6 +76,7 @@ public class StackScrollAlgorithm { private float mSmallCornerRadius; private float mLargeCornerRadius; private int mHeadsUpAppearHeightBottom; + private int mHeadsUpCyclingPadding; public StackScrollAlgorithm( Context context, @@ -99,6 +101,8 @@ public class StackScrollAlgorithm { R.dimen.heads_up_status_bar_padding); mHeadsUpAppearStartAboveScreen = res.getDimensionPixelSize( R.dimen.heads_up_appear_y_above_screen); + mHeadsUpCyclingPadding = context.getResources() + .getDimensionPixelSize(R.dimen.heads_up_cycling_padding); mPinnedZTranslationExtra = res.getDimensionPixelSize( R.dimen.heads_up_pinned_elevation); mGapHeight = res.getDimensionPixelSize(R.dimen.notification_section_divider_height); @@ -348,7 +352,8 @@ public class StackScrollAlgorithm { && !firstHeadsUp && (isHeadsUp || child.isHeadsUpAnimatingAway()) && newNotificationEnd > firstHeadsUpEnd - && !ambientState.isShadeExpanded()) { + && !ambientState.isShadeExpanded() + && !skipClipBottomForCycling(child, ambientState)) { // The bottom of this view is peeking out from under the previous view. // Clip the part that is peeking out. float overlapAmount = newNotificationEnd - firstHeadsUpEnd; @@ -370,6 +375,44 @@ public class StackScrollAlgorithm { } } + /** + * @return Should we skip clipping the bottom clipping when new hun has lower bottom line for + * the hun cycling animation. + */ + private boolean skipClipBottomForCycling(ExpandableView view, AmbientState ambientState) { + if (!NotificationHeadsUpCycling.isEnabled()) return false; + if (!isCyclingOut(view, ambientState)) return false; + // skip bottom clipping if we animate the bottom line + return NotificationHeadsUpCycling.getAnimateTallToShort(); + } + + /** + * Whether the view is the hun that is cycling out by the notification avalanche. + */ + public boolean isCyclingOut(ExpandableView view, AmbientState ambientState) { + if (!NotificationHeadsUpCycling.isEnabled()) return false; + if (!(view instanceof ExpandableNotificationRow)) return false; + return isCyclingOut((ExpandableNotificationRow) view, ambientState); + } + + /** + * Whether the row is the hun that is cycling out by the notification avalanche. + */ + public boolean isCyclingOut(ExpandableNotificationRow row, AmbientState ambientState) { + if (!NotificationHeadsUpCycling.isEnabled()) return false; + String cyclingOutKey = ambientState.getAvalanchePreviousHunKey(); + return row.getEntry().getKey().equals(cyclingOutKey); + } + + /** + * Whether the row is the hun that is cycling in by the notification avalanche. + */ + public boolean isCyclingIn(ExpandableNotificationRow row, AmbientState ambientState) { + if (!NotificationHeadsUpCycling.isEnabled()) return false; + String cyclingInKey = ambientState.getAvalancheShowingHunKey(); + return row.getEntry().getKey().equals(cyclingInKey); + } + /** Updates the dimmed and hiding sensitive states of the children. */ private void updateDimmedAndHideSensitive(AmbientState ambientState, StackScrollAlgorithmState algorithmState) { @@ -799,6 +842,7 @@ public class StackScrollAlgorithm { } ExpandableNotificationRow topHeadsUpEntry = null; + int cyclingInHunHeight = -1; for (int i = 0; i < childCount; i++) { View child = algorithmState.visibleChildren.get(i); if (!(child instanceof ExpandableNotificationRow row)) { @@ -839,6 +883,13 @@ public class StackScrollAlgorithm { childState.setYTranslation( Math.max(childState.getYTranslation(), headsUpTranslation)); childState.height = Math.max(row.getIntrinsicHeight(), childState.height); + if (NotificationHeadsUpCycling.isEnabled()) { + if (isCyclingIn(row, ambientState)) { + if (cyclingInHunHeight == -1) { + cyclingInHunHeight = childState.height; + } + } + } childState.hidden = false; ExpandableViewState topState = topHeadsUpEntry == null ? null : topHeadsUpEntry.getViewState(); @@ -860,6 +911,26 @@ public class StackScrollAlgorithm { } } if (row.isHeadsUpAnimatingAway()) { + if (NotificationHeadsUpCycling.isEnabled() && isCyclingOut(row, ambientState)) { + // If the two HUNs in the cycling animation have different heights, we need + // an extra y translation to align the animation. + int extraTranslation; + if (NotificationHeadsUpCycling.getAnimateTallToShort()) { + if (cyclingInHunHeight > 0) { + extraTranslation = cyclingInHunHeight - childState.height; + } else { + extraTranslation = 0; + } + } else { + extraTranslation = cyclingInHunHeight >= childState.height + ? cyclingInHunHeight - childState.height : 0; + } + extraTranslation += mHeadsUpCyclingPadding; + float inSpaceTranslation = Math.max(childState.getYTranslation(), + headsUpTranslation); + childState.setYTranslation(inSpaceTranslation + extraTranslation); + cyclingInHunHeight = -1; + } else if (NotificationsImprovedHunAnimation.isEnabled() && !ambientState.isDozing()) { if (shouldHunAppearFromBottom(ambientState, childState)) { // move to the bottom of the screen diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java index 5963d358443e..5dc544993ddc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.notification.stack; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR; +import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_CYCLING_IN; +import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_CYCLING_OUT; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK; @@ -57,6 +59,7 @@ public class StackStateAnimator { public static final int ANIMATION_DURATION_CLOSE_REMOTE_INPUT = 150; public static final int ANIMATION_DURATION_HEADS_UP_APPEAR = 400; public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 400; + public static final int ANIMATION_DURATION_HEADS_UP_CYCLING = 400; public static final int ANIMATION_DURATION_FOLD_TO_AOD = AnimatableClockView.ANIMATION_DURATION_FOLD_TO_AOD; public static final int ANIMATION_DURATION_PRIORITY_CHANGE = 500; @@ -68,6 +71,8 @@ public class StackStateAnimator { @VisibleForTesting int mGoToFullShadeAppearingTranslation; @VisibleForTesting float mHeadsUpAppearStartAboveScreen; + // Padding between the old and new heads up notifications for the hun cycling animation + private float mHeadsUpCyclingPadding; private final ExpandableViewState mTmpState = new ExpandableViewState(); private final AnimationProperties mAnimationProperties; public NotificationStackScrollLayout mHostLayout; @@ -125,6 +130,8 @@ public class StackStateAnimator { R.dimen.go_to_full_shade_appearing_translation); mHeadsUpAppearStartAboveScreen = context.getResources() .getDimensionPixelSize(R.dimen.heads_up_appear_y_above_screen); + mHeadsUpCyclingPadding = context.getResources() + .getDimensionPixelSize(R.dimen.heads_up_cycling_padding); } protected void setLogger(StackStateLogger logger) { @@ -449,7 +456,8 @@ public class StackStateAnimator { } changingView.performRemoveAnimation(ANIMATION_DURATION_APPEAR_DISAPPEAR, 0 /* delay */, translationDirection, false /* isHeadsUpAppear */, - startAnimation, postAnimation, getGlobalAnimationFinishedListener()); + startAnimation, postAnimation, getGlobalAnimationFinishedListener(), + ExpandableView.ClipSide.BOTTOM); needsCustomAnimation = true; } else if (event.animationType == NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT) { @@ -464,6 +472,27 @@ public class StackStateAnimator { .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) { ExpandableNotificationRow row = (ExpandableNotificationRow) event.mChangingView; row.prepareExpansionChanged(); + } else if (event.animationType == ANIMATION_TYPE_HEADS_UP_CYCLING_IN) { + mHeadsUpAppearChildren.add(changingView); + + mTmpState.copyFrom(changingView.getViewState()); + mTmpState.setYTranslation(changingView.getViewState().getYTranslation() + + getHeadsUpCyclingInYTranslationStart(event.headsUpFromBottom)); + mTmpState.applyToView(changingView); + + // TODO(b/339519404): use a different interpolator + Runnable onAnimationEnd = null; + if (loggable) { + // This only captures HEADS_UP_APPEAR animations, but HUNs can appear with + // normal ADD animations, which would not be logged here. + String finalKey = key; + mLogger.logHUNViewAppearing(key); + onAnimationEnd = () -> { + mLogger.appearAnimationEnded(finalKey); + }; + } + changingView.performAddAnimation(0, ANIMATION_DURATION_HEADS_UP_CYCLING, + /* isHeadsUpAppear= */ true, onAnimationEnd); } else if (NotificationsImprovedHunAnimation.isEnabled() && (event.animationType == ANIMATION_TYPE_HEADS_UP_APPEAR)) { mHeadsUpAppearChildren.add(changingView); @@ -486,6 +515,87 @@ public class StackStateAnimator { } changingView.performAddAnimation(0, ANIMATION_DURATION_HEADS_UP_APPEAR, /* isHeadsUpAppear= */ true, onAnimationEnd); + } else if (event.animationType == ANIMATION_TYPE_HEADS_UP_CYCLING_OUT) { + mHeadsUpDisappearChildren.add(changingView); + Runnable endRunnable = null; + mTmpState.copyFrom(changingView.getViewState()); + + if (changingView.getParent() == null) { + // This notification was actually removed, so we need to add it + // transiently + mHostLayout.addTransientView(changingView, 0); + changingView.setTransientContainer(mHostLayout); + // TODO(b/316404716): remove the hard-coded height + // StackScrollAlgorithm cannot find this view because it has been removed + // from the NSSL. To correctly translate the view to the top or bottom of + // the screen (where it animated from), we need to update its translation. + mTmpState.setYTranslation( + mTmpState.getYTranslation() + 10 + ); + endRunnable = changingView::removeFromTransientContainer; + } + + boolean needsAnimation = true; + if (changingView instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = + (ExpandableNotificationRow) changingView; + if (row.isDismissed()) { + needsAnimation = false; + } + } + if (needsAnimation) { + // We need to add the global animation listener, since once no animations are + // running anymore, the panel will instantly hide itself. We need to wait until + // the animation is fully finished for this though. + final Runnable tmpEndRunnable = endRunnable; + Runnable postAnimation; + Runnable startAnimation; + if (loggable) { + String finalKey1 = key; + final boolean finalIsHeadsUp = isHeadsUp; + final String type = "ANIMATION_TYPE_HEADS_UP_CYCLING_OUT"; + startAnimation = () -> { + mLogger.animationStart(finalKey1, type, finalIsHeadsUp); + changingView.setInRemovalAnimation(true); + }; + postAnimation = () -> { + mLogger.animationEnd(finalKey1, type, finalIsHeadsUp); + changingView.setInRemovalAnimation(false); + if (tmpEndRunnable != null) { + tmpEndRunnable.run(); + } + + }; + } else { + postAnimation = () -> { + changingView.setInRemovalAnimation(false); + if (tmpEndRunnable != null) { + tmpEndRunnable.run(); + } + }; + startAnimation = () -> { + changingView.setInRemovalAnimation(true); + }; + } + long removeAnimationDelay = changingView.performRemoveAnimation( + ANIMATION_DURATION_HEADS_UP_CYCLING, + /* delay= */ 0, + // It's a shame that translationDirection isn't where we do the y + // translation, the actual translation is in StackScrollAlgorithm. + /* translationDirection= */ 0.0f, + /* isHeadsUpAnimation= */ true, + startAnimation, postAnimation, + getGlobalAnimationFinishedListener(), ExpandableView.ClipSide.TOP); + mAnimationProperties.delay += removeAnimationDelay; + mAnimationProperties.duration = ANIMATION_DURATION_HEADS_UP_CYCLING; + mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y, + Interpolators.LINEAR); + mAnimationProperties.getAnimationFilter().animateY = true; + mTmpState.animateTo(changingView, mAnimationProperties); + } else if (endRunnable != null) { + endRunnable.run(); + } + needsCustomAnimation |= needsAnimation; } else if (event.animationType == ANIMATION_TYPE_HEADS_UP_APPEAR) { NotificationsImprovedHunAnimation.assertInLegacyMode(); // This item is added, initialize its properties. @@ -565,21 +675,21 @@ public class StackStateAnimator { } }; } else { + startAnimation = () -> { + changingView.setInRemovalAnimation(true); + }; postAnimation = () -> { changingView.setInRemovalAnimation(false); if (tmpEndRunnable != null) { tmpEndRunnable.run(); } }; - startAnimation = () -> { - changingView.setInRemovalAnimation(true); - }; } long removeAnimationDelay = changingView.performRemoveAnimation( ANIMATION_DURATION_HEADS_UP_DISAPPEAR, 0, 0.0f, true /* isHeadsUpAppear */, startAnimation, postAnimation, - getGlobalAnimationFinishedListener()); + getGlobalAnimationFinishedListener(), ExpandableView.ClipSide.BOTTOM); mAnimationProperties.delay += removeAnimationDelay; if (NotificationsImprovedHunAnimation.isEnabled()) { mAnimationProperties.duration = ANIMATION_DURATION_HEADS_UP_DISAPPEAR; @@ -607,6 +717,38 @@ public class StackStateAnimator { return -mStackTopMargin - mHeadsUpAppearStartAboveScreen; } + /** + * @param headsUpFromBottom Whether we are showing the HUNs at the bottom of the screen + * @return The start y translation of the HUN cycling in animation + */ + private float getHeadsUpCyclingInYTranslationStart(boolean headsUpFromBottom) { + if (headsUpFromBottom) { + // start from the bottom of the screen + return mHeadsUpAppearHeightBottom + mHeadsUpCyclingPadding; + } + // start from the top of the screen + return -mHeadsUpCyclingPadding; + } + + /** + * @param headsUpFromBottom Whether we are showing the HUNs at the bottom of the screen + * @param oldHunHeight Height of the old HUN + * @param newHunHeight Height of the new HUN + * @return The y translation target value of the HUN cycling out animation + */ + private float getHeadsUpCyclingOutYTranslation( + boolean headsUpFromBottom, + int oldHunHeight, + int newHunHeight + ) { + final float translationDistance = mHeadsUpCyclingPadding + newHunHeight - oldHunHeight; + if (headsUpFromBottom) { + // start from the bottom of the screen + return mHeadsUpAppearHeightBottom - translationDistance; + } + return translationDistance; + } + public void animateOverScrollToAmount(float targetAmount, final boolean onTop, final boolean isRubberbanded) { final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt index 4f0f91a7ee56..926c35f32967 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt @@ -134,7 +134,8 @@ class StackStateAnimatorTest : SysuiTestCase() { /* isHeadsUpAnimation= */ eq(true), /* onStartedRunnable= */ any(), /* onFinishedRunnable= */ runnableCaptor.capture(), - /* animationListener= */ any() + /* animationListener= */ any(), + /* clipSide= */ eq(ExpandableView.ClipSide.BOTTOM), ) animatorTestRule.advanceTimeBy(disappearDuration) // move to the end of SSA animations |