[flexiglass] Add nested scrolling to NSSL and notif scrim in Flexiglass
Allows NSSL to scroll inside the notification scrim when its contents exceed the height of the scrim, and the scrim to expand to cover the entire screen (excluding the Shade Header) as the NSSL scrolls.
- The scrim is able to expand even when NSSL itself isn't scrollable (i.e. when its contents exceed the height of the "collapsed" scrim but not the height of the "expanded" scrim).
- Single-finger swipes to expand notifications instead scroll the NSSL/scrim when the scrim is expanded.
- While the scrim is expanded, if the NSSL's contents become shorter than the default collapsed scrim height (usually due to notifications being dismissed or collapsed), the scrim will collapse.
Touch handling on notifications that have just been received is fixed in this CL as well.
Bug: 296118689
Test: TBD
Flag: ACONFIG com.android.systemui.scene_container DEVELOPMENT
Change-Id: If2620821b7493dfa37e478eb3be8c1de34ca541e
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 04db653..90f2cd8 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 1143481..c4b5dc3 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,
)
}