summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java97
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt175
2 files changed, 248 insertions, 24 deletions
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 4013254c6592..6d513d0da5c1 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
@@ -24,8 +24,7 @@ import android.util.MathUtils;
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.VisibleForTesting;
-
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.SystemBarUtils;
import com.android.keyguard.BouncerPanelExpansionCalculator;
import com.android.systemui.R;
@@ -65,6 +64,9 @@ public class StackScrollAlgorithm {
private int mPinnedZTranslationExtra;
private float mNotificationScrimPadding;
private int mMarginBottom;
+ private float mQuickQsOffsetHeight;
+ private float mSmallCornerRadius;
+ private float mLargeCornerRadius;
public StackScrollAlgorithm(
Context context,
@@ -74,10 +76,10 @@ public class StackScrollAlgorithm {
}
public void initView(Context context) {
- initConstants(context);
+ updateResources(context);
}
- private void initConstants(Context context) {
+ private void updateResources(Context context) {
Resources res = context.getResources();
mPaddingBetweenElements = res.getDimensionPixelSize(
R.dimen.notification_divider_height);
@@ -93,6 +95,9 @@ public class StackScrollAlgorithm {
R.dimen.notification_section_divider_height_lockscreen);
mNotificationScrimPadding = res.getDimensionPixelSize(R.dimen.notification_side_paddings);
mMarginBottom = res.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom);
+ mQuickQsOffsetHeight = SystemBarUtils.getQuickQsOffsetHeight(context);
+ mSmallCornerRadius = res.getDimension(R.dimen.notification_corner_radius_small);
+ mLargeCornerRadius = res.getDimension(R.dimen.notification_corner_radius);
}
/**
@@ -441,6 +446,15 @@ public class StackScrollAlgorithm {
return false;
}
+ @VisibleForTesting
+ void maybeUpdateHeadsUpIsVisible(ExpandableViewState viewState, boolean isShadeExpanded,
+ boolean mustStayOnScreen, boolean topVisible, float viewEnd, float hunMax) {
+
+ if (isShadeExpanded && mustStayOnScreen && topVisible) {
+ viewState.headsUpIsVisible = viewEnd < hunMax;
+ }
+ }
+
// TODO(b/172289889) polish shade open from HUN
/**
* Populates the {@link ExpandableViewState} for a single child.
@@ -474,14 +488,6 @@ public class StackScrollAlgorithm {
: ShadeInterpolation.getContentAlpha(expansion);
}
- if (ambientState.isShadeExpanded() && view.mustStayOnScreen()
- && viewState.yTranslation >= 0) {
- // Even if we're not scrolled away we're in view and we're also not in the
- // shelf. We can relax the constraints and let us scroll off the top!
- float end = viewState.yTranslation + viewState.height + ambientState.getStackY();
- viewState.headsUpIsVisible = end < ambientState.getMaxHeadsUpTranslation();
- }
-
final float expansionFraction = getExpansionFractionWithoutShelf(
algorithmState, ambientState);
@@ -497,8 +503,15 @@ public class StackScrollAlgorithm {
algorithmState.mCurrentExpandedYPosition += gap;
}
+ // Must set viewState.yTranslation _before_ use.
+ // Incoming views have yTranslation=0 by default.
viewState.yTranslation = algorithmState.mCurrentYPosition;
+ maybeUpdateHeadsUpIsVisible(viewState, ambientState.isShadeExpanded(),
+ view.mustStayOnScreen(), /* topVisible */ viewState.yTranslation >= 0,
+ /* viewEnd */ viewState.yTranslation + viewState.height + ambientState.getStackY(),
+ /* hunMax */ ambientState.getMaxHeadsUpTranslation()
+ );
if (view instanceof FooterView) {
final boolean shadeClosed = !ambientState.isShadeExpanded();
final boolean isShelfShowing = algorithmState.firstViewInShelf != null;
@@ -682,7 +695,8 @@ public class StackScrollAlgorithm {
if (row.mustStayOnScreen() && !childState.headsUpIsVisible
&& !row.showingPulsing()) {
// Ensure that the heads up is always visible even when scrolled off
- clampHunToTop(ambientState, row, childState);
+ clampHunToTop(mQuickQsOffsetHeight, ambientState.getStackTranslation(),
+ row.getCollapsedHeight(), childState);
if (isTopEntry && row.isAboveShelf()) {
// the first hun can't get off screen.
clampHunToMaxTranslation(ambientState, row, childState);
@@ -719,27 +733,62 @@ public class StackScrollAlgorithm {
}
}
- private void clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row,
- ExpandableViewState childState) {
- float newTranslation = Math.max(ambientState.getTopPadding()
- + ambientState.getStackTranslation(), childState.yTranslation);
- childState.height = (int) Math.max(childState.height - (newTranslation
- - childState.yTranslation), row.getCollapsedHeight());
- childState.yTranslation = newTranslation;
+ /**
+ * When shade is open and we are scrolled to the bottom of notifications,
+ * clamp incoming HUN in its collapsed form, right below qs offset.
+ * Transition pinned collapsed HUN to full height when scrolling back up.
+ */
+ @VisibleForTesting
+ void clampHunToTop(float quickQsOffsetHeight, float stackTranslation, float collapsedHeight,
+ ExpandableViewState viewState) {
+
+ final float newTranslation = Math.max(quickQsOffsetHeight + stackTranslation,
+ viewState.yTranslation);
+
+ // Transition from collapsed pinned state to fully expanded state
+ // when the pinned HUN approaches its actual location (when scrolling back to top).
+ final float distToRealY = newTranslation - viewState.yTranslation;
+ viewState.height = (int) Math.max(viewState.height - distToRealY, collapsedHeight);
+ viewState.yTranslation = newTranslation;
}
+ // Pin HUN to bottom of expanded QS
+ // while the rest of notifications are scrolled offscreen.
private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row,
ExpandableViewState childState) {
- float newTranslation;
float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation();
- float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding()
+ final float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding()
+ ambientState.getStackTranslation();
maxHeadsUpTranslation = Math.min(maxHeadsUpTranslation, maxShelfPosition);
- float bottomPosition = maxHeadsUpTranslation - row.getCollapsedHeight();
- newTranslation = Math.min(childState.yTranslation, bottomPosition);
+
+ final float bottomPosition = maxHeadsUpTranslation - row.getCollapsedHeight();
+ final float newTranslation = Math.min(childState.yTranslation, bottomPosition);
childState.height = (int) Math.min(childState.height, maxHeadsUpTranslation
- newTranslation);
childState.yTranslation = newTranslation;
+
+ // Animate pinned HUN bottom corners to and from original roundness.
+ final float originalCornerRadius =
+ row.isLastInSection() ? 1f : (mSmallCornerRadius / mLargeCornerRadius);
+ final float roundness = computeCornerRoundnessForPinnedHun(mHostView.getHeight(),
+ ambientState.getStackY(), getMaxAllowedChildHeight(row), originalCornerRadius);
+ row.setBottomRoundness(roundness, /* animate= */ false);
+ }
+
+ @VisibleForTesting
+ float computeCornerRoundnessForPinnedHun(float hostViewHeight, float stackY,
+ float viewMaxHeight, float originalCornerRadius) {
+
+ // Compute y where corner roundness should be in its original unpinned state.
+ // We use view max height because the pinned collapsed HUN expands to max height
+ // when it becomes unpinned.
+ final float originalRoundnessY = hostViewHeight - viewMaxHeight;
+
+ final float distToOriginalRoundness = Math.max(0f, stackY - originalRoundnessY);
+ final float progressToPinnedRoundness = Math.min(1f,
+ distToOriginalRoundness / viewMaxHeight);
+
+ return MathUtils.lerp(originalCornerRadius, 1f, progressToPinnedRoundness);
}
protected int getMaxAllowedChildHeight(View child) {
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 275dbfd516e4..8fd6842911de 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
@@ -15,6 +15,7 @@ import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.google.common.truth.Truth.assertThat
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
+import junit.framework.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.mock
@@ -164,4 +165,178 @@ class StackScrollAlgorithmTest : SysuiTestCase() {
stackScrollAlgorithm.updateViewWithShelf(expandableView, expandableViewState, shelfStart)
assertFalse(expandableViewState.hidden)
}
+
+ @Test
+ fun maybeUpdateHeadsUpIsVisible_endVisible_true() {
+ val expandableViewState = ExpandableViewState()
+ expandableViewState.headsUpIsVisible = false
+
+ stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState,
+ /* isShadeExpanded= */ true,
+ /* mustStayOnScreen= */ true,
+ /* isViewEndVisible= */ true,
+ /* viewEnd= */ 0f,
+ /* maxHunY= */ 10f)
+
+ assertTrue(expandableViewState.headsUpIsVisible)
+ }
+
+ @Test
+ fun maybeUpdateHeadsUpIsVisible_endHidden_false() {
+ val expandableViewState = ExpandableViewState()
+ expandableViewState.headsUpIsVisible = true
+
+ stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState,
+ /* isShadeExpanded= */ true,
+ /* mustStayOnScreen= */ true,
+ /* isViewEndVisible= */ true,
+ /* viewEnd= */ 10f,
+ /* maxHunY= */ 0f)
+
+ assertFalse(expandableViewState.headsUpIsVisible)
+ }
+
+ @Test
+ fun maybeUpdateHeadsUpIsVisible_shadeClosed_noUpdate() {
+ val expandableViewState = ExpandableViewState()
+ expandableViewState.headsUpIsVisible = true
+
+ stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState,
+ /* isShadeExpanded= */ false,
+ /* mustStayOnScreen= */ true,
+ /* isViewEndVisible= */ true,
+ /* viewEnd= */ 10f,
+ /* maxHunY= */ 1f)
+
+ assertTrue(expandableViewState.headsUpIsVisible)
+ }
+
+ @Test
+ fun maybeUpdateHeadsUpIsVisible_notHUN_noUpdate() {
+ val expandableViewState = ExpandableViewState()
+ expandableViewState.headsUpIsVisible = true
+
+ stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState,
+ /* isShadeExpanded= */ true,
+ /* mustStayOnScreen= */ false,
+ /* isViewEndVisible= */ true,
+ /* viewEnd= */ 10f,
+ /* maxHunY= */ 1f)
+
+ assertTrue(expandableViewState.headsUpIsVisible)
+ }
+
+ @Test
+ fun maybeUpdateHeadsUpIsVisible_topHidden_noUpdate() {
+ val expandableViewState = ExpandableViewState()
+ expandableViewState.headsUpIsVisible = true
+
+ stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState,
+ /* isShadeExpanded= */ true,
+ /* mustStayOnScreen= */ true,
+ /* isViewEndVisible= */ false,
+ /* viewEnd= */ 10f,
+ /* maxHunY= */ 1f)
+
+ assertTrue(expandableViewState.headsUpIsVisible)
+ }
+
+ @Test
+ fun clampHunToTop_viewYGreaterThanQqs_viewYUnchanged() {
+ val expandableViewState = ExpandableViewState()
+ expandableViewState.yTranslation = 50f
+
+ stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f,
+ /* stackTranslation= */ 0f,
+ /* collapsedHeight= */ 1f, expandableViewState)
+
+ // qqs (10 + 0) < viewY (50)
+ assertEquals(50f, expandableViewState.yTranslation)
+ }
+
+ @Test
+ fun clampHunToTop_viewYLessThanQqs_viewYChanged() {
+ val expandableViewState = ExpandableViewState()
+ expandableViewState.yTranslation = -10f
+
+ stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f,
+ /* stackTranslation= */ 0f,
+ /* collapsedHeight= */ 1f, expandableViewState)
+
+ // qqs (10 + 0) > viewY (-10)
+ assertEquals(10f, expandableViewState.yTranslation)
+ }
+
+
+ @Test
+ fun clampHunToTop_viewYFarAboveVisibleStack_heightCollapsed() {
+ val expandableViewState = ExpandableViewState()
+ expandableViewState.height = 20
+ expandableViewState.yTranslation = -100f
+
+ stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f,
+ /* stackTranslation= */ 0f,
+ /* collapsedHeight= */ 10f, expandableViewState)
+
+ // newTranslation = max(10, -100) = 10
+ // distToRealY = 10 - (-100f) = 110
+ // height = max(20 - 110, 10f)
+ assertEquals(10, expandableViewState.height)
+ }
+
+ @Test
+ fun clampHunToTop_viewYNearVisibleStack_heightTallerThanCollapsed() {
+ val expandableViewState = ExpandableViewState()
+ expandableViewState.height = 20
+ expandableViewState.yTranslation = 5f
+
+ stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f,
+ /* stackTranslation= */ 0f,
+ /* collapsedHeight= */ 10f, expandableViewState)
+
+ // newTranslation = max(10, 5) = 10
+ // distToRealY = 10 - 5 = 5
+ // height = max(20 - 5, 10) = 15
+ assertEquals(15, expandableViewState.height)
+ }
+
+ @Test
+ fun computeCornerRoundnessForPinnedHun_stackBelowScreen_round() {
+ val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
+ /* hostViewHeight= */ 100f,
+ /* stackY= */ 110f,
+ /* viewMaxHeight= */ 20f,
+ /* originalCornerRoundness= */ 0f)
+ assertEquals(1f, currentRoundness)
+ }
+
+ @Test
+ fun computeCornerRoundnessForPinnedHun_stackAboveScreenBelowPinPoint_halfRound() {
+ val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
+ /* hostViewHeight= */ 100f,
+ /* stackY= */ 90f,
+ /* viewMaxHeight= */ 20f,
+ /* originalCornerRoundness= */ 0f)
+ assertEquals(0.5f, currentRoundness)
+ }
+
+ @Test
+ fun computeCornerRoundnessForPinnedHun_stackAbovePinPoint_notRound() {
+ val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
+ /* hostViewHeight= */ 100f,
+ /* stackY= */ 0f,
+ /* viewMaxHeight= */ 20f,
+ /* originalCornerRoundness= */ 0f)
+ assertEquals(0f, currentRoundness)
+ }
+
+ @Test
+ fun computeCornerRoundnessForPinnedHun_originallyRoundAndStackAbovePinPoint_round() {
+ val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
+ /* hostViewHeight= */ 100f,
+ /* stackY= */ 0f,
+ /* viewMaxHeight= */ 20f,
+ /* originalCornerRoundness= */ 1f)
+ assertEquals(1f, currentRoundness)
+ }
}