Merge "[flexiglass] Add nested scrolling to NSSL and notif scrim in Flexiglass" into main
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt
new file mode 100644
index 0000000..2ba78cf
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2024 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.notifications.ui.composable
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import com.android.compose.nestedscroll.PriorityNestedScrollConnection
+
+/**
+ * A [NestedScrollConnection] that listens for all vertical scroll events and responds in the
+ * following way:
+ * - If you **scroll up**, it **first brings the [scrimOffset]** back to the [minScrimOffset] and
+ * then allows scrolling of the children (usually the content).
+ * - If you **scroll down**, it **first allows scrolling of the children** (usually the content) and
+ * then resets the [scrimOffset] to [maxScrimOffset].
+ */
+fun NotificationScrimNestedScrollConnection(
+ scrimOffset: () -> Float,
+ onScrimOffsetChanged: (Float) -> Unit,
+ minScrimOffset: () -> Float,
+ maxScrimOffset: Float,
+ contentHeight: () -> Float,
+ minVisibleScrimHeight: () -> Float,
+): PriorityNestedScrollConnection {
+ return PriorityNestedScrollConnection(
+ orientation = Orientation.Vertical,
+ // scrolling up and inner content is taller than the scrim, so scrim needs to
+ // expand; content can scroll once scrim is at the minScrimOffset.
+ canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
+ offsetAvailable < 0 &&
+ offsetBeforeStart == 0f &&
+ contentHeight() > minVisibleScrimHeight() &&
+ scrimOffset() > minScrimOffset()
+ },
+ // scrolling down and content is done scrolling to top. After that, the scrim
+ // needs to collapse; collapse the scrim until it is at the maxScrimOffset.
+ canStartPostScroll = { offsetAvailable, _ ->
+ offsetAvailable > 0 && scrimOffset() < maxScrimOffset
+ },
+ canStartPostFling = { false },
+ canContinueScroll = {
+ val currentHeight = scrimOffset()
+ minScrimOffset() < currentHeight && currentHeight < maxScrimOffset
+ },
+ canScrollOnFling = true,
+ onStart = { /* do nothing */},
+ onScroll = { offsetAvailable ->
+ val currentHeight = scrimOffset()
+ val amountConsumed =
+ if (offsetAvailable > 0) {
+ val amountLeft = maxScrimOffset - currentHeight
+ offsetAvailable.coerceAtMost(amountLeft)
+ } else {
+ val amountLeft = minScrimOffset() - currentHeight
+ offsetAvailable.coerceAtLeast(amountLeft)
+ }
+ onScrimOffsetChanged(currentHeight + amountConsumed)
+ amountConsumed
+ },
+ // Don't consume the velocity on pre/post fling
+ onStop = { 0f },
+ )
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index e835d3e..0e08a19 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -20,32 +20,53 @@
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInWindow
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.NestedScrollBehavior
import com.android.compose.animation.scene.SceneScope
import com.android.compose.modifiers.height
import com.android.systemui.notifications.ui.composable.Notifications.Form
+import com.android.systemui.scene.ui.composable.Gone
+import com.android.systemui.scene.ui.composable.Shade
+import com.android.systemui.shade.ui.composable.ShadeHeader
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import kotlin.math.roundToInt
@@ -100,33 +121,109 @@
@Composable
fun SceneScope.NotificationScrollingStack(
viewModel: NotificationsPlaceholderViewModel,
+ maxScrimTop: () -> Float,
modifier: Modifier = Modifier,
) {
+ val density = LocalDensity.current
val cornerRadius by viewModel.cornerRadiusDp.collectAsState()
-
- val contentHeight by viewModel.intrinsicContentHeight.collectAsState()
-
val expansionFraction by viewModel.expandFraction.collectAsState(0f)
- Box(
- modifier =
- modifier
- .verticalNestedScrollToScene()
- .fillMaxWidth()
- .element(Notifications.Elements.NotificationScrim)
- .graphicsLayer {
- shape = RoundedCornerShape(cornerRadius.dp)
- clip = true
- alpha = expansionFraction
- }
- .background(MaterialTheme.colorScheme.surface)
- .debugBackground(viewModel, Color(0.5f, 0.5f, 0f, 0.2f))
- ) {
- NotificationPlaceholder(
- viewModel = viewModel,
- form = Form.Stack,
- modifier = Modifier.fillMaxWidth().height { contentHeight.roundToInt() }
+ val navBarHeight =
+ with(density) { WindowInsets.systemBars.asPaddingValues().calculateBottomPadding().toPx() }
+ val statusBarHeight =
+ with(density) { WindowInsets.systemBars.asPaddingValues().calculateTopPadding().toPx() }
+ val displayCutoutHeight =
+ with(density) { WindowInsets.displayCutout.asPaddingValues().calculateTopPadding().toPx() }
+ val screenHeight =
+ with(density) { LocalConfiguration.current.screenHeightDp.dp.toPx() } +
+ navBarHeight +
+ maxOf(statusBarHeight, displayCutoutHeight)
+
+ val contentHeight = viewModel.intrinsicContentHeight.collectAsState()
+
+ // the offset for the notifications scrim. Its upper bound is 0, and its lower bound is
+ // calculated in minScrimOffset. The scrim is the same height as the screen minus the
+ // height of the Shade Header, and at rest (scrimOffset = 0) its top bound is at maxScrimStartY.
+ // When fully expanded (scrimOffset = minScrimOffset), its top bound is at minScrimStartY,
+ // which is equal to the height of the Shade Header. Thus, when the scrim is fully expanded, the
+ // entire height of the scrim is visible on screen.
+ val scrimOffset = remember { mutableStateOf(0f) }
+
+ val minScrimTop = with(density) { ShadeHeader.Dimensions.CollapsedHeight.toPx() }
+
+ // The minimum offset for the scrim. The scrim is considered fully expanded when it
+ // is at this offset.
+ val minScrimOffset: () -> Float = { minScrimTop - maxScrimTop() }
+
+ // The height of the scrim visible on screen when it is in its resting (collapsed) state.
+ val minVisibleScrimHeight: () -> Float = { screenHeight - maxScrimTop() }
+
+ // we are not scrolled to the top unless the scrim is at its maximum offset.
+ LaunchedEffect(viewModel, scrimOffset) {
+ snapshotFlow { scrimOffset.value >= 0f }
+ .collect { isScrolledToTop -> viewModel.setScrolledToTop(isScrolledToTop) }
+ }
+
+ // if contentHeight drops below minimum visible scrim height while scrim is
+ // expanded, reset scrim offset.
+ LaunchedEffect(contentHeight, screenHeight, maxScrimTop, scrimOffset) {
+ snapshotFlow { contentHeight.value < minVisibleScrimHeight() && scrimOffset.value < 0f }
+ .collect { shouldCollapse -> if (shouldCollapse) scrimOffset.value = 0f }
+ }
+
+ Box(modifier = modifier.element(Notifications.Elements.NotificationScrim)) {
+ Spacer(
+ modifier =
+ Modifier.fillMaxSize()
+ .graphicsLayer {
+ shape = RoundedCornerShape(cornerRadius.dp)
+ clip = true
+ }
+ .drawBehind { drawRect(Color.Black, blendMode = BlendMode.DstOut) }
)
+ Box(
+ modifier =
+ Modifier.fillMaxSize()
+ .offset { IntOffset(0, scrimOffset.value.roundToInt()) }
+ .graphicsLayer {
+ shape = RoundedCornerShape(cornerRadius.dp)
+ clip = true
+ alpha =
+ if (layoutState.isTransitioningBetween(Gone, Shade)) {
+ (expansionFraction / 0.3f).coerceAtMost(1f)
+ } else 1f
+ }
+ .background(MaterialTheme.colorScheme.surface)
+ .debugBackground(viewModel, Color(0.5f, 0.5f, 0f, 0.2f))
+ ) {
+ NotificationPlaceholder(
+ viewModel = viewModel,
+ form = Form.Stack,
+ modifier =
+ Modifier.verticalNestedScrollToScene(
+ topBehavior = NestedScrollBehavior.EdgeWithPreview,
+ )
+ .nestedScroll(
+ remember(
+ scrimOffset,
+ maxScrimTop,
+ minScrimTop,
+ ) {
+ NotificationScrimNestedScrollConnection(
+ scrimOffset = { scrimOffset.value },
+ onScrimOffsetChanged = { scrimOffset.value = it },
+ minScrimOffset = minScrimOffset,
+ maxScrimOffset = 0f,
+ contentHeight = { contentHeight.value },
+ minVisibleScrimHeight = minVisibleScrimHeight,
+ )
+ }
+ )
+ .verticalScroll(rememberScrollState())
+ .fillMaxWidth()
+ .height { (contentHeight.value + navBarHeight).roundToInt() },
+ )
+ }
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index bbfe0fd..5531f9c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -36,6 +36,7 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
@@ -46,6 +47,11 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.boundsInWindow
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
@@ -56,7 +62,7 @@
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace
+import com.android.systemui.notifications.ui.composable.Notifications
import com.android.systemui.qs.footer.ui.compose.FooterActions
import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
import com.android.systemui.scene.shared.model.SceneKey
@@ -116,6 +122,8 @@
statusBarIconController: StatusBarIconController,
modifier: Modifier = Modifier,
) {
+ val cornerRadius by viewModel.notifications.cornerRadiusDp.collectAsState()
+
// TODO(b/280887232): implement the real UI.
Box(modifier = modifier.fillMaxSize()) {
val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
@@ -234,10 +242,32 @@
}
}
}
- HeadsUpNotificationSpace(
- viewModel = viewModel.notifications,
- isPeekFromBottom = true,
- modifier = Modifier.padding(16.dp).fillMaxSize(),
+ // Scrim with height 0 aligned to bottom of the screen to facilitate shared element
+ // transition from Shade scene.
+ Box(
+ modifier =
+ Modifier.element(Notifications.Elements.NotificationScrim)
+ .fillMaxWidth()
+ .height(0.dp)
+ .graphicsLayer {
+ shape = RoundedCornerShape(cornerRadius.dp)
+ clip = true
+ alpha = 1f
+ }
+ .background(MaterialTheme.colorScheme.surface)
+ .align(Alignment.BottomCenter)
+ .onPlaced { coordinates: LayoutCoordinates ->
+ viewModel.notifications.onContentTopChanged(
+ coordinates.positionInWindow().y
+ )
+ val boundsInWindow = coordinates.boundsInWindow()
+ viewModel.notifications.onBoundsChanged(
+ left = boundsInWindow.left,
+ top = boundsInWindow.top,
+ right = boundsInWindow.right,
+ bottom = boundsInWindow.bottom,
+ )
+ }
)
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
index 747faab..770d654 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
@@ -18,13 +18,10 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.SceneScope
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace
import com.android.systemui.notifications.ui.composable.Notifications
import com.android.systemui.scene.shared.model.Direction
import com.android.systemui.scene.shared.model.Edge
@@ -66,12 +63,6 @@
override fun SceneScope.Content(
modifier: Modifier,
) {
- Box(modifier = modifier) {
- Box(modifier = Modifier.fillMaxSize().element(Notifications.Elements.NotificationScrim))
- HeadsUpNotificationSpace(
- viewModel = notificationsViewModel,
- modifier = Modifier.padding(16.dp).fillMaxSize(),
- )
- }
+ Box(modifier = Modifier.fillMaxSize().element(Notifications.Elements.NotificationScrim))
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 1545372..497fe87 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -32,6 +32,7 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.dimensionResource
@@ -62,6 +63,7 @@
import com.android.systemui.util.animation.MeasurementInput
import javax.inject.Inject
import javax.inject.Named
+import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -154,62 +156,104 @@
mediaHost: MediaHost,
modifier: Modifier = Modifier,
) {
- val localDensity = LocalDensity.current
+ val density = LocalDensity.current
val layoutWidth = remember { mutableStateOf(0) }
+ val maxNotifScrimTop = remember { mutableStateOf(0f) }
Box(
modifier =
modifier.element(Shade.Elements.Scrim).background(MaterialTheme.colorScheme.scrim),
)
Box {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- modifier = Modifier.fillMaxWidth().clickable(onClick = { viewModel.onContentClicked() })
- ) {
- CollapsedShadeHeader(
- viewModel = viewModel.shadeHeaderViewModel,
- createTintedIconManager = createTintedIconManager,
- createBatteryMeterViewController = createBatteryMeterViewController,
- statusBarIconController = statusBarIconController,
- modifier = Modifier.padding(horizontal = Shade.Dimensions.HorizontalPadding)
- )
- QuickSettings(
- modifier = Modifier.height(130.dp),
- viewModel.qsSceneAdapter,
- )
-
- if (viewModel.isMediaVisible()) {
- val mediaHeight = dimensionResource(R.dimen.qs_media_session_height_expanded)
- MediaCarousel(
- modifier =
- Modifier.height(mediaHeight).fillMaxWidth().layout { measurable, constraints
- ->
- val placeable = measurable.measure(constraints)
-
- // Notify controller to size the carousel for the current space
- mediaHost.measurementInput =
- MeasurementInput(placeable.width, placeable.height)
- mediaCarouselController.setSceneContainerSize(
- placeable.width,
- placeable.height
+ Layout(
+ contents =
+ listOf(
+ {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier =
+ Modifier.fillMaxWidth()
+ .clickable(onClick = { viewModel.onContentClicked() })
+ ) {
+ CollapsedShadeHeader(
+ viewModel = viewModel.shadeHeaderViewModel,
+ createTintedIconManager = createTintedIconManager,
+ createBatteryMeterViewController = createBatteryMeterViewController,
+ statusBarIconController = statusBarIconController,
+ modifier =
+ Modifier.padding(
+ horizontal = Shade.Dimensions.HorizontalPadding
+ )
+ )
+ QuickSettings(
+ modifier = Modifier.height(130.dp),
+ viewModel.qsSceneAdapter,
)
- layout(placeable.width, placeable.height) {
- placeable.placeRelative(0, 0)
- }
- },
- mediaHost = mediaHost,
- layoutWidth = layoutWidth.value,
- layoutHeight = with(localDensity) { mediaHeight.toPx() }.toInt(),
- carouselController = mediaCarouselController,
- )
- }
+ if (viewModel.isMediaVisible()) {
+ val mediaHeight =
+ dimensionResource(R.dimen.qs_media_session_height_expanded)
+ MediaCarousel(
+ modifier =
+ Modifier.height(mediaHeight).fillMaxWidth().layout {
+ measurable,
+ constraints ->
+ val placeable = measurable.measure(constraints)
- Spacer(modifier = Modifier.height(16.dp))
- NotificationScrollingStack(
- viewModel = viewModel.notifications,
- modifier = Modifier.fillMaxWidth().weight(1f),
- )
+ // Notify controller to size the carousel for the
+ // current space
+ mediaHost.measurementInput =
+ MeasurementInput(placeable.width, placeable.height)
+ mediaCarouselController.setSceneContainerSize(
+ placeable.width,
+ placeable.height
+ )
+
+ layout(placeable.width, placeable.height) {
+ placeable.placeRelative(0, 0)
+ }
+ },
+ mediaHost = mediaHost,
+ layoutWidth = layoutWidth.value,
+ layoutHeight = with(density) { mediaHeight.toPx() }.toInt(),
+ carouselController = mediaCarouselController,
+ )
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ },
+ {
+ NotificationScrollingStack(
+ viewModel = viewModel.notifications,
+ maxScrimTop = { maxNotifScrimTop.value },
+ )
+ },
+ )
+ ) { measurables, constraints ->
+ check(measurables.size == 2)
+ check(measurables[0].size == 1)
+ check(measurables[1].size == 1)
+
+ val quickSettingsPlaceable = measurables[0][0].measure(constraints)
+
+ val notificationsMeasurable = measurables[1][0]
+ val notificationsScrimMaxHeight =
+ constraints.maxHeight - ShadeHeader.Dimensions.CollapsedHeight.roundToPx()
+ val notificationsPlaceable =
+ notificationsMeasurable.measure(
+ constraints.copy(
+ minHeight = notificationsScrimMaxHeight,
+ maxHeight = notificationsScrimMaxHeight
+ )
+ )
+
+ maxNotifScrimTop.value = quickSettingsPlaceable.height.toFloat()
+
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ quickSettingsPlaceable.placeRelative(x = 0, y = 0)
+ notificationsPlaceable.placeRelative(x = 0, y = maxNotifScrimTop.value.roundToInt())
+ }
}
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
index 6a2e317..4d7d5d3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
@@ -87,21 +87,21 @@
}
@Test
- fun updateShadeExpansion() =
+ fun shadeExpansion_goneToShade() =
testScope.runTest {
+ val transitionState =
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Idle(scene = SceneKey.Gone)
+ )
+ sceneInteractor.setTransitionState(transitionState)
val expandFraction by collectLastValue(appearanceViewModel.expandFraction)
assertThat(expandFraction).isEqualTo(0f)
- val transitionState =
- MutableStateFlow<ObservableTransitionState>(
- ObservableTransitionState.Idle(scene = SceneKey.Lockscreen)
- )
- sceneInteractor.setTransitionState(transitionState)
sceneInteractor.changeScene(SceneModel(SceneKey.Shade), "reason")
val transitionProgress = MutableStateFlow(0f)
transitionState.value =
ObservableTransitionState.Transition(
- fromScene = SceneKey.Lockscreen,
+ fromScene = SceneKey.Gone,
toScene = SceneKey.Shade,
progress = transitionProgress,
isInitiatedByUserInput = false,
@@ -118,4 +118,49 @@
sceneInteractor.onSceneChanged(SceneModel(SceneKey.Shade), "reason")
assertThat(expandFraction).isWithin(0.01f).of(1f)
}
+
+ @Test
+ fun shadeExpansion_idleOnLockscreen() =
+ testScope.runTest {
+ val transitionState =
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Idle(scene = SceneKey.Lockscreen)
+ )
+ sceneInteractor.setTransitionState(transitionState)
+ val expandFraction by collectLastValue(appearanceViewModel.expandFraction)
+ assertThat(expandFraction).isEqualTo(1f)
+ }
+
+ @Test
+ fun shadeExpansion_shadeToQs() =
+ testScope.runTest {
+ val transitionState =
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Idle(scene = SceneKey.Shade)
+ )
+ sceneInteractor.setTransitionState(transitionState)
+ val expandFraction by collectLastValue(appearanceViewModel.expandFraction)
+ assertThat(expandFraction).isEqualTo(1f)
+
+ sceneInteractor.changeScene(SceneModel(SceneKey.QuickSettings), "reason")
+ val transitionProgress = MutableStateFlow(0f)
+ transitionState.value =
+ ObservableTransitionState.Transition(
+ fromScene = SceneKey.Shade,
+ toScene = SceneKey.QuickSettings,
+ progress = transitionProgress,
+ isInitiatedByUserInput = false,
+ isUserInputOngoing = flowOf(false),
+ )
+ val steps = 10
+ repeat(steps) { repetition ->
+ val progress = (1f / steps) * (repetition + 1)
+ transitionProgress.value = progress
+ runCurrent()
+ assertThat(expandFraction).isEqualTo(1f)
+ }
+
+ sceneInteractor.onSceneChanged(SceneModel(SceneKey.QuickSettings), "reason")
+ assertThat(expandFraction).isEqualTo(1f)
+ }
}
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 dd04531..b9afb14 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
@@ -617,7 +617,11 @@
private final ScrollAdapter mScrollAdapter = new ScrollAdapter() {
@Override
public boolean isScrolledToTop() {
- return mOwnScrollY == 0;
+ if (SceneContainerFlag.isEnabled()) {
+ return mController.isPlaceholderScrolledToTop();
+ } else {
+ return mOwnScrollY == 0;
+ }
}
@Override
@@ -1442,7 +1446,14 @@
fraction = BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(fraction);
}
final float stackY = MathUtils.lerp(0, endTopPosition, fraction);
- mAmbientState.setStackY(stackY);
+ // TODO(b/322228881): Clean up scene container vs legacy behavior in NSSL
+ if (SceneContainerFlag.isEnabled()) {
+ // stackY should be driven by scene container, not NSSL
+ mAmbientState.setStackY(mTopPadding);
+ } else {
+ mAmbientState.setStackY(stackY);
+ }
+
if (mOnStackYChanged != null) {
mOnStackYChanged.accept(listenerNeedsAnimation);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 49fde39..ed26677 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -1144,6 +1144,14 @@
return mStackAppearanceInteractor.getStackBounds().getValue().getTop();
}
+ /**
+ * Returns whether the notification stack is scrolled to the top; i.e., it cannot be scrolled
+ * down any further.
+ */
+ public boolean isPlaceholderScrolledToTop() {
+ return mStackAppearanceInteractor.getScrolledToTop().getValue();
+ }
+
/** Set the intrinsic height of the stack content without additional padding. */
public void setIntrinsicContentHeight(float intrinsicContentHeight) {
mStackAppearanceInteractor.setIntrinsicContentHeight(intrinsicContentHeight);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
index aac3c28..0197264 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
@@ -44,4 +44,10 @@
* screen.
*/
val contentTop = MutableStateFlow(0f)
+
+ /**
+ * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any
+ * further.
+ */
+ val scrolledToTop = MutableStateFlow(true)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
index 1dfde09..8307397 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
@@ -42,10 +42,16 @@
* notifications, this can exceed the space available on screen to show notifications, at which
* point the notification stack should become scrollable.
*/
- val intrinsicContentHeight = repository.intrinsicContentHeight.asStateFlow()
+ val intrinsicContentHeight: StateFlow<Float> = repository.intrinsicContentHeight.asStateFlow()
/** The y-coordinate in px of top of the contents of the notification stack. */
- val contentTop = repository.contentTop.asStateFlow()
+ val contentTop: StateFlow<Float> = repository.contentTop.asStateFlow()
+
+ /**
+ * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any
+ * further.
+ */
+ val scrolledToTop: StateFlow<Boolean> = repository.scrolledToTop.asStateFlow()
/** Sets the position of the notification stack in the current scene. */
fun setStackBounds(bounds: NotificationContainerBounds) {
@@ -62,4 +68,9 @@
fun setContentTop(startY: Float) {
repository.contentTop.value = startY
}
+
+ /** Sets whether the notification stack is scrolled to the top. */
+ fun setScrolledToTop(scrolledToTop: Boolean) {
+ repository.scrolledToTop.value = scrolledToTop
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
index ed15f55..6c2cbbe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
@@ -25,7 +25,6 @@
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
-import kotlin.math.pow
import kotlin.math.roundToInt
import kotlinx.coroutines.launch
@@ -65,7 +64,9 @@
viewModel.expandFraction.collect { expandFraction ->
ambientState.expansionFraction = expandFraction
controller.expandedHeight = expandFraction * controller.view.height
- controller.setMaxAlphaForExpansion(expandFraction.pow(0.75f))
+ controller.setMaxAlphaForExpansion(
+ ((expandFraction - 0.5f) / 0.5f).coerceAtLeast(0f)
+ )
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
index 74db583..56ff7f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
@@ -19,11 +19,15 @@
import com.android.systemui.common.shared.model.NotificationContainerBounds
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.ObservableTransitionState
+import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
/** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */
@SysUISingleton
@@ -32,9 +36,40 @@
constructor(
stackAppearanceInteractor: NotificationStackAppearanceInteractor,
shadeInteractor: ShadeInteractor,
+ sceneInteractor: SceneInteractor,
) {
- /** The expansion fraction from the top of the notification shade. */
- val expandFraction: Flow<Float> = shadeInteractor.shadeExpansion
+ /**
+ * The expansion fraction of the notification stack. It should go from 0 to 1 when transitioning
+ * from Gone to Shade scenes, and remain at 1 when in Lockscreen or Shade scenes and while
+ * transitioning from Shade to QuickSettings scenes.
+ */
+ val expandFraction: Flow<Float> =
+ combine(
+ shadeInteractor.shadeExpansion,
+ sceneInteractor.transitionState,
+ ) { shadeExpansion, transitionState ->
+ when (transitionState) {
+ is ObservableTransitionState.Idle -> {
+ if (transitionState.scene == SceneKey.Lockscreen) {
+ 1f
+ } else {
+ shadeExpansion
+ }
+ }
+ is ObservableTransitionState.Transition -> {
+ if (
+ (transitionState.fromScene == SceneKey.Shade &&
+ transitionState.toScene == SceneKey.QuickSettings) ||
+ (transitionState.fromScene == SceneKey.QuickSettings &&
+ transitionState.toScene == SceneKey.Shade)
+ ) {
+ 1f
+ } else {
+ shadeExpansion
+ }
+ }
+ }
+ }
/** The bounds of the notification stack in the current scene. */
val stackBounds: Flow<NotificationContainerBounds> = stackAppearanceInteractor.stackBounds
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
index 385f061..a436f17 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
@@ -87,4 +87,9 @@
fun onContentTopChanged(padding: Float) {
interactor.setContentTop(padding)
}
+
+ /** Sets whether the notification stack is scrolled to the top. */
+ fun setScrolledToTop(scrolledToTop: Boolean) {
+ interactor.setScrolledToTop(scrolledToTop)
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt
index f2f3a5a..d79633a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt
@@ -18,6 +18,7 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
@@ -25,5 +26,6 @@
NotificationStackAppearanceViewModel(
stackAppearanceInteractor = notificationStackAppearanceInteractor,
shadeInteractor = shadeInteractor,
+ sceneInteractor = sceneInteractor,
)
}