Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +73 −24 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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, Loading @@ -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); Loading @@ -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); } /** Loading Loading @@ -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. Loading Loading @@ -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); Loading @@ -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; Loading Loading @@ -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); Loading Loading @@ -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) { Loading packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt +175 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) } } Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +73 −24 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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, Loading @@ -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); Loading @@ -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); } /** Loading Loading @@ -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. Loading Loading @@ -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); Loading @@ -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; Loading Loading @@ -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); Loading Loading @@ -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) { Loading
packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt +175 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) } }