Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +56 −26 Original line number Diff line number Diff line Loading @@ -62,7 +62,8 @@ public class StackScrollAlgorithm { private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState(); private boolean mIsExpanded; private boolean mClipNotificationScrollToTop; @VisibleForTesting float mHeadsUpInset; @VisibleForTesting float mHeadsUpInset; private int mPinnedZTranslationExtra; private float mNotificationScrimPadding; private int mMarginBottom; Loading Loading @@ -504,6 +505,7 @@ public class StackScrollAlgorithm { } // TODO(b/172289889) polish shade open from HUN /** * Populates the {@link ExpandableViewState} for a single child. * Loading Loading @@ -862,30 +864,53 @@ public class StackScrollAlgorithm { } } /** * Calculate and update the Z positions for a given child. We currently only give shadows to * HUNs to distinguish a HUN from its surroundings. * * @param isTopHun Whether the child is a top HUN. A top HUN means a HUN that shows on the * vertically top of screen. Top HUNs should have drop shadows * @param childrenOnTop It is greater than 0 when there's an existing HUN that is elevated * @return childrenOnTop The decimal part represents the fraction of the elevated HUN's height * that overlaps with QQS Panel. The integer part represents the count of * previous HUNs whose Z positions are greater than 0. */ protected float updateChildZValue(int i, float childrenOnTop, StackScrollAlgorithmState algorithmState, AmbientState ambientState, boolean shouldElevateHun) { boolean isTopHun) { ExpandableView child = algorithmState.visibleChildren.get(i); ExpandableViewState childViewState = child.getViewState(); int zDistanceBetweenElements = ambientState.getZDistanceBetweenElements(); float baseZ = ambientState.getBaseZHeight(); // Handles HUN shadow when Shade is opened if (child.mustStayOnScreen() && !childViewState.headsUpIsVisible && !ambientState.isDozingAndNotPulsing(child) && childViewState.getYTranslation() < ambientState.getTopPadding() + ambientState.getStackTranslation()) { // Handles HUN shadow when Shade is opened, and AmbientState.mScrollY > 0 // Calculate the HUN's z-value based on its overlapping fraction with QQS Panel. // When scrolling down shade to make HUN back to in-position in Notification Panel, // The over-lapping fraction goes to 0, and shadows hides gradually. if (childrenOnTop != 0.0f) { // To elevate the later HUN over previous HUN childrenOnTop++; } else { float overlap = ambientState.getTopPadding() + ambientState.getStackTranslation() - childViewState.getYTranslation(); childrenOnTop += Math.min(1.0f, overlap / childViewState.height); // To prevent over-shadow during HUN entry childrenOnTop += Math.min( 1.0f, overlap / childViewState.height ); MathUtils.saturate(childrenOnTop); } childViewState.setZTranslation(baseZ + childrenOnTop * zDistanceBetweenElements); } else if (shouldElevateHun) { + childrenOnTop * mPinnedZTranslationExtra); } else if (isTopHun) { // In case this is a new view that has never been measured before, we don't want to // elevate if we are currently expanded more then the notification // elevate if we are currently expanded more than the notification int shelfHeight = ambientState.getShelf() == null ? 0 : ambientState.getShelf().getIntrinsicHeight(); float shelfStart = ambientState.getInnerHeight() Loading @@ -894,23 +919,28 @@ public class StackScrollAlgorithm { float notificationEnd = childViewState.getYTranslation() + child.getIntrinsicHeight() + mPaddingBetweenElements; if (shelfStart > notificationEnd) { // When the notification doesn't overlap with Notification Shelf, there's no shadow childViewState.setZTranslation(baseZ); } else { // Give shadow to the notification if it overlaps with Notification Shelf float factor = (notificationEnd - shelfStart) / shelfHeight; if (Float.isNaN(factor)) { // Avoid problems when the above is 0/0. factor = 1.0f; } factor = Math.min(factor, 1.0f); childViewState.setZTranslation(baseZ + factor * zDistanceBetweenElements); childViewState.setZTranslation(baseZ + factor * mPinnedZTranslationExtra); } } else { childViewState.setZTranslation(baseZ); } // We need to scrim the notification more from its surrounding content when we are pinned, // and we therefore elevate it higher. // We can use the headerVisibleAmount for this, since the value nicely goes from 0 to 1 when // expanding after which we have a normal elevation again. // Handles HUN shadow when shade is closed. // While HUN is showing and Shade is closed: headerVisibleAmount stays 0, shadow stays. // During HUN-to-Shade (eg. dragging down HUN to open Shade): headerVisibleAmount goes // gradually from 0 to 1, shadow hides gradually. // Header visibility is a deprecated concept, we are using headerVisibleAmount only because // this value nicely goes from 0 to 1 during the HUN-to-Shade process. childViewState.setZTranslation(childViewState.getZTranslation() + (1.0f - child.getHeaderVisibleAmount()) * mPinnedZTranslationExtra); return childrenOnTop; Loading packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt +193 −6 Original line number Diff line number Diff line Loading @@ -14,6 +14,7 @@ import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse Loading @@ -31,10 +32,10 @@ class StackScrollAlgorithmTest : SysuiTestCase() { private val hostView = FrameLayout(context) private val stackScrollAlgorithm = StackScrollAlgorithm(context, hostView) private val notificationRow = mock(ExpandableNotificationRow::class.java) private val dumpManager = mock(DumpManager::class.java) private val mStatusBarKeyguardViewManager = mock(StatusBarKeyguardViewManager::class.java) private val notificationShelf = mock(NotificationShelf::class.java) private val notificationRow = mock<ExpandableNotificationRow>() private val dumpManager = mock<DumpManager>() 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) } Loading @@ -46,7 +47,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { mStatusBarKeyguardViewManager ) private val testableResources = mContext.orCreateTestableResources private val testableResources = mContext.getOrCreateTestableResources() private fun px(@DimenRes id: Int): Float = testableResources.resources.getDimensionPixelSize(id).toFloat() Loading Loading @@ -507,6 +508,192 @@ class StackScrollAlgorithmTest : SysuiTestCase() { assertEquals(1f, currentRoundness) } @Test fun shadeOpened_hunFullyOverlapsQqsPanel_hunShouldHaveFullShadow() { // 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 ) 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 ) // Then: full shadow would be applied assertEquals(px(R.dimen.heads_up_pinned_elevation), childHunView.viewState.zTranslation) } @Test fun shadeOpened_hunPartiallyOverlapsQQS_hunShouldHavePartialShadow() { // 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 ) // Use half of the HUN's height as overlap childHunView.viewState.yTranslation = (childHunView.viewState.height + 1 shr 1).toFloat() 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 ) // 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)) } @Test fun shadeOpened_hunDoesNotOverlapQQS_hunShouldHaveNoShadow() { // 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) // Mock the height of shade ambientState.setLayoutMinHeight(1000) val childHunView = createHunViewMock( isShadeOpen = true, fullyVisible = true, headerVisibleAmount = 1f ) // HUN doesn't overlap with QQS Panel 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 ) // Then: HUN should not have shadow assertEquals(0f, childHunView.viewState.zTranslation) } @Test fun shadeClosed_hunShouldHaveFullShadow() { // Given: shade is closed, ambientState.stackTranslation == -ambientState.topPadding, // the height of HUN is equal to the height of QQS Panel, ambientState.stackTranslation = -ambientState.topPadding // Mock the height of shade ambientState.setLayoutMinHeight(1000) 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 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 ) // Then: HUN should have full shadow assertEquals(px(R.dimen.heads_up_pinned_elevation), childHunView.viewState.zTranslation) } @Test fun draggingHunToOpenShade_hunShouldHavePartialShadow() { // Given: shade is closed when HUN pops up, // now drags down the HUN to open shade ambientState.stackTranslation = -ambientState.topPadding // Mock the height of shade ambientState.setLayoutMinHeight(1000) 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 childHunView.headerVisibleAmount = 0.5f 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 ) // 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)) } 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) } 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 resetViewStates_expansionChanging_notificationAlphaUpdated( expansionFraction: Float, expectedAlpha: Float Loading Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +56 −26 Original line number Diff line number Diff line Loading @@ -62,7 +62,8 @@ public class StackScrollAlgorithm { private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState(); private boolean mIsExpanded; private boolean mClipNotificationScrollToTop; @VisibleForTesting float mHeadsUpInset; @VisibleForTesting float mHeadsUpInset; private int mPinnedZTranslationExtra; private float mNotificationScrimPadding; private int mMarginBottom; Loading Loading @@ -504,6 +505,7 @@ public class StackScrollAlgorithm { } // TODO(b/172289889) polish shade open from HUN /** * Populates the {@link ExpandableViewState} for a single child. * Loading Loading @@ -862,30 +864,53 @@ public class StackScrollAlgorithm { } } /** * Calculate and update the Z positions for a given child. We currently only give shadows to * HUNs to distinguish a HUN from its surroundings. * * @param isTopHun Whether the child is a top HUN. A top HUN means a HUN that shows on the * vertically top of screen. Top HUNs should have drop shadows * @param childrenOnTop It is greater than 0 when there's an existing HUN that is elevated * @return childrenOnTop The decimal part represents the fraction of the elevated HUN's height * that overlaps with QQS Panel. The integer part represents the count of * previous HUNs whose Z positions are greater than 0. */ protected float updateChildZValue(int i, float childrenOnTop, StackScrollAlgorithmState algorithmState, AmbientState ambientState, boolean shouldElevateHun) { boolean isTopHun) { ExpandableView child = algorithmState.visibleChildren.get(i); ExpandableViewState childViewState = child.getViewState(); int zDistanceBetweenElements = ambientState.getZDistanceBetweenElements(); float baseZ = ambientState.getBaseZHeight(); // Handles HUN shadow when Shade is opened if (child.mustStayOnScreen() && !childViewState.headsUpIsVisible && !ambientState.isDozingAndNotPulsing(child) && childViewState.getYTranslation() < ambientState.getTopPadding() + ambientState.getStackTranslation()) { // Handles HUN shadow when Shade is opened, and AmbientState.mScrollY > 0 // Calculate the HUN's z-value based on its overlapping fraction with QQS Panel. // When scrolling down shade to make HUN back to in-position in Notification Panel, // The over-lapping fraction goes to 0, and shadows hides gradually. if (childrenOnTop != 0.0f) { // To elevate the later HUN over previous HUN childrenOnTop++; } else { float overlap = ambientState.getTopPadding() + ambientState.getStackTranslation() - childViewState.getYTranslation(); childrenOnTop += Math.min(1.0f, overlap / childViewState.height); // To prevent over-shadow during HUN entry childrenOnTop += Math.min( 1.0f, overlap / childViewState.height ); MathUtils.saturate(childrenOnTop); } childViewState.setZTranslation(baseZ + childrenOnTop * zDistanceBetweenElements); } else if (shouldElevateHun) { + childrenOnTop * mPinnedZTranslationExtra); } else if (isTopHun) { // In case this is a new view that has never been measured before, we don't want to // elevate if we are currently expanded more then the notification // elevate if we are currently expanded more than the notification int shelfHeight = ambientState.getShelf() == null ? 0 : ambientState.getShelf().getIntrinsicHeight(); float shelfStart = ambientState.getInnerHeight() Loading @@ -894,23 +919,28 @@ public class StackScrollAlgorithm { float notificationEnd = childViewState.getYTranslation() + child.getIntrinsicHeight() + mPaddingBetweenElements; if (shelfStart > notificationEnd) { // When the notification doesn't overlap with Notification Shelf, there's no shadow childViewState.setZTranslation(baseZ); } else { // Give shadow to the notification if it overlaps with Notification Shelf float factor = (notificationEnd - shelfStart) / shelfHeight; if (Float.isNaN(factor)) { // Avoid problems when the above is 0/0. factor = 1.0f; } factor = Math.min(factor, 1.0f); childViewState.setZTranslation(baseZ + factor * zDistanceBetweenElements); childViewState.setZTranslation(baseZ + factor * mPinnedZTranslationExtra); } } else { childViewState.setZTranslation(baseZ); } // We need to scrim the notification more from its surrounding content when we are pinned, // and we therefore elevate it higher. // We can use the headerVisibleAmount for this, since the value nicely goes from 0 to 1 when // expanding after which we have a normal elevation again. // Handles HUN shadow when shade is closed. // While HUN is showing and Shade is closed: headerVisibleAmount stays 0, shadow stays. // During HUN-to-Shade (eg. dragging down HUN to open Shade): headerVisibleAmount goes // gradually from 0 to 1, shadow hides gradually. // Header visibility is a deprecated concept, we are using headerVisibleAmount only because // this value nicely goes from 0 to 1 during the HUN-to-Shade process. childViewState.setZTranslation(childViewState.getZTranslation() + (1.0f - child.getHeaderVisibleAmount()) * mPinnedZTranslationExtra); return childrenOnTop; Loading
packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt +193 −6 Original line number Diff line number Diff line Loading @@ -14,6 +14,7 @@ import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse Loading @@ -31,10 +32,10 @@ class StackScrollAlgorithmTest : SysuiTestCase() { private val hostView = FrameLayout(context) private val stackScrollAlgorithm = StackScrollAlgorithm(context, hostView) private val notificationRow = mock(ExpandableNotificationRow::class.java) private val dumpManager = mock(DumpManager::class.java) private val mStatusBarKeyguardViewManager = mock(StatusBarKeyguardViewManager::class.java) private val notificationShelf = mock(NotificationShelf::class.java) private val notificationRow = mock<ExpandableNotificationRow>() private val dumpManager = mock<DumpManager>() 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) } Loading @@ -46,7 +47,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { mStatusBarKeyguardViewManager ) private val testableResources = mContext.orCreateTestableResources private val testableResources = mContext.getOrCreateTestableResources() private fun px(@DimenRes id: Int): Float = testableResources.resources.getDimensionPixelSize(id).toFloat() Loading Loading @@ -507,6 +508,192 @@ class StackScrollAlgorithmTest : SysuiTestCase() { assertEquals(1f, currentRoundness) } @Test fun shadeOpened_hunFullyOverlapsQqsPanel_hunShouldHaveFullShadow() { // 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 ) 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 ) // Then: full shadow would be applied assertEquals(px(R.dimen.heads_up_pinned_elevation), childHunView.viewState.zTranslation) } @Test fun shadeOpened_hunPartiallyOverlapsQQS_hunShouldHavePartialShadow() { // 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 ) // Use half of the HUN's height as overlap childHunView.viewState.yTranslation = (childHunView.viewState.height + 1 shr 1).toFloat() 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 ) // 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)) } @Test fun shadeOpened_hunDoesNotOverlapQQS_hunShouldHaveNoShadow() { // 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) // Mock the height of shade ambientState.setLayoutMinHeight(1000) val childHunView = createHunViewMock( isShadeOpen = true, fullyVisible = true, headerVisibleAmount = 1f ) // HUN doesn't overlap with QQS Panel 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 ) // Then: HUN should not have shadow assertEquals(0f, childHunView.viewState.zTranslation) } @Test fun shadeClosed_hunShouldHaveFullShadow() { // Given: shade is closed, ambientState.stackTranslation == -ambientState.topPadding, // the height of HUN is equal to the height of QQS Panel, ambientState.stackTranslation = -ambientState.topPadding // Mock the height of shade ambientState.setLayoutMinHeight(1000) 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 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 ) // Then: HUN should have full shadow assertEquals(px(R.dimen.heads_up_pinned_elevation), childHunView.viewState.zTranslation) } @Test fun draggingHunToOpenShade_hunShouldHavePartialShadow() { // Given: shade is closed when HUN pops up, // now drags down the HUN to open shade ambientState.stackTranslation = -ambientState.topPadding // Mock the height of shade ambientState.setLayoutMinHeight(1000) 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 childHunView.headerVisibleAmount = 0.5f 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 ) // 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)) } 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) } 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 resetViewStates_expansionChanging_notificationAlphaUpdated( expansionFraction: Float, expectedAlpha: Float Loading