summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt166
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/SingleShadeMeasurePolicy.kt155
2 files changed, 225 insertions, 96 deletions
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 f8513a8c4dd4..c2c67a0e5346 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
@@ -30,7 +30,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
-import androidx.compose.foundation.layout.displayCutoutPadding
+import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -47,8 +47,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.CompositingStrategy
@@ -99,7 +100,6 @@ import com.android.systemui.notifications.ui.composable.NotificationScrollingSta
import com.android.systemui.notifications.ui.composable.NotificationStackCutoffGuideline
import com.android.systemui.qs.footer.ui.compose.FooterActionsWithAnimatedVisibility
import com.android.systemui.qs.ui.composable.BrightnessMirror
-import com.android.systemui.qs.ui.composable.QSMediaMeasurePolicy
import com.android.systemui.qs.ui.composable.QuickSettings
import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaLandscapeTopOffset
import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaOffset.InQQS
@@ -266,13 +266,14 @@ private fun SceneScope.SingleShade(
shadeSession: SaveableSession,
) {
val cutoutLocation = LocalDisplayCutout.current.location
+ val cutoutInsets = WindowInsets.Companion.displayCutout
val isLandscape = LocalWindowSizeClass.current.heightSizeClass == WindowHeightSizeClass.Compact
val usingCollapsedLandscapeMedia =
Utils.useCollapsedMediaInLandscape(LocalContext.current.resources)
val isExpanded = !usingCollapsedLandscapeMedia || !isLandscape
mediaHost.expansion = if (isExpanded) EXPANDED else COLLAPSED
- val maxNotifScrimTop = remember { mutableStateOf(0f) }
+ var maxNotifScrimTop by remember { mutableIntStateOf(0) }
val tileSquishiness by
animateSceneFloatAsState(
value = 1f,
@@ -298,6 +299,24 @@ private fun SceneScope.SingleShade(
viewModel.qsSceneAdapter,
)
}
+ val shadeMeasurePolicy =
+ remember(mediaInRow) {
+ SingleShadeMeasurePolicy(
+ isMediaInRow = mediaInRow,
+ mediaOffset = { mediaOffset.roundToPx() },
+ onNotificationsTopChanged = { maxNotifScrimTop = it },
+ mediaZIndex = {
+ if (MediaContentPicker.shouldElevateMedia(layoutState)) 1f else 0f
+ },
+ cutoutInsetsProvider = {
+ if (cutoutLocation == CutoutLocation.CENTER) {
+ null
+ } else {
+ cutoutInsets
+ }
+ }
+ )
+ }
Box(
modifier =
@@ -315,101 +334,54 @@ private fun SceneScope.SingleShade(
.background(colorResource(R.color.shade_scrim_background_dark)),
)
Layout(
- contents =
- listOf(
- {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- modifier =
- Modifier.fillMaxWidth()
- .thenIf(isEmptySpaceClickable) {
- Modifier.clickable(
- onClick = { viewModel.onEmptySpaceClicked() }
- )
- }
- .thenIf(cutoutLocation != CutoutLocation.CENTER) {
- Modifier.displayCutoutPadding()
- },
- ) {
- CollapsedShadeHeader(
- viewModelFactory = viewModel.shadeHeaderViewModelFactory,
- createTintedIconManager = createTintedIconManager,
- createBatteryMeterViewController = createBatteryMeterViewController,
- statusBarIconController = statusBarIconController,
- )
-
- val content: @Composable () -> Unit = {
- Box(
- Modifier.element(QuickSettings.Elements.QuickQuickSettings)
- .layoutId(QSMediaMeasurePolicy.LayoutId.QS)
- ) {
- QuickSettings(
- viewModel.qsSceneAdapter,
- { viewModel.qsSceneAdapter.qqsHeight },
- isSplitShade = false,
- squishiness = { tileSquishiness },
- )
- }
-
- ShadeMediaCarousel(
- isVisible = isMediaVisible,
- mediaHost = mediaHost,
- mediaOffsetProvider = mediaOffsetProvider,
- modifier =
- Modifier.layoutId(QSMediaMeasurePolicy.LayoutId.Media),
- carouselController = mediaCarouselController,
- )
- }
- val landscapeQsMediaMeasurePolicy = remember {
- QSMediaMeasurePolicy(
- { viewModel.qsSceneAdapter.qqsHeight },
- { mediaOffset.roundToPx() },
- )
- }
- if (mediaInRow) {
- Layout(
- content = content,
- measurePolicy = landscapeQsMediaMeasurePolicy,
- )
- } else {
- content()
- }
- }
- },
- {
- NotificationScrollingStack(
- shadeSession = shadeSession,
- stackScrollView = notificationStackScrollView,
- viewModel = notificationsPlaceholderViewModel,
- maxScrimTop = { maxNotifScrimTop.value },
- shadeMode = ShadeMode.Single,
- shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim,
- onEmptySpaceClick =
- viewModel::onEmptySpaceClicked.takeIf { isEmptySpaceClickable },
- )
- },
+ modifier =
+ Modifier.thenIf(isEmptySpaceClickable) {
+ Modifier.clickable { viewModel.onEmptySpaceClicked() }
+ },
+ content = {
+ CollapsedShadeHeader(
+ viewModelFactory = viewModel.shadeHeaderViewModelFactory,
+ createTintedIconManager = createTintedIconManager,
+ createBatteryMeterViewController = createBatteryMeterViewController,
+ statusBarIconController = statusBarIconController,
+ modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader),
)
- ) { measurables, constraints ->
- check(measurables.size == 2)
- check(measurables[0].size == 1)
- check(measurables[1].size == 1)
- val quickSettingsPlaceable = measurables[0][0].measure(constraints)
- val notificationsPlaceable = measurables[1][0].measure(constraints)
+ Box(
+ Modifier.element(QuickSettings.Elements.QuickQuickSettings)
+ .layoutId(SingleShadeMeasurePolicy.LayoutId.QuickSettings)
+ ) {
+ QuickSettings(
+ viewModel.qsSceneAdapter,
+ { viewModel.qsSceneAdapter.qqsHeight },
+ isSplitShade = false,
+ squishiness = { tileSquishiness },
+ )
+ }
- maxNotifScrimTop.value = quickSettingsPlaceable.height.toFloat()
+ ShadeMediaCarousel(
+ isVisible = isMediaVisible,
+ isInRow = mediaInRow,
+ mediaHost = mediaHost,
+ mediaOffsetProvider = mediaOffsetProvider,
+ carouselController = mediaCarouselController,
+ modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.Media),
+ )
- layout(constraints.maxWidth, constraints.maxHeight) {
- val qsZIndex =
- if (MediaContentPicker.shouldElevateMedia(layoutState)) {
- 1f
- } else {
- 0f
- }
- quickSettingsPlaceable.placeRelative(x = 0, y = 0, zIndex = qsZIndex)
- notificationsPlaceable.placeRelative(x = 0, y = maxNotifScrimTop.value.roundToInt())
- }
- }
+ NotificationScrollingStack(
+ shadeSession = shadeSession,
+ stackScrollView = notificationStackScrollView,
+ viewModel = notificationsPlaceholderViewModel,
+ maxScrimTop = { maxNotifScrimTop.toFloat() },
+ shadeMode = ShadeMode.Single,
+ shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim,
+ onEmptySpaceClick =
+ viewModel::onEmptySpaceClicked.takeIf { isEmptySpaceClickable },
+ modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.Notifications),
+ )
+ },
+ measurePolicy = shadeMeasurePolicy,
+ )
Box(
modifier =
Modifier.align(Alignment.BottomCenter)
@@ -596,6 +568,7 @@ private fun SceneScope.SplitShade(
ShadeMediaCarousel(
isVisible = isMediaVisible,
+ isInRow = false,
mediaHost = mediaHost,
mediaOffsetProvider = mediaOffsetProvider,
modifier =
@@ -653,6 +626,7 @@ private fun SceneScope.SplitShade(
@Composable
private fun SceneScope.ShadeMediaCarousel(
isVisible: Boolean,
+ isInRow: Boolean,
mediaHost: MediaHost,
carouselController: MediaCarouselController,
mediaOffsetProvider: ShadeMediaOffsetProvider,
@@ -664,7 +638,7 @@ private fun SceneScope.ShadeMediaCarousel(
mediaHost = mediaHost,
carouselController = carouselController,
offsetProvider =
- if (MediaContentPicker.shouldElevateMedia(layoutState)) {
+ if (isInRow || MediaContentPicker.shouldElevateMedia(layoutState)) {
null
} else {
{ mediaOffsetProvider.offset }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/SingleShadeMeasurePolicy.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/SingleShadeMeasurePolicy.kt
new file mode 100644
index 000000000000..6275ac396628
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/SingleShadeMeasurePolicy.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.shade.ui.composable
+
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.offset
+import androidx.compose.ui.util.fastFirst
+import androidx.compose.ui.util.fastFirstOrNull
+import com.android.systemui.shade.ui.composable.SingleShadeMeasurePolicy.LayoutId
+import kotlin.math.max
+
+/**
+ * Lays out elements from the [LayoutId] in the shade. This policy supports the case when the QS and
+ * UMO share the same row and when they should be one below another.
+ */
+class SingleShadeMeasurePolicy(
+ private val isMediaInRow: Boolean,
+ private val mediaOffset: MeasureScope.() -> Int,
+ private val onNotificationsTopChanged: (Int) -> Unit,
+ private val mediaZIndex: () -> Float,
+ private val cutoutInsetsProvider: () -> WindowInsets?,
+) : MeasurePolicy {
+
+ enum class LayoutId {
+ QuickSettings,
+ Media,
+ Notifications,
+ ShadeHeader,
+ }
+
+ override fun MeasureScope.measure(
+ measurables: List<Measurable>,
+ constraints: Constraints,
+ ): MeasureResult {
+ val cutoutInsets: WindowInsets? = cutoutInsetsProvider()
+ val constraintsWithCutout = applyCutout(constraints, cutoutInsets)
+ val insetsLeft = cutoutInsets?.getLeft(this, layoutDirection) ?: 0
+ val insetsTop = cutoutInsets?.getTop(this) ?: 0
+
+ val shadeHeaderPlaceable =
+ measurables
+ .fastFirst { it.layoutId == LayoutId.ShadeHeader }
+ .measure(constraintsWithCutout)
+ val mediaPlaceable =
+ measurables
+ .fastFirstOrNull { it.layoutId == LayoutId.Media }
+ ?.measure(applyMediaConstraints(constraintsWithCutout, isMediaInRow))
+ val quickSettingsPlaceable =
+ measurables
+ .fastFirst { it.layoutId == LayoutId.QuickSettings }
+ .measure(constraintsWithCutout)
+ val notificationsPlaceable =
+ measurables.fastFirst { it.layoutId == LayoutId.Notifications }.measure(constraints)
+
+ val notificationsTop =
+ calculateNotificationsTop(
+ statusBarHeaderPlaceable = shadeHeaderPlaceable,
+ quickSettingsPlaceable = quickSettingsPlaceable,
+ mediaPlaceable = mediaPlaceable,
+ insetsTop = insetsTop,
+ isMediaInRow = isMediaInRow,
+ )
+ onNotificationsTopChanged(notificationsTop)
+
+ return layout(constraints.maxWidth, constraints.maxHeight) {
+ shadeHeaderPlaceable.placeRelative(x = insetsLeft, y = insetsTop)
+ quickSettingsPlaceable.placeRelative(
+ x = insetsLeft,
+ y = insetsTop + shadeHeaderPlaceable.height,
+ )
+
+ if (isMediaInRow) {
+ mediaPlaceable?.placeRelative(
+ x = insetsLeft + constraintsWithCutout.maxWidth / 2,
+ y = mediaOffset() + insetsTop + shadeHeaderPlaceable.height,
+ zIndex = mediaZIndex(),
+ )
+ } else {
+ mediaPlaceable?.placeRelative(
+ x = insetsLeft,
+ y = insetsTop + shadeHeaderPlaceable.height + quickSettingsPlaceable.height,
+ zIndex = mediaZIndex(),
+ )
+ }
+
+ // Notifications don't need to accommodate for horizontal insets
+ notificationsPlaceable.placeRelative(x = 0, y = notificationsTop)
+ }
+ }
+
+ private fun calculateNotificationsTop(
+ statusBarHeaderPlaceable: Placeable,
+ quickSettingsPlaceable: Placeable,
+ mediaPlaceable: Placeable?,
+ insetsTop: Int,
+ isMediaInRow: Boolean,
+ ): Int {
+ val mediaHeight = mediaPlaceable?.height ?: 0
+ return insetsTop +
+ statusBarHeaderPlaceable.height +
+ if (isMediaInRow) {
+ max(quickSettingsPlaceable.height, mediaHeight)
+ } else {
+ quickSettingsPlaceable.height + mediaHeight
+ }
+ }
+
+ private fun applyMediaConstraints(
+ constraints: Constraints,
+ isMediaInRow: Boolean,
+ ): Constraints {
+ return if (isMediaInRow) {
+ constraints.copy(maxWidth = constraints.maxWidth / 2)
+ } else {
+ constraints
+ }
+ }
+
+ private fun MeasureScope.applyCutout(
+ constraints: Constraints,
+ cutoutInsets: WindowInsets?,
+ ): Constraints {
+ return if (cutoutInsets == null) {
+ constraints
+ } else {
+ val left = cutoutInsets.getLeft(this, layoutDirection)
+ val top = cutoutInsets.getTop(this)
+ val right = cutoutInsets.getRight(this, layoutDirection)
+ val bottom = cutoutInsets.getBottom(this)
+
+ constraints.offset(horizontal = -(left + right), vertical = -(top + bottom))
+ }
+ }
+}