diff options
7 files changed, 636 insertions, 260 deletions
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index b9478018d7d3..f85b57b5c3c8 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -274,6 +274,10 @@ <!-- Side padding on the side of notifications --> <dimen name="notification_side_paddings">16dp</dimen> + <!-- Starting translateY offset of the HUN appear and disappear animations. Indicates + 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 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 a9b071df1135..fca527f5fc4d 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 @@ -31,7 +31,6 @@ import android.view.Choreographer; import android.view.MotionEvent; import android.view.View; import android.view.animation.Interpolator; -import android.view.animation.PathInterpolator; import com.android.app.animation.Interpolators; import com.android.internal.jank.InteractionJankMonitor; @@ -68,7 +67,8 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView * The content of the view should start showing at animation progress value of * #ALPHA_APPEAR_START_FRACTION. */ - private static final float ALPHA_APPEAR_START_FRACTION = .4f; + + private static final float ALPHA_APPEAR_START_FRACTION = .7f; /** * The content should show fully with progress at #ALPHA_APPEAR_END_FRACTION * The start of the animation is at #ALPHA_APPEAR_START_FRACTION @@ -87,9 +87,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView */ private boolean mActivated; - private final Interpolator mSlowOutFastInInterpolator; private Interpolator mCurrentAppearInterpolator; - NotificationBackgroundView mBackgroundNormal; private float mAnimationTranslationY; private boolean mDrawingAppearAnimation; @@ -117,7 +115,6 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView public ActivatableNotificationView(Context context, AttributeSet attrs) { super(context, attrs); - mSlowOutFastInInterpolator = new PathInterpolator(0.8f, 0.0f, 0.6f, 1.0f); setClipChildren(false); setClipToPadding(false); updateColors(); @@ -401,12 +398,16 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView mCurrentAppearInterpolator = Interpolators.FAST_OUT_SLOW_IN; targetValue = 1.0f; } else { - mCurrentAppearInterpolator = mSlowOutFastInInterpolator; + mCurrentAppearInterpolator = Interpolators.FAST_OUT_SLOW_IN_REVERSE; targetValue = 0.0f; } mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction, targetValue); - mAppearAnimator.setInterpolator(Interpolators.LINEAR); + if (NotificationsImprovedHunAnimation.isEnabled()) { + mAppearAnimator.setInterpolator(mCurrentAppearInterpolator); + } else { + mAppearAnimator.setInterpolator(Interpolators.LINEAR); + } mAppearAnimator.setDuration( (long) (duration * Math.abs(mAppearAnimationFraction - targetValue))); mAppearAnimator.addUpdateListener(animation -> { @@ -503,8 +504,9 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView } private void updateAppearRect() { - float interpolatedFraction = mCurrentAppearInterpolator.getInterpolation( - mAppearAnimationFraction); + float interpolatedFraction = + NotificationsImprovedHunAnimation.isEnabled() ? mAppearAnimationFraction + : mCurrentAppearInterpolator.getInterpolation(mAppearAnimationFraction); mAppearAnimationTranslation = (1.0f - interpolatedFraction) * mAnimationTranslationY; final int actualHeight = getActualHeight(); float bottom = actualHeight * interpolatedFraction; @@ -525,6 +527,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView } private float getInterpolatedAppearAnimationFraction() { + if (mAppearAnimationFraction >= 0) { return mCurrentAppearInterpolator.getInterpolation(mAppearAnimationFraction); } 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 3bbdfd164ba7..b7373568d71c 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 @@ -115,6 +115,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.NotificationsImprovedHunAnimation; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; import com.android.systemui.statusbar.phone.HeadsUpTouchHelper; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; @@ -3328,8 +3329,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable logHunAnimationSkipped(row, "row has no viewState"); continue; } + boolean shouldHunAppearFromTheBottom = + mStackScrollAlgorithm.shouldHunAppearFromBottom(mAmbientState, viewState); if (isHeadsUp && (mAddedHeadsUpChildren.contains(row) || pinnedAndClosed)) { - if (pinnedAndClosed || shouldHunAppearFromBottom(viewState)) { + if (pinnedAndClosed || shouldHunAppearFromTheBottom) { // Our custom add animation type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR; } else { @@ -3341,6 +3344,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } AnimationEvent event = new AnimationEvent(row, type); event.headsUpFromBottom = onBottom; + if (NotificationsImprovedHunAnimation.isEnabled()) { + // TODO(b/283084712) remove this with the flag and update the HUN filters at + // creation + event.filter.animateHeight = false; + } mAnimationEvents.add(event); if (SPEW) { Log.v(TAG, "Generating HUN animation event: " @@ -3355,11 +3363,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mAddedHeadsUpChildren.clear(); } - private boolean shouldHunAppearFromBottom(ExpandableViewState viewState) { - return viewState.getYTranslation() + viewState.height - >= mAmbientState.getMaxHeadsUpTranslation(); - } - private void generateGroupExpansionEvent() { // Generate a group expansion/collapsing event if there is such a group at all if (mExpandedGroupView != null) { @@ -4932,7 +4935,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable */ public void setHeadsUpBoundaries(int height, int bottomBarHeight) { mAmbientState.setMaxHeadsUpTranslation(height - bottomBarHeight); + mStackScrollAlgorithm.setHeadsUpAppearHeightBottom(height); mStateAnimator.setHeadsUpAppearHeightBottom(height); + mStateAnimator.setStackTopMargin(mAmbientState.getStackTopMargin()); requestChildrenUpdate(); } 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 06ca9a50bb6d..c4e6b909d023 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 @@ -37,6 +37,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.NotificationsImprovedHunAnimation; import java.util.ArrayList; import java.util.List; @@ -66,12 +67,15 @@ public class StackScrollAlgorithm { private boolean mClipNotificationScrollToTop; @VisibleForTesting float mHeadsUpInset; + @VisibleForTesting + float mHeadsUpAppearStartAboveScreen; private int mPinnedZTranslationExtra; private float mNotificationScrimPadding; private int mMarginBottom; private float mQuickQsOffsetHeight; private float mSmallCornerRadius; private float mLargeCornerRadius; + private int mHeadsUpAppearHeightBottom; public StackScrollAlgorithm( Context context, @@ -94,6 +98,8 @@ public class StackScrollAlgorithm { int statusBarHeight = SystemBarUtils.getStatusBarHeight(context); mHeadsUpInset = statusBarHeight + res.getDimensionPixelSize( R.dimen.heads_up_status_bar_padding); + mHeadsUpAppearStartAboveScreen = res.getDimensionPixelSize( + R.dimen.heads_up_appear_y_above_screen); mPinnedZTranslationExtra = res.getDimensionPixelSize( R.dimen.heads_up_pinned_elevation); mGapHeight = res.getDimensionPixelSize(R.dimen.notification_section_divider_height); @@ -221,6 +227,25 @@ public class StackScrollAlgorithm { return getExpansionFractionWithoutShelf(mTempAlgorithmState, ambientState); } + public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) { + mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom; + } + + /** + * If the QuickSettings is showing full screen, we want to animate the HeadsUp Notifications + * from the bottom of the screen. + * + * @param ambientState Current ambient state. + * @param viewState The state of the HUN that is being queried to appear from the bottom. + * + * @return true if the HeadsUp Notifications should appear from the bottom + */ + public boolean shouldHunAppearFromBottom(AmbientState ambientState, + ExpandableViewState viewState) { + return viewState.getYTranslation() + viewState.height + >= ambientState.getMaxHeadsUpTranslation(); + } + public static void log(String s) { if (DEBUG) { android.util.Log.i(TAG, s); @@ -793,10 +818,16 @@ public class StackScrollAlgorithm { } } if (row.isPinned()) { - // Make sure row yTranslation is at maximum the HUN yTranslation, - // which accounts for AmbientState.stackTopMargin in split-shade. - childState.setYTranslation( - Math.max(childState.getYTranslation(), headsUpTranslation)); + if (NotificationsImprovedHunAnimation.isEnabled()) { + // Make sure row yTranslation is at the HUN yTranslation, + // which accounts for AmbientState.stackTopMargin in split-shade. + childState.setYTranslation(headsUpTranslation); + } else { + // Make sure row yTranslation is at maximum the HUN yTranslation, + // which accounts for AmbientState.stackTopMargin in split-shade. + childState.setYTranslation( + Math.max(childState.getYTranslation(), headsUpTranslation)); + } childState.height = Math.max(row.getIntrinsicHeight(), childState.height); childState.hidden = false; ExpandableViewState topState = @@ -819,10 +850,22 @@ public class StackScrollAlgorithm { } } if (row.isHeadsUpAnimatingAway()) { - // Make sure row yTranslation is at maximum the HUN yTranslation, - // which accounts for AmbientState.stackTopMargin in split-shade. - childState.setYTranslation( - Math.max(childState.getYTranslation(), headsUpTranslation)); + if (NotificationsImprovedHunAnimation.isEnabled()) { + if (shouldHunAppearFromBottom(ambientState, childState)) { + // move to the bottom of the screen + childState.setYTranslation( + mHeadsUpAppearHeightBottom + mHeadsUpAppearStartAboveScreen); + } else { + // move to the top of the screen + childState.setYTranslation(-ambientState.getStackTopMargin() + - mHeadsUpAppearStartAboveScreen); + } + } else { + // Make sure row yTranslation is at maximum the HUN yTranslation, + // which accounts for AmbientState.stackTopMargin in split-shade. + childState.setYTranslation( + Math.max(childState.getYTranslation(), headsUpTranslation)); + } // keep it visible for the animation childState.hidden = false; } 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 e94258f416ac..a3e09417b34c 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 @@ -16,6 +16,7 @@ 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_DISAPPEAR; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK; @@ -26,6 +27,7 @@ import android.util.Property; import android.view.View; import com.android.app.animation.Interpolators; +import com.android.internal.annotations.VisibleForTesting; import com.android.keyguard.KeyguardSliceView; import com.android.systemui.res.R; import com.android.systemui.shared.clocks.AnimatableClockView; @@ -33,6 +35,7 @@ import com.android.systemui.statusbar.NotificationShelf; 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.NotificationsImprovedHunAnimation; import java.util.ArrayList; import java.util.HashSet; @@ -68,6 +71,8 @@ public class StackStateAnimator { private final int mGoToFullShadeAppearingTranslation; private final int mPulsingAppearingTranslation; + @VisibleForTesting + float mHeadsUpAppearStartAboveScreen; private final ExpandableViewState mTmpState = new ExpandableViewState(); private final AnimationProperties mAnimationProperties; public NotificationStackScrollLayout mHostLayout; @@ -85,21 +90,23 @@ public class StackStateAnimator { private ValueAnimator mTopOverScrollAnimator; private ValueAnimator mBottomOverScrollAnimator; private int mHeadsUpAppearHeightBottom; + private int mStackTopMargin; private boolean mShadeExpanded; private ArrayList<ExpandableView> mTransientViewsToRemove = new ArrayList<>(); private NotificationShelf mShelf; - private float mStatusBarIconLocation; - private int[] mTmpLocation = new int[2]; private StackStateLogger mLogger; public StackStateAnimator(NotificationStackScrollLayout hostLayout) { mHostLayout = hostLayout; + // TODO(b/317061579) reload on configuration changes mGoToFullShadeAppearingTranslation = hostLayout.getContext().getResources().getDimensionPixelSize( R.dimen.go_to_full_shade_appearing_translation); mPulsingAppearingTranslation = hostLayout.getContext().getResources().getDimensionPixelSize( R.dimen.pulsing_notification_appear_translation); + mHeadsUpAppearStartAboveScreen = hostLayout.getContext().getResources() + .getDimensionPixelSize(R.dimen.heads_up_appear_y_above_screen); mAnimationProperties = new AnimationProperties() { @Override public AnimationFilter getAnimationFilter() { @@ -455,8 +462,37 @@ public class StackStateAnimator { .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) { ExpandableNotificationRow row = (ExpandableNotificationRow) event.mChangingView; row.prepareExpansionChanged(); - } else if (event.animationType == NotificationStackScrollLayout - .AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) { + } else if (NotificationsImprovedHunAnimation.isEnabled() + && (event.animationType == ANIMATION_TYPE_HEADS_UP_APPEAR)) { + mHeadsUpAppearChildren.add(changingView); + + mTmpState.copyFrom(changingView.getViewState()); + if (event.headsUpFromBottom) { + // start from the bottom of the screen + mTmpState.setYTranslation( + mHeadsUpAppearHeightBottom + mHeadsUpAppearStartAboveScreen); + } else { + // start from the top of the screen + mTmpState.setYTranslation( + -mStackTopMargin - mHeadsUpAppearStartAboveScreen); + } + // set the height and the initial position + mTmpState.applyToView(changingView); + mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y, + Interpolators.FAST_OUT_SLOW_IN); + + 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_APPEAR, + /* isHeadsUpAppear= */ true, onAnimationEnd); + } else if (event.animationType == ANIMATION_TYPE_HEADS_UP_APPEAR) { + NotificationsImprovedHunAnimation.assertInLegacyMode(); // This item is added, initialize its properties. ExpandableViewState viewState = changingView.getViewState(); mTmpState.copyFrom(viewState); @@ -536,6 +572,10 @@ public class StackStateAnimator { changingView.setInRemovalAnimation(true); }; } + if (NotificationsImprovedHunAnimation.isEnabled()) { + mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y, + Interpolators.FAST_OUT_SLOW_IN_REVERSE); + } long removeAnimationDelay = changingView.performRemoveAnimation( ANIMATION_DURATION_HEADS_UP_DISAPPEAR, 0, 0.0f, true /* isHeadsUpAppear */, @@ -601,6 +641,10 @@ public class StackStateAnimator { mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom; } + public void setStackTopMargin(int stackTopMargin) { + mStackTopMargin = stackTopMargin; + } + public void setShadeExpanded(boolean shadeExpanded) { mShadeExpanded = shadeExpanded; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt index 08ef47765174..f266f039958f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt @@ -2,21 +2,28 @@ package com.android.systemui.statusbar.notification.stack import android.annotation.DimenRes import android.content.pm.PackageManager +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ShadeInterpolation.getContentAlpha import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.res.R import com.android.systemui.shade.transition.LargeScreenShadeInterpolator import com.android.systemui.statusbar.EmptyShadeView import com.android.systemui.statusbar.NotificationShelf import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.notification.RoundableState +import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.footer.ui.view.FooterView import com.android.systemui.statusbar.notification.footer.ui.view.FooterView.FooterViewState import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView +import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.util.mockito.mock import com.google.common.truth.Expect @@ -24,6 +31,7 @@ import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue +import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Assume import org.junit.Before import org.junit.Rule @@ -37,22 +45,26 @@ import org.mockito.Mockito.`when` as whenever @SmallTest class StackScrollAlgorithmTest : SysuiTestCase() { - @JvmField @Rule - var expect: Expect = Expect.create() + @JvmField @Rule var expect: Expect = Expect.create() private val largeScreenShadeInterpolator = mock<LargeScreenShadeInterpolator>() private val hostView = FrameLayout(context) private val stackScrollAlgorithm = StackScrollAlgorithm(context, hostView) private val notificationRow = mock<ExpandableNotificationRow>() + private val notificationEntry = mock<NotificationEntry>() private val dumpManager = mock<DumpManager>() + @OptIn(ExperimentalCoroutinesApi::class) private val mStatusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>() private val notificationShelf = mock<NotificationShelf>() - private val emptyShadeView = EmptyShadeView(context, /* attrs= */ null).apply { - layout(/* l= */ 0, /* t= */ 0, /* r= */ 100, /* b= */ 100) - } - private val footerView = FooterView(context, /*attrs=*/null) - private val ambientState = AmbientState( + private val emptyShadeView = + EmptyShadeView(context, /* attrs= */ null).apply { + layout(/* l= */ 0, /* t= */ 0, /* r= */ 100, /* b= */ 100) + } + private val footerView = FooterView(context, /*attrs=*/ null) + @OptIn(ExperimentalCoroutinesApi::class) + private val ambientState = + AmbientState( context, dumpManager, /* sectionProvider */ { _, _ -> false }, @@ -62,13 +74,14 @@ class StackScrollAlgorithmTest : SysuiTestCase() { ) private val testableResources = mContext.getOrCreateTestableResources() + private val featureFlags = mock<FeatureFlagsClassic>() private val maxPanelHeight = mContext.resources.displayMetrics.heightPixels - - px(R.dimen.notification_panel_margin_top) - - px(R.dimen.notification_panel_margin_bottom) + px(R.dimen.notification_panel_margin_top) - + px(R.dimen.notification_panel_margin_bottom) private fun px(@DimenRes id: Int): Float = - testableResources.resources.getDimensionPixelSize(id).toFloat() + testableResources.resources.getDimensionPixelSize(id).toFloat() private val bigGap = px(R.dimen.notification_section_divider_height) private val smallGap = px(R.dimen.notification_section_divider_height_lockscreen) @@ -76,9 +89,12 @@ class StackScrollAlgorithmTest : SysuiTestCase() { @Before fun setUp() { Assume.assumeFalse(isTv()) - + mDependency.injectTestDependency(FeatureFlags::class.java, featureFlags) whenever(notificationShelf.viewState).thenReturn(ExpandableViewState()) whenever(notificationRow.viewState).thenReturn(ExpandableViewState()) + whenever(notificationRow.entry).thenReturn(notificationEntry) + whenever(notificationRow.roundableState) + .thenReturn(RoundableState(notificationRow, notificationRow, 0f)) ambientState.isSmallScreen = true hostView.addView(notificationRow) @@ -92,7 +108,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { fun resetViewStates_defaultHun_yTranslationIsInset() { whenever(notificationRow.isPinned).thenReturn(true) whenever(notificationRow.isHeadsUp).thenReturn(true) - resetViewStates_hunYTranslationIsInset() + resetViewStates_hunYTranslationIs(stackScrollAlgorithm.mHeadsUpInset) } @Test @@ -103,18 +119,87 @@ class StackScrollAlgorithmTest : SysuiTestCase() { } @Test + @DisableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) fun resetViewStates_hunAnimatingAway_yTranslationIsInset() { whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true) - resetViewStates_hunYTranslationIsInset() + resetViewStates_hunYTranslationIs(stackScrollAlgorithm.mHeadsUpInset) } @Test + @DisableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) fun resetViewStates_hunAnimatingAway_StackMarginChangesHunYTranslation() { whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true) resetViewStates_stackMargin_changesHunYTranslation() } @Test + @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) + fun resetViewStates_defaultHun_newHeadsUpAnim_yTranslationIsInset() { + whenever(notificationRow.isPinned).thenReturn(true) + whenever(notificationRow.isHeadsUp).thenReturn(true) + resetViewStates_hunYTranslationIs(stackScrollAlgorithm.mHeadsUpInset) + } + + @Test + @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) + fun resetViewStates_defaultHunWithStackMargin_newHeadsUpAnim_changesHunYTranslation() { + whenever(notificationRow.isPinned).thenReturn(true) + whenever(notificationRow.isHeadsUp).thenReturn(true) + resetViewStates_stackMargin_changesHunYTranslation() + } + + @Test + @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) + fun resetViewStates_defaultHun_showingQS_newHeadsUpAnim_hunTranslatedToMax() { + // Given: the shade is open and scrolled to the bottom to show the QuickSettings + val maxHunTranslation = 2000f + ambientState.maxHeadsUpTranslation = maxHunTranslation + ambientState.setLayoutMinHeight(2500) // Mock the height of shade + ambientState.stackY = 2500f // Scroll over the max translation + stackScrollAlgorithm.setIsExpanded(true) // Mark the shade open + whenever(notificationRow.mustStayOnScreen()).thenReturn(true) + whenever(notificationRow.isHeadsUp).thenReturn(true) + whenever(notificationRow.isAboveShelf).thenReturn(true) + + resetViewStates_hunYTranslationIs(maxHunTranslation) + } + + @Test + @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) + fun resetViewStates_hunAnimatingAway_showingQS_newHeadsUpAnim_hunTranslatedToBottomOfScreen() { + // Given: the shade is open and scrolled to the bottom to show the QuickSettings + val bottomOfScreen = 2600f + val maxHunTranslation = 2000f + ambientState.maxHeadsUpTranslation = maxHunTranslation + ambientState.setLayoutMinHeight(2500) // Mock the height of shade + ambientState.stackY = 2500f // Scroll over the max translation + stackScrollAlgorithm.setIsExpanded(true) // Mark the shade open + stackScrollAlgorithm.setHeadsUpAppearHeightBottom(bottomOfScreen.toInt()) + whenever(notificationRow.mustStayOnScreen()).thenReturn(true) + whenever(notificationRow.isHeadsUp).thenReturn(true) + whenever(notificationRow.isAboveShelf).thenReturn(true) + whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true) + + resetViewStates_hunYTranslationIs( + expected = bottomOfScreen + stackScrollAlgorithm.mHeadsUpAppearStartAboveScreen + ) + } + + @Test + @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) + fun resetViewStates_hunAnimatingAway_newHeadsUpAnim_hunTranslatedToTopOfScreen() { + val topMargin = 100f + ambientState.maxHeadsUpTranslation = 2000f + ambientState.stackTopMargin = topMargin.toInt() + whenever(notificationRow.intrinsicHeight).thenReturn(100) + whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true) + + resetViewStates_hunYTranslationIs( + expected = -topMargin - stackScrollAlgorithm.mHeadsUpAppearStartAboveScreen + ) + } + + @Test fun resetViewStates_hunAnimatingAway_bottomNotClipped() { whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true) @@ -136,6 +221,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { } @Test + @DisableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) fun resetViewStates_hunsOverlappingAndBottomHunAnimatingAway_bottomHunClipped() { val topHun = mockExpandableNotificationRow() val bottomHun = mockExpandableNotificationRow() @@ -156,7 +242,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0) val marginBottom = - context.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom) + context.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom) val fullHeight = ambientState.layoutMaxHeight + marginBottom - ambientState.stackY val centeredY = ambientState.stackY + fullHeight / 2f - emptyShadeView.height / 2f assertThat(emptyShadeView.viewState.yTranslation).isEqualTo(centeredY) @@ -174,33 +260,37 @@ class StackScrollAlgorithmTest : SysuiTestCase() { assertThat(notificationRow.viewState.alpha).isEqualTo(1f) } + @OptIn(ExperimentalCoroutinesApi::class) @Test fun resetViewStates_expansionChanging_notificationBecomesTransparent() { whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(false) resetViewStates_expansionChanging_notificationAlphaUpdated( - expansionFraction = 0.25f, - expectedAlpha = 0.0f + expansionFraction = 0.25f, + expectedAlpha = 0.0f ) } + @OptIn(ExperimentalCoroutinesApi::class) @Test fun resetViewStates_expansionChangingWhileBouncerInTransit_viewBecomesTransparent() { whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(true) resetViewStates_expansionChanging_notificationAlphaUpdated( - expansionFraction = 0.85f, - expectedAlpha = 0.0f + expansionFraction = 0.85f, + expectedAlpha = 0.0f ) } + @OptIn(ExperimentalCoroutinesApi::class) @Test fun resetViewStates_expansionChanging_notificationAlphaUpdated() { whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(false) resetViewStates_expansionChanging_notificationAlphaUpdated( - expansionFraction = 0.6f, - expectedAlpha = getContentAlpha(0.6f) + expansionFraction = 0.6f, + expectedAlpha = getContentAlpha(0.6f) ) } + @OptIn(ExperimentalCoroutinesApi::class) @Test fun resetViewStates_largeScreen_expansionChanging_alphaUpdated_largeScreenValue() { val expansionFraction = 0.6f @@ -216,13 +306,14 @@ class StackScrollAlgorithmTest : SysuiTestCase() { ) } + @OptIn(ExperimentalCoroutinesApi::class) @Test fun expansionChanging_largeScreen_bouncerInTransit_alphaUpdated_bouncerValues() { ambientState.isSmallScreen = false whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(true) resetViewStates_expansionChanging_notificationAlphaUpdated( - expansionFraction = 0.95f, - expectedAlpha = aboutToShowBouncerProgress(0.95f), + expansionFraction = 0.95f, + expectedAlpha = aboutToShowBouncerProgress(0.95f), ) } @@ -235,10 +326,8 @@ class StackScrollAlgorithmTest : SysuiTestCase() { stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0) - verify(notificationShelf).updateState( - /* algorithmState= */any(), - /* ambientState= */eq(ambientState) - ) + verify(notificationShelf) + .updateState(/* algorithmState= */ any(), /* ambientState= */ eq(ambientState)) } @Test @@ -397,22 +486,31 @@ class StackScrollAlgorithmTest : SysuiTestCase() { @Test fun getGapForLocation_onLockscreen_returnsSmallGap() { - val gap = stackScrollAlgorithm.getGapForLocation( - /* fractionToShade= */ 0f, /* onKeyguard= */ true) + val gap = + stackScrollAlgorithm.getGapForLocation( + /* fractionToShade= */ 0f, + /* onKeyguard= */ true + ) assertThat(gap).isEqualTo(smallGap) } @Test fun getGapForLocation_goingToShade_interpolatesGap() { - val gap = stackScrollAlgorithm.getGapForLocation( - /* fractionToShade= */ 0.5f, /* onKeyguard= */ true) + val gap = + stackScrollAlgorithm.getGapForLocation( + /* fractionToShade= */ 0.5f, + /* onKeyguard= */ true + ) assertThat(gap).isEqualTo(smallGap * 0.5f + bigGap * 0.5f) } @Test fun getGapForLocation_notOnLockscreen_returnsBigGap() { - val gap = stackScrollAlgorithm.getGapForLocation( - /* fractionToShade= */ 0f, /* onKeyguard= */ false) + val gap = + stackScrollAlgorithm.getGapForLocation( + /* fractionToShade= */ 0f, + /* onKeyguard= */ false + ) assertThat(gap).isEqualTo(bigGap) } @@ -469,12 +567,14 @@ class StackScrollAlgorithmTest : SysuiTestCase() { val expandableViewState = ExpandableViewState() expandableViewState.headsUpIsVisible = false - stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState, - /* isShadeExpanded= */ true, - /* mustStayOnScreen= */ true, - /* isViewEndVisible= */ true, - /* viewEnd= */ 0f, - /* maxHunY= */ 10f) + stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible( + expandableViewState, + /* isShadeExpanded= */ true, + /* mustStayOnScreen= */ true, + /* isViewEndVisible= */ true, + /* viewEnd= */ 0f, + /* maxHunY= */ 10f + ) assertTrue(expandableViewState.headsUpIsVisible) } @@ -484,12 +584,14 @@ class StackScrollAlgorithmTest : SysuiTestCase() { val expandableViewState = ExpandableViewState() expandableViewState.headsUpIsVisible = true - stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState, - /* isShadeExpanded= */ true, - /* mustStayOnScreen= */ true, - /* isViewEndVisible= */ true, - /* viewEnd= */ 10f, - /* maxHunY= */ 0f) + stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible( + expandableViewState, + /* isShadeExpanded= */ true, + /* mustStayOnScreen= */ true, + /* isViewEndVisible= */ true, + /* viewEnd= */ 10f, + /* maxHunY= */ 0f + ) assertFalse(expandableViewState.headsUpIsVisible) } @@ -499,12 +601,14 @@ class StackScrollAlgorithmTest : SysuiTestCase() { val expandableViewState = ExpandableViewState() expandableViewState.headsUpIsVisible = true - stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState, - /* isShadeExpanded= */ false, - /* mustStayOnScreen= */ true, - /* isViewEndVisible= */ true, - /* viewEnd= */ 10f, - /* maxHunY= */ 1f) + stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible( + expandableViewState, + /* isShadeExpanded= */ false, + /* mustStayOnScreen= */ true, + /* isViewEndVisible= */ true, + /* viewEnd= */ 10f, + /* maxHunY= */ 1f + ) assertTrue(expandableViewState.headsUpIsVisible) } @@ -514,12 +618,14 @@ class StackScrollAlgorithmTest : SysuiTestCase() { val expandableViewState = ExpandableViewState() expandableViewState.headsUpIsVisible = true - stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState, - /* isShadeExpanded= */ true, - /* mustStayOnScreen= */ false, - /* isViewEndVisible= */ true, - /* viewEnd= */ 10f, - /* maxHunY= */ 1f) + stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible( + expandableViewState, + /* isShadeExpanded= */ true, + /* mustStayOnScreen= */ false, + /* isViewEndVisible= */ true, + /* viewEnd= */ 10f, + /* maxHunY= */ 1f + ) assertTrue(expandableViewState.headsUpIsVisible) } @@ -529,12 +635,14 @@ class StackScrollAlgorithmTest : SysuiTestCase() { val expandableViewState = ExpandableViewState() expandableViewState.headsUpIsVisible = true - stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState, - /* isShadeExpanded= */ true, - /* mustStayOnScreen= */ true, - /* isViewEndVisible= */ false, - /* viewEnd= */ 10f, - /* maxHunY= */ 1f) + stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible( + expandableViewState, + /* isShadeExpanded= */ true, + /* mustStayOnScreen= */ true, + /* isViewEndVisible= */ false, + /* viewEnd= */ 10f, + /* maxHunY= */ 1f + ) assertTrue(expandableViewState.headsUpIsVisible) } @@ -544,9 +652,12 @@ class StackScrollAlgorithmTest : SysuiTestCase() { val expandableViewState = ExpandableViewState() expandableViewState.yTranslation = 50f - stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f, - /* stackTranslation= */ 0f, - /* collapsedHeight= */ 1f, expandableViewState) + stackScrollAlgorithm.clampHunToTop( + /* quickQsOffsetHeight= */ 10f, + /* stackTranslation= */ 0f, + /* collapsedHeight= */ 1f, + expandableViewState + ) // qqs (10 + 0) < viewY (50) assertEquals(50f, expandableViewState.yTranslation) @@ -557,9 +668,12 @@ class StackScrollAlgorithmTest : SysuiTestCase() { val expandableViewState = ExpandableViewState() expandableViewState.yTranslation = -10f - stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f, - /* stackTranslation= */ 0f, - /* collapsedHeight= */ 1f, expandableViewState) + stackScrollAlgorithm.clampHunToTop( + /* quickQsOffsetHeight= */ 10f, + /* stackTranslation= */ 0f, + /* collapsedHeight= */ 1f, + expandableViewState + ) // qqs (10 + 0) > viewY (-10) assertEquals(10f, expandableViewState.yTranslation) @@ -571,9 +685,12 @@ class StackScrollAlgorithmTest : SysuiTestCase() { expandableViewState.height = 20 expandableViewState.yTranslation = -100f - stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f, - /* stackTranslation= */ 0f, - /* collapsedHeight= */ 10f, expandableViewState) + stackScrollAlgorithm.clampHunToTop( + /* quickQsOffsetHeight= */ 10f, + /* stackTranslation= */ 0f, + /* collapsedHeight= */ 10f, + expandableViewState + ) // newTranslation = max(10, -100) = 10 // distToRealY = 10 - (-100f) = 110 @@ -587,9 +704,12 @@ class StackScrollAlgorithmTest : SysuiTestCase() { expandableViewState.height = 20 expandableViewState.yTranslation = 5f - stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f, - /* stackTranslation= */ 0f, - /* collapsedHeight= */ 10f, expandableViewState) + stackScrollAlgorithm.clampHunToTop( + /* quickQsOffsetHeight= */ 10f, + /* stackTranslation= */ 0f, + /* collapsedHeight= */ 10f, + expandableViewState + ) // newTranslation = max(10, 5) = 10 // distToRealY = 10 - 5 = 5 @@ -599,41 +719,49 @@ class StackScrollAlgorithmTest : SysuiTestCase() { @Test fun computeCornerRoundnessForPinnedHun_stackBelowScreen_round() { - val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun( + val currentRoundness = + stackScrollAlgorithm.computeCornerRoundnessForPinnedHun( /* hostViewHeight= */ 100f, /* stackY= */ 110f, /* viewMaxHeight= */ 20f, - /* originalCornerRoundness= */ 0f) + /* originalCornerRoundness= */ 0f + ) assertEquals(1f, currentRoundness) } @Test fun computeCornerRoundnessForPinnedHun_stackAboveScreenBelowPinPoint_halfRound() { - val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun( + val currentRoundness = + stackScrollAlgorithm.computeCornerRoundnessForPinnedHun( /* hostViewHeight= */ 100f, /* stackY= */ 90f, /* viewMaxHeight= */ 20f, - /* originalCornerRoundness= */ 0f) + /* originalCornerRoundness= */ 0f + ) assertEquals(0.5f, currentRoundness) } @Test fun computeCornerRoundnessForPinnedHun_stackAbovePinPoint_notRound() { - val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun( + val currentRoundness = + stackScrollAlgorithm.computeCornerRoundnessForPinnedHun( /* hostViewHeight= */ 100f, /* stackY= */ 0f, /* viewMaxHeight= */ 20f, - /* originalCornerRoundness= */ 0f) + /* originalCornerRoundness= */ 0f + ) assertEquals(0f, currentRoundness) } @Test fun computeCornerRoundnessForPinnedHun_originallyRoundAndStackAbovePinPoint_round() { - val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun( + val currentRoundness = + stackScrollAlgorithm.computeCornerRoundnessForPinnedHun( /* hostViewHeight= */ 100f, /* stackY= */ 0f, /* viewMaxHeight= */ 20f, - /* originalCornerRoundness= */ 1f) + /* originalCornerRoundness= */ 1f + ) assertEquals(1f, currentRoundness) } @@ -642,23 +770,20 @@ class StackScrollAlgorithmTest : SysuiTestCase() { // Given: shade is opened, yTranslation of HUN is 0, // the height of HUN equals to the height of QQS Panel, // and HUN fully overlaps with QQS Panel - ambientState.stackTranslation = px(R.dimen.qqs_layout_margin_top) + - px(R.dimen.qqs_layout_padding_bottom) - val childHunView = createHunViewMock( - isShadeOpen = true, - fullyVisible = false, - headerVisibleAmount = 1f - ) + ambientState.stackTranslation = + px(R.dimen.qqs_layout_margin_top) + px(R.dimen.qqs_layout_padding_bottom) + val childHunView = + createHunViewMock(isShadeOpen = true, fullyVisible = false, headerVisibleAmount = 1f) val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState() algorithmState.visibleChildren.add(childHunView) // When: updateChildZValue() is called for the top HUN stackScrollAlgorithm.updateChildZValue( - /* i= */ 0, - /* childrenOnTop= */ 0.0f, - /* StackScrollAlgorithmState= */ algorithmState, - /* ambientState= */ ambientState, - /* shouldElevateHun= */ true + /* i= */ 0, + /* childrenOnTop= */ 0.0f, + /* StackScrollAlgorithmState= */ algorithmState, + /* ambientState= */ ambientState, + /* shouldElevateHun= */ true ) // Then: full shadow would be applied @@ -670,13 +795,10 @@ class StackScrollAlgorithmTest : SysuiTestCase() { // Given: shade is opened, yTranslation of HUN is greater than 0, // the height of HUN is equal to the height of QQS Panel, // and HUN partially overlaps with QQS Panel - ambientState.stackTranslation = px(R.dimen.qqs_layout_margin_top) + - px(R.dimen.qqs_layout_padding_bottom) - val childHunView = createHunViewMock( - isShadeOpen = true, - fullyVisible = false, - headerVisibleAmount = 1f - ) + ambientState.stackTranslation = + px(R.dimen.qqs_layout_margin_top) + px(R.dimen.qqs_layout_padding_bottom) + val childHunView = + createHunViewMock(isShadeOpen = true, fullyVisible = false, headerVisibleAmount = 1f) // Use half of the HUN's height as overlap childHunView.viewState.yTranslation = (childHunView.viewState.height + 1 shr 1).toFloat() val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState() @@ -684,17 +806,17 @@ class StackScrollAlgorithmTest : SysuiTestCase() { // When: updateChildZValue() is called for the top HUN stackScrollAlgorithm.updateChildZValue( - /* i= */ 0, - /* childrenOnTop= */ 0.0f, - /* StackScrollAlgorithmState= */ algorithmState, - /* ambientState= */ ambientState, - /* shouldElevateHun= */ true + /* i= */ 0, + /* childrenOnTop= */ 0.0f, + /* StackScrollAlgorithmState= */ algorithmState, + /* ambientState= */ ambientState, + /* shouldElevateHun= */ true ) // Then: HUN should have shadow, but not as full size assertThat(childHunView.viewState.zTranslation).isGreaterThan(0.0f) assertThat(childHunView.viewState.zTranslation) - .isLessThan(px(R.dimen.heads_up_pinned_elevation)) + .isLessThan(px(R.dimen.heads_up_pinned_elevation)) } @Test @@ -702,28 +824,25 @@ class StackScrollAlgorithmTest : SysuiTestCase() { // Given: shade is opened, yTranslation of HUN is equal to QQS Panel's height, // the height of HUN is equal to the height of QQS Panel, // and HUN doesn't overlap with QQS Panel - ambientState.stackTranslation = px(R.dimen.qqs_layout_margin_top) + - px(R.dimen.qqs_layout_padding_bottom) + ambientState.stackTranslation = + px(R.dimen.qqs_layout_margin_top) + px(R.dimen.qqs_layout_padding_bottom) // Mock the height of shade ambientState.setLayoutMinHeight(1000) - val childHunView = createHunViewMock( - isShadeOpen = true, - fullyVisible = true, - headerVisibleAmount = 1f - ) + val childHunView = + createHunViewMock(isShadeOpen = true, fullyVisible = true, headerVisibleAmount = 1f) // HUN doesn't overlap with QQS Panel - childHunView.viewState.yTranslation = ambientState.topPadding + - ambientState.stackTranslation + childHunView.viewState.yTranslation = + ambientState.topPadding + ambientState.stackTranslation val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState() algorithmState.visibleChildren.add(childHunView) // When: updateChildZValue() is called for the top HUN stackScrollAlgorithm.updateChildZValue( - /* i= */ 0, - /* childrenOnTop= */ 0.0f, - /* StackScrollAlgorithmState= */ algorithmState, - /* ambientState= */ ambientState, - /* shouldElevateHun= */ true + /* i= */ 0, + /* childrenOnTop= */ 0.0f, + /* StackScrollAlgorithmState= */ algorithmState, + /* ambientState= */ ambientState, + /* shouldElevateHun= */ true ) // Then: HUN should not have shadow @@ -737,11 +856,8 @@ class StackScrollAlgorithmTest : SysuiTestCase() { ambientState.stackTranslation = -ambientState.topPadding // Mock the height of shade ambientState.setLayoutMinHeight(1000) - val childHunView = createHunViewMock( - isShadeOpen = false, - fullyVisible = false, - headerVisibleAmount = 0f - ) + val childHunView = + createHunViewMock(isShadeOpen = false, fullyVisible = false, headerVisibleAmount = 0f) childHunView.viewState.yTranslation = 0f // Shade is closed, thus childHunView's headerVisibleAmount is 0 childHunView.headerVisibleAmount = 0f @@ -750,11 +866,11 @@ class StackScrollAlgorithmTest : SysuiTestCase() { // When: updateChildZValue() is called for the top HUN stackScrollAlgorithm.updateChildZValue( - /* i= */ 0, - /* childrenOnTop= */ 0.0f, - /* StackScrollAlgorithmState= */ algorithmState, - /* ambientState= */ ambientState, - /* shouldElevateHun= */ true + /* i= */ 0, + /* childrenOnTop= */ 0.0f, + /* StackScrollAlgorithmState= */ algorithmState, + /* ambientState= */ ambientState, + /* shouldElevateHun= */ true ) // Then: HUN should have full shadow @@ -768,11 +884,8 @@ class StackScrollAlgorithmTest : SysuiTestCase() { ambientState.stackTranslation = -ambientState.topPadding // Mock the height of shade ambientState.setLayoutMinHeight(1000) - val childHunView = createHunViewMock( - isShadeOpen = false, - fullyVisible = false, - headerVisibleAmount = 0.5f - ) + val childHunView = + createHunViewMock(isShadeOpen = false, fullyVisible = false, headerVisibleAmount = 0.5f) childHunView.viewState.yTranslation = 0f // Shade is being opened, thus childHunView's headerVisibleAmount is between 0 and 1 // use 0.5 as headerVisibleAmount here @@ -782,17 +895,17 @@ class StackScrollAlgorithmTest : SysuiTestCase() { // When: updateChildZValue() is called for the top HUN stackScrollAlgorithm.updateChildZValue( - /* i= */ 0, - /* childrenOnTop= */ 0.0f, - /* StackScrollAlgorithmState= */ algorithmState, - /* ambientState= */ ambientState, - /* shouldElevateHun= */ true + /* i= */ 0, + /* childrenOnTop= */ 0.0f, + /* StackScrollAlgorithmState= */ algorithmState, + /* ambientState= */ ambientState, + /* shouldElevateHun= */ true ) // Then: HUN should have shadow, but not as full size assertThat(childHunView.viewState.zTranslation).isGreaterThan(0.0f) assertThat(childHunView.viewState.zTranslation) - .isLessThan(px(R.dimen.heads_up_pinned_elevation)) + .isLessThan(px(R.dimen.heads_up_pinned_elevation)) } @Test @@ -862,134 +975,174 @@ class StackScrollAlgorithmTest : SysuiTestCase() { // stackScrollAlgorithm.resetViewStates is called. ambientState.dozeAmount = 0.5f setExpansionFractionWithoutShelfDuringAodToLockScreen( - ambientState, - algorithmState, - fraction = 0.5f + ambientState, + algorithmState, + fraction = 0.5f ) stackScrollAlgorithm.resetViewStates(ambientState, 0) // Then: pulsingNotificationView should show at full height assertEquals( - stackScrollAlgorithm.getMaxAllowedChildHeight(pulsingNotificationView), - pulsingNotificationView.viewState.height + stackScrollAlgorithm.getMaxAllowedChildHeight(pulsingNotificationView), + pulsingNotificationView.viewState.height ) // After: reset dozeAmount and expansionFraction ambientState.dozeAmount = 0f setExpansionFractionWithoutShelfDuringAodToLockScreen( - ambientState, - algorithmState, - fraction = 1f + ambientState, + algorithmState, + fraction = 1f ) } // region shouldPinHunToBottomOfExpandedQs @Test fun shouldHunBeVisibleWhenScrolled_mustStayOnScreenFalse_false() { - assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( - /* mustStayOnScreen= */false, - /* headsUpIsVisible= */false, - /* showingPulsing= */false, - /* isOnKeyguard=*/false, - /*headsUpOnKeyguard=*/false - )).isFalse() + assertThat( + stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( + /* mustStayOnScreen= */ false, + /* headsUpIsVisible= */ false, + /* showingPulsing= */ false, + /* isOnKeyguard=*/ false, + /*headsUpOnKeyguard=*/ false + ) + ) + .isFalse() } @Test fun shouldPinHunToBottomOfExpandedQs_headsUpIsVisible_false() { - assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( - /* mustStayOnScreen= */true, - /* headsUpIsVisible= */true, - /* showingPulsing= */false, - /* isOnKeyguard=*/false, - /*headsUpOnKeyguard=*/false - )).isFalse() + assertThat( + stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( + /* mustStayOnScreen= */ true, + /* headsUpIsVisible= */ true, + /* showingPulsing= */ false, + /* isOnKeyguard=*/ false, + /*headsUpOnKeyguard=*/ false + ) + ) + .isFalse() } @Test fun shouldHunBeVisibleWhenScrolled_showingPulsing_false() { - assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( - /* mustStayOnScreen= */true, - /* headsUpIsVisible= */false, - /* showingPulsing= */true, - /* isOnKeyguard=*/false, - /* headsUpOnKeyguard= */false - )).isFalse() + assertThat( + stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( + /* mustStayOnScreen= */ true, + /* headsUpIsVisible= */ false, + /* showingPulsing= */ true, + /* isOnKeyguard=*/ false, + /* headsUpOnKeyguard= */ false + ) + ) + .isFalse() } @Test fun shouldHunBeVisibleWhenScrolled_isOnKeyguard_false() { - assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( - /* mustStayOnScreen= */true, - /* headsUpIsVisible= */false, - /* showingPulsing= */false, - /* isOnKeyguard=*/true, - /* headsUpOnKeyguard= */false - )).isFalse() + assertThat( + stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( + /* mustStayOnScreen= */ true, + /* headsUpIsVisible= */ false, + /* showingPulsing= */ false, + /* isOnKeyguard=*/ true, + /* headsUpOnKeyguard= */ false + ) + ) + .isFalse() } @Test fun shouldHunBeVisibleWhenScrolled_isNotOnKeyguard_true() { - assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( - /* mustStayOnScreen= */true, - /* headsUpIsVisible= */false, - /* showingPulsing= */false, - /* isOnKeyguard=*/false, - /* headsUpOnKeyguard= */false - )).isTrue() + assertThat( + stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( + /* mustStayOnScreen= */ true, + /* headsUpIsVisible= */ false, + /* showingPulsing= */ false, + /* isOnKeyguard=*/ false, + /* headsUpOnKeyguard= */ false + ) + ) + .isTrue() } @Test fun shouldHunBeVisibleWhenScrolled_headsUpOnKeyguard_true() { - assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( - /* mustStayOnScreen= */true, - /* headsUpIsVisible= */false, - /* showingPulsing= */false, - /* isOnKeyguard=*/true, - /* headsUpOnKeyguard= */true - )).isTrue() + assertThat( + stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled( + /* mustStayOnScreen= */ true, + /* headsUpIsVisible= */ false, + /* showingPulsing= */ false, + /* isOnKeyguard=*/ true, + /* headsUpOnKeyguard= */ true + ) + ) + .isTrue() } - // endregion - private fun createHunViewMock( - isShadeOpen: Boolean, - fullyVisible: Boolean, - headerVisibleAmount: Float - ) = - mock<ExpandableNotificationRow>().apply { - val childViewStateMock = createHunChildViewState(isShadeOpen, fullyVisible) - whenever(this.viewState).thenReturn(childViewStateMock) - - whenever(this.mustStayOnScreen()).thenReturn(true) - whenever(this.headerVisibleAmount).thenReturn(headerVisibleAmount) + @Test + fun shouldHunAppearFromBottom_hunAtMaxHunTranslation() { + ambientState.maxHeadsUpTranslation = 400f + val viewState = + ExpandableViewState().apply { + height = 100 + yTranslation = ambientState.maxHeadsUpTranslation - height // move it to the max } + assertTrue(stackScrollAlgorithm.shouldHunAppearFromBottom(ambientState, viewState)) + } - private fun createHunChildViewState(isShadeOpen: Boolean, fullyVisible: Boolean) = + @Test + fun shouldHunAppearFromBottom_hunBelowMaxHunTranslation() { + ambientState.maxHeadsUpTranslation = 400f + val viewState = ExpandableViewState().apply { - // Mock the HUN's height with ambientState.topPadding + - // ambientState.stackTranslation - height = (ambientState.topPadding + ambientState.stackTranslation).toInt() - if (isShadeOpen && fullyVisible) { - yTranslation = - ambientState.topPadding + ambientState.stackTranslation - } else { - yTranslation = 0f - } - headsUpIsVisible = fullyVisible + height = 100 + yTranslation = + ambientState.maxHeadsUpTranslation - height - 1 // move it below the max } - private fun createPulsingViewMock( + assertFalse(stackScrollAlgorithm.shouldHunAppearFromBottom(ambientState, viewState)) + } + // endregion + + private fun createHunViewMock( + isShadeOpen: Boolean, + fullyVisible: Boolean, + headerVisibleAmount: Float ) = - mock<ExpandableNotificationRow>().apply { - whenever(this.viewState).thenReturn(ExpandableViewState()) - whenever(this.showingPulsing()).thenReturn(true) + mock<ExpandableNotificationRow>().apply { + val childViewStateMock = createHunChildViewState(isShadeOpen, fullyVisible) + whenever(this.viewState).thenReturn(childViewStateMock) + + whenever(this.mustStayOnScreen()).thenReturn(true) + whenever(this.headerVisibleAmount).thenReturn(headerVisibleAmount) + } + + private fun createHunChildViewState(isShadeOpen: Boolean, fullyVisible: Boolean) = + ExpandableViewState().apply { + // Mock the HUN's height with ambientState.topPadding + + // ambientState.stackTranslation + height = (ambientState.topPadding + ambientState.stackTranslation).toInt() + if (isShadeOpen && fullyVisible) { + yTranslation = ambientState.topPadding + ambientState.stackTranslation + } else { + yTranslation = 0f } + headsUpIsVisible = fullyVisible + } + + private fun createPulsingViewMock() = + mock<ExpandableNotificationRow>().apply { + whenever(this.viewState).thenReturn(ExpandableViewState()) + whenever(this.showingPulsing()).thenReturn(true) + } private fun setExpansionFractionWithoutShelfDuringAodToLockScreen( - ambientState: AmbientState, - algorithmState: StackScrollAlgorithm.StackScrollAlgorithmState, - fraction: Float + ambientState: AmbientState, + algorithmState: StackScrollAlgorithm.StackScrollAlgorithmState, + fraction: Float ) { // showingShelf: false algorithmState.firstViewInShelf = null @@ -1002,11 +1155,10 @@ class StackScrollAlgorithmTest : SysuiTestCase() { ambientState.stackHeight = ambientState.stackEndHeight * fraction } - private fun resetViewStates_hunYTranslationIsInset() { + private fun resetViewStates_hunYTranslationIs(expected: Float) { stackScrollAlgorithm.resetViewStates(ambientState, 0) - assertThat(notificationRow.viewState.yTranslation) - .isEqualTo(stackScrollAlgorithm.mHeadsUpInset) + assertThat(notificationRow.viewState.yTranslation).isEqualTo(expected) } private fun resetViewStates_stackMargin_changesHunYTranslation() { @@ -1025,13 +1177,13 @@ class StackScrollAlgorithmTest : SysuiTestCase() { } private fun resetViewStates_hunsOverlapping_bottomHunClipped( - topHun: ExpandableNotificationRow, - bottomHun: ExpandableNotificationRow + topHun: ExpandableNotificationRow, + bottomHun: ExpandableNotificationRow ) { - val topHunHeight = mContext.resources.getDimensionPixelSize( - R.dimen.notification_content_min_height) - val bottomHunHeight = mContext.resources.getDimensionPixelSize( - R.dimen.notification_max_heads_up_height) + val topHunHeight = + mContext.resources.getDimensionPixelSize(R.dimen.notification_content_min_height) + val bottomHunHeight = + mContext.resources.getDimensionPixelSize(R.dimen.notification_max_heads_up_height) whenever(topHun.intrinsicHeight).thenReturn(topHunHeight) whenever(bottomHun.intrinsicHeight).thenReturn(bottomHunHeight) @@ -1054,8 +1206,8 @@ class StackScrollAlgorithmTest : SysuiTestCase() { } private fun resetViewStates_expansionChanging_notificationAlphaUpdated( - expansionFraction: Float, - expectedAlpha: Float, + expansionFraction: Float, + expectedAlpha: Float, ) { ambientState.isExpansionChanging = true ambientState.expansionFraction = expansionFraction 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 new file mode 100644 index 000000000000..5a5703512a39 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.stack + +import android.platform.test.annotations.EnableFlags +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.row.ExpandableView +import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent +import com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_HEADS_UP_APPEAR +import com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_HEADS_UP_DISAPPEAR +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Mockito.any +import org.mockito.Mockito.description +import org.mockito.Mockito.eq +import org.mockito.Mockito.verify + +private const val VIEW_HEIGHT = 100 + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class StackStateAnimatorTest : SysuiTestCase() { + + private lateinit var stackStateAnimator: StackStateAnimator + private val stackScroller: NotificationStackScrollLayout = mock() + private val view: ExpandableView = mock() + private val viewState: ExpandableViewState = + ExpandableViewState().apply { height = VIEW_HEIGHT } + private val runnableCaptor: ArgumentCaptor<Runnable> = argumentCaptor() + @Before + fun setUp() { + whenever(stackScroller.context).thenReturn(context) + whenever(view.viewState).thenReturn(viewState) + stackStateAnimator = StackStateAnimator(stackScroller) + } + + @Test + @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) + fun startAnimationForEvents_headsUpFromTop_startsHeadsUpAppearAnim() { + val topMargin = 50f + val expectedStartY = -topMargin - stackStateAnimator.mHeadsUpAppearStartAboveScreen + val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) + stackStateAnimator.setStackTopMargin(topMargin.toInt()) + + stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0) + + verify(view).setActualHeight(VIEW_HEIGHT, false) + verify(view, description("should animate from the top")).translationY = expectedStartY + verify(view) + .performAddAnimation( + /* delay= */ 0L, + /* duration= */ ANIMATION_DURATION_HEADS_UP_APPEAR.toLong(), + /* isHeadsUpAppear= */ true, + /* onEndRunnable= */ null + ) + } + + @Test + @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) + fun startAnimationForEvents_headsUpFromBottom_startsHeadsUpAppearAnim() { + val screenHeight = 2000f + val expectedStartY = screenHeight + stackStateAnimator.mHeadsUpAppearStartAboveScreen + val event = + AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR).apply { + headsUpFromBottom = true + } + stackStateAnimator.setHeadsUpAppearHeightBottom(screenHeight.toInt()) + + stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0) + + verify(view).setActualHeight(VIEW_HEIGHT, false) + verify(view, description("should animate from the bottom")).translationY = expectedStartY + verify(view) + .performAddAnimation( + /* delay= */ 0L, + /* duration= */ ANIMATION_DURATION_HEADS_UP_APPEAR.toLong(), + /* isHeadsUpAppear= */ true, + /* onEndRunnable= */ null + ) + } + + @Test + fun startAnimationForEvents_startsHeadsUpDisappearAnim() { + val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR) + stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0) + + verify(view) + .performRemoveAnimation( + /* duration= */ eq(ANIMATION_DURATION_HEADS_UP_DISAPPEAR.toLong()), + /* delay= */ eq(0L), + /* translationDirection= */ eq(0f), + /* isHeadsUpAnimation= */ eq(true), + /* onStartedRunnable= */ any(), + /* onFinishedRunnable= */ runnableCaptor.capture(), + /* animationListener= */ any() + ) + + runnableCaptor.value.run() // execute the end runnable + + verify(view, description("should be called at the end of the animation")) + .removeFromTransientContainer() + } +} |