diff options
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)) + } + } +} |