Loading packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +70 −96 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -269,13 +269,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, Loading @@ -301,6 +302,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 = Loading @@ -318,33 +337,22 @@ 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() Modifier.thenIf(isEmptySpaceClickable) { Modifier.clickable { viewModel.onEmptySpaceClicked() } }, ) { content = { CollapsedShadeHeader( viewModelFactory = viewModel.shadeHeaderViewModelFactory, createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader), ) val content: @Composable () -> Unit = { Box( Modifier.element(QuickSettings.Elements.QuickQuickSettings) .layoutId(QSMediaMeasurePolicy.LayoutId.QS) .layoutId(SingleShadeMeasurePolicy.LayoutId.QuickSettings) ) { QuickSettings( viewModel.qsSceneAdapter, Loading @@ -356,63 +364,27 @@ private fun SceneScope.SingleShade( ShadeMediaCarousel( isVisible = isMediaVisible, isInRow = mediaInRow, mediaHost = mediaHost, mediaOffsetProvider = mediaOffsetProvider, modifier = Modifier.layoutId(QSMediaMeasurePolicy.LayoutId.Media), carouselController = mediaCarouselController, modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.Media), ) } 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 }, maxScrimTop = { maxNotifScrimTop.toFloat() }, shadeMode = ShadeMode.Single, shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim, onEmptySpaceClick = viewModel::onEmptySpaceClicked.takeIf { isEmptySpaceClickable }, modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.Notifications), ) }, measurePolicy = shadeMeasurePolicy, ) ) { 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) maxNotifScrimTop.value = quickSettingsPlaceable.height.toFloat() 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()) } } Box( modifier = Modifier.align(Alignment.BottomCenter) Loading Loading @@ -600,6 +572,7 @@ private fun SceneScope.SplitShade( ShadeMediaCarousel( isVisible = isMediaVisible, isInRow = false, mediaHost = mediaHost, mediaOffsetProvider = mediaOffsetProvider, modifier = Loading Loading @@ -657,6 +630,7 @@ private fun SceneScope.SplitShade( @Composable private fun SceneScope.ShadeMediaCarousel( isVisible: Boolean, isInRow: Boolean, mediaHost: MediaHost, carouselController: MediaCarouselController, mediaOffsetProvider: ShadeMediaOffsetProvider, Loading @@ -668,7 +642,7 @@ private fun SceneScope.ShadeMediaCarousel( mediaHost = mediaHost, carouselController = carouselController, offsetProvider = if (MediaContentPicker.shouldElevateMedia(layoutState)) { if (isInRow || MediaContentPicker.shouldElevateMedia(layoutState)) { null } else { { mediaOffsetProvider.offset } Loading packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/SingleShadeMeasurePolicy.kt 0 → 100644 +155 −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.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)) } } } Loading
packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +70 −96 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -269,13 +269,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, Loading @@ -301,6 +302,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 = Loading @@ -318,33 +337,22 @@ 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() Modifier.thenIf(isEmptySpaceClickable) { Modifier.clickable { viewModel.onEmptySpaceClicked() } }, ) { content = { CollapsedShadeHeader( viewModelFactory = viewModel.shadeHeaderViewModelFactory, createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader), ) val content: @Composable () -> Unit = { Box( Modifier.element(QuickSettings.Elements.QuickQuickSettings) .layoutId(QSMediaMeasurePolicy.LayoutId.QS) .layoutId(SingleShadeMeasurePolicy.LayoutId.QuickSettings) ) { QuickSettings( viewModel.qsSceneAdapter, Loading @@ -356,63 +364,27 @@ private fun SceneScope.SingleShade( ShadeMediaCarousel( isVisible = isMediaVisible, isInRow = mediaInRow, mediaHost = mediaHost, mediaOffsetProvider = mediaOffsetProvider, modifier = Modifier.layoutId(QSMediaMeasurePolicy.LayoutId.Media), carouselController = mediaCarouselController, modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.Media), ) } 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 }, maxScrimTop = { maxNotifScrimTop.toFloat() }, shadeMode = ShadeMode.Single, shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim, onEmptySpaceClick = viewModel::onEmptySpaceClicked.takeIf { isEmptySpaceClickable }, modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.Notifications), ) }, measurePolicy = shadeMeasurePolicy, ) ) { 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) maxNotifScrimTop.value = quickSettingsPlaceable.height.toFloat() 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()) } } Box( modifier = Modifier.align(Alignment.BottomCenter) Loading Loading @@ -600,6 +572,7 @@ private fun SceneScope.SplitShade( ShadeMediaCarousel( isVisible = isMediaVisible, isInRow = false, mediaHost = mediaHost, mediaOffsetProvider = mediaOffsetProvider, modifier = Loading Loading @@ -657,6 +630,7 @@ private fun SceneScope.SplitShade( @Composable private fun SceneScope.ShadeMediaCarousel( isVisible: Boolean, isInRow: Boolean, mediaHost: MediaHost, carouselController: MediaCarouselController, mediaOffsetProvider: ShadeMediaOffsetProvider, Loading @@ -668,7 +642,7 @@ private fun SceneScope.ShadeMediaCarousel( mediaHost = mediaHost, carouselController = carouselController, offsetProvider = if (MediaContentPicker.shouldElevateMedia(layoutState)) { if (isInRow || MediaContentPicker.shouldElevateMedia(layoutState)) { null } else { { mediaOffsetProvider.offset } Loading
packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/SingleShadeMeasurePolicy.kt 0 → 100644 +155 −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.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)) } } }