Loading packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt 0 → 100644 +77 −0 Original line number Diff line number Diff line /* * 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 }, ) } packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +118 −21 Original line number Diff line number Diff line Loading @@ -20,32 +20,53 @@ package com.android.systemui.notifications.ui.composable 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 Loading Loading @@ -100,24 +121,77 @@ fun SceneScope.NotificationStack( @Composable fun SceneScope.NotificationScrollingStack( viewModel: NotificationsPlaceholderViewModel, maxScrimTop: () -> Float, modifier: Modifier = Modifier, ) { val density = LocalDensity.current val cornerRadius by viewModel.cornerRadiusDp.collectAsState() val expansionFraction by viewModel.expandFraction.collectAsState(0f) val contentHeight by viewModel.intrinsicContentHeight.collectAsState() 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 expansionFraction by viewModel.expandFraction.collectAsState(0f) 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 .verticalNestedScrollToScene() .fillMaxWidth() .element(Notifications.Elements.NotificationScrim) Modifier.fillMaxSize() .offset { IntOffset(0, scrimOffset.value.roundToInt()) } .graphicsLayer { shape = RoundedCornerShape(cornerRadius.dp) clip = true alpha = expansionFraction 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)) Loading @@ -125,8 +199,31 @@ fun SceneScope.NotificationScrollingStack( NotificationPlaceholder( viewModel = viewModel, form = Form.Stack, modifier = Modifier.fillMaxWidth().height { contentHeight.roundToInt() } 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() }, ) } } } Loading packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +35 −5 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ import androidx.compose.foundation.layout.height 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 Loading @@ -46,6 +47,11 @@ import androidx.compose.runtime.getValue 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 Loading @@ -56,7 +62,7 @@ import com.android.systemui.battery.BatteryMeterViewController 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 Loading Loading @@ -116,6 +122,8 @@ private fun SceneScope.QuickSettingsScene( 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() Loading Loading @@ -234,10 +242,32 @@ private fun SceneScope.QuickSettingsScene( } } } 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, ) } ) } } packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt +1 −10 Original line number Diff line number Diff line Loading @@ -18,13 +18,10 @@ package com.android.systemui.scene.ui.composable 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 Loading Loading @@ -66,12 +63,6 @@ constructor( 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(), ) } } } packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +88 −44 Original line number Diff line number Diff line Loading @@ -32,6 +32,7 @@ import androidx.compose.runtime.mutableStateOf 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 Loading Loading @@ -62,6 +63,7 @@ import com.android.systemui.statusbar.phone.StatusBarLocation 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 Loading Loading @@ -154,24 +156,34 @@ private fun SceneScope.ShadeScene( 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 { Layout( contents = listOf( { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth().clickable(onClick = { viewModel.onContentClicked() }) modifier = Modifier.fillMaxWidth() .clickable(onClick = { viewModel.onContentClicked() }) ) { CollapsedShadeHeader( viewModel = viewModel.shadeHeaderViewModel, createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, modifier = Modifier.padding(horizontal = Shade.Dimensions.HorizontalPadding) modifier = Modifier.padding( horizontal = Shade.Dimensions.HorizontalPadding ) ) QuickSettings( modifier = Modifier.height(130.dp), Loading @@ -179,14 +191,17 @@ private fun SceneScope.ShadeScene( ) if (viewModel.isMediaVisible()) { val mediaHeight = dimensionResource(R.dimen.qs_media_session_height_expanded) val mediaHeight = dimensionResource(R.dimen.qs_media_session_height_expanded) MediaCarousel( modifier = Modifier.height(mediaHeight).fillMaxWidth().layout { measurable, constraints -> Modifier.height(mediaHeight).fillMaxWidth().layout { measurable, constraints -> val placeable = measurable.measure(constraints) // Notify controller to size the carousel for the current space // Notify controller to size the carousel for the // current space mediaHost.measurementInput = MeasurementInput(placeable.width, placeable.height) mediaCarouselController.setSceneContainerSize( Loading @@ -200,16 +215,45 @@ private fun SceneScope.ShadeScene( }, mediaHost = mediaHost, layoutWidth = layoutWidth.value, layoutHeight = with(localDensity) { mediaHeight.toPx() }.toInt(), layoutHeight = with(density) { mediaHeight.toPx() }.toInt(), carouselController = mediaCarouselController, ) } Spacer(modifier = Modifier.height(16.dp)) } }, { NotificationScrollingStack( viewModel = viewModel.notifications, modifier = Modifier.fillMaxWidth().weight(1f), 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()) } } } } Loading
packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt 0 → 100644 +77 −0 Original line number Diff line number Diff line /* * 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 }, ) }
packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +118 −21 Original line number Diff line number Diff line Loading @@ -20,32 +20,53 @@ package com.android.systemui.notifications.ui.composable 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 Loading Loading @@ -100,24 +121,77 @@ fun SceneScope.NotificationStack( @Composable fun SceneScope.NotificationScrollingStack( viewModel: NotificationsPlaceholderViewModel, maxScrimTop: () -> Float, modifier: Modifier = Modifier, ) { val density = LocalDensity.current val cornerRadius by viewModel.cornerRadiusDp.collectAsState() val expansionFraction by viewModel.expandFraction.collectAsState(0f) val contentHeight by viewModel.intrinsicContentHeight.collectAsState() 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 expansionFraction by viewModel.expandFraction.collectAsState(0f) 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 .verticalNestedScrollToScene() .fillMaxWidth() .element(Notifications.Elements.NotificationScrim) Modifier.fillMaxSize() .offset { IntOffset(0, scrimOffset.value.roundToInt()) } .graphicsLayer { shape = RoundedCornerShape(cornerRadius.dp) clip = true alpha = expansionFraction 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)) Loading @@ -125,8 +199,31 @@ fun SceneScope.NotificationScrollingStack( NotificationPlaceholder( viewModel = viewModel, form = Form.Stack, modifier = Modifier.fillMaxWidth().height { contentHeight.roundToInt() } 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() }, ) } } } Loading
packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +35 −5 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ import androidx.compose.foundation.layout.height 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 Loading @@ -46,6 +47,11 @@ import androidx.compose.runtime.getValue 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 Loading @@ -56,7 +62,7 @@ import com.android.systemui.battery.BatteryMeterViewController 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 Loading Loading @@ -116,6 +122,8 @@ private fun SceneScope.QuickSettingsScene( 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() Loading Loading @@ -234,10 +242,32 @@ private fun SceneScope.QuickSettingsScene( } } } 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, ) } ) } }
packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt +1 −10 Original line number Diff line number Diff line Loading @@ -18,13 +18,10 @@ package com.android.systemui.scene.ui.composable 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 Loading Loading @@ -66,12 +63,6 @@ constructor( 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(), ) } } }
packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +88 −44 Original line number Diff line number Diff line Loading @@ -32,6 +32,7 @@ import androidx.compose.runtime.mutableStateOf 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 Loading Loading @@ -62,6 +63,7 @@ import com.android.systemui.statusbar.phone.StatusBarLocation 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 Loading Loading @@ -154,24 +156,34 @@ private fun SceneScope.ShadeScene( 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 { Layout( contents = listOf( { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth().clickable(onClick = { viewModel.onContentClicked() }) modifier = Modifier.fillMaxWidth() .clickable(onClick = { viewModel.onContentClicked() }) ) { CollapsedShadeHeader( viewModel = viewModel.shadeHeaderViewModel, createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, modifier = Modifier.padding(horizontal = Shade.Dimensions.HorizontalPadding) modifier = Modifier.padding( horizontal = Shade.Dimensions.HorizontalPadding ) ) QuickSettings( modifier = Modifier.height(130.dp), Loading @@ -179,14 +191,17 @@ private fun SceneScope.ShadeScene( ) if (viewModel.isMediaVisible()) { val mediaHeight = dimensionResource(R.dimen.qs_media_session_height_expanded) val mediaHeight = dimensionResource(R.dimen.qs_media_session_height_expanded) MediaCarousel( modifier = Modifier.height(mediaHeight).fillMaxWidth().layout { measurable, constraints -> Modifier.height(mediaHeight).fillMaxWidth().layout { measurable, constraints -> val placeable = measurable.measure(constraints) // Notify controller to size the carousel for the current space // Notify controller to size the carousel for the // current space mediaHost.measurementInput = MeasurementInput(placeable.width, placeable.height) mediaCarouselController.setSceneContainerSize( Loading @@ -200,16 +215,45 @@ private fun SceneScope.ShadeScene( }, mediaHost = mediaHost, layoutWidth = layoutWidth.value, layoutHeight = with(localDensity) { mediaHeight.toPx() }.toInt(), layoutHeight = with(density) { mediaHeight.toPx() }.toInt(), carouselController = mediaCarouselController, ) } Spacer(modifier = Modifier.height(16.dp)) } }, { NotificationScrollingStack( viewModel = viewModel.notifications, modifier = Modifier.fillMaxWidth().weight(1f), 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()) } } } }