summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/res/values/dimens.xml4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java21
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java17
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java59
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java52
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt618
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt125
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()
+ }
+}