diff options
9 files changed, 280 insertions, 41 deletions
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt index 609ea90e9159..5b4a8fb6ab7a 100644 --- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -19,6 +19,7 @@ package com.android.systemui.compose import android.content.Context import android.view.View +import android.view.WindowInsets import androidx.activity.ComponentActivity import androidx.lifecycle.LifecycleOwner import com.android.systemui.people.ui.viewmodel.PeopleViewModel @@ -26,6 +27,8 @@ import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow /** The Compose facade, when Compose is *not* available. */ object ComposeFacade : BaseComposeFacade { @@ -52,8 +55,10 @@ object ComposeFacade : BaseComposeFacade { } override fun createSceneContainerView( + scope: CoroutineScope, context: Context, viewModel: SceneContainerViewModel, + windowInsets: StateFlow<WindowInsets?>, sceneByKey: Map<SceneKey, Scene>, ): View { throwComposeUnavailableError() diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt index 0ee88b90bcc4..ac599897553a 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -17,12 +17,19 @@ package com.android.systemui.compose import android.content.Context +import android.graphics.Point import android.view.View +import android.view.WindowInsets import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import androidx.lifecycle.LifecycleOwner import com.android.compose.theme.PlatformTheme +import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation +import com.android.systemui.common.ui.compose.windowinsets.DisplayCutout +import com.android.systemui.common.ui.compose.windowinsets.DisplayCutoutProvider import com.android.systemui.people.ui.compose.PeopleScreen import com.android.systemui.people.ui.viewmodel.PeopleViewModel import com.android.systemui.qs.footer.ui.compose.FooterActions @@ -32,6 +39,11 @@ import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.composable.ComposableScene import com.android.systemui.scene.ui.composable.SceneContainer import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn /** The Compose facade, when Compose is available. */ object ComposeFacade : BaseComposeFacade { @@ -58,20 +70,69 @@ object ComposeFacade : BaseComposeFacade { } override fun createSceneContainerView( + scope: CoroutineScope, context: Context, viewModel: SceneContainerViewModel, + windowInsets: StateFlow<WindowInsets?>, sceneByKey: Map<SceneKey, Scene>, ): View { return ComposeView(context).apply { setContent { PlatformTheme { - SceneContainer( - viewModel = viewModel, - sceneByKey = - sceneByKey.mapValues { (_, scene) -> scene as ComposableScene }, - ) + DisplayCutoutProvider( + displayCutout = displayCutoutFromWindowInsets(scope, context, windowInsets) + ) { + SceneContainer( + viewModel = viewModel, + sceneByKey = + sceneByKey.mapValues { (_, scene) -> scene as ComposableScene }, + ) + } } } } } + + // TODO(b/298525212): remove once Compose exposes window inset bounds. + private fun displayCutoutFromWindowInsets( + scope: CoroutineScope, + context: Context, + windowInsets: StateFlow<WindowInsets?>, + ): StateFlow<DisplayCutout> = + windowInsets + .map { + val boundingRect = it?.displayCutout?.boundingRectTop + val width = boundingRect?.let { boundingRect.right - boundingRect.left } ?: 0 + val left = boundingRect?.left?.toDp(context) ?: 0.dp + val top = boundingRect?.top?.toDp(context) ?: 0.dp + val right = boundingRect?.right?.toDp(context) ?: 0.dp + val bottom = boundingRect?.bottom?.toDp(context) ?: 0.dp + val location = + when { + width <= 0f -> CutoutLocation.NONE + left <= 0.dp -> CutoutLocation.LEFT + right >= getDisplayWidth(context) -> CutoutLocation.RIGHT + else -> CutoutLocation.CENTER + } + DisplayCutout( + left, + top, + right, + bottom, + location, + ) + } + .stateIn(scope, SharingStarted.WhileSubscribed(), DisplayCutout()) + + // TODO(b/298525212): remove once Compose exposes window inset bounds. + private fun getDisplayWidth(context: Context): Dp { + val point = Point() + checkNotNull(context.display).getRealSize(point) + return point.x.dp + } + + // TODO(b/298525212): remove once Compose exposes window inset bounds. + private fun Int.toDp(context: Context): Dp { + return (this.toFloat() / context.resources.displayMetrics.density).dp + } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutout.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutout.kt new file mode 100644 index 000000000000..8dda067d8c8a --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutout.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2023 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.common.ui.compose.windowinsets + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlin.math.abs + +/** Represents the global position of the bounds for the display cutout for this display */ +data class DisplayCutout( + val left: Dp = 0.dp, + val top: Dp = 0.dp, + val right: Dp = 0.dp, + val bottom: Dp = 0.dp, + val location: CutoutLocation = CutoutLocation.NONE, +) { + fun width() = abs(right.value - left.value).dp +} + +enum class CutoutLocation { + NONE, + CENTER, + LEFT, + RIGHT, +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutoutProvider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutoutProvider.kt new file mode 100644 index 000000000000..ed393c0a9cb5 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutoutProvider.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 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.common.ui.compose.windowinsets + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.staticCompositionLocalOf +import kotlinx.coroutines.flow.StateFlow + +/** The bounds and [CutoutLocation] of the current display. */ +val LocalDisplayCutout = staticCompositionLocalOf { DisplayCutout() } + +@Composable +fun DisplayCutoutProvider( + displayCutout: StateFlow<DisplayCutout>, + content: @Composable () -> Unit, +) { + val cutout by displayCutout.collectAsState() + + CompositionLocalProvider(LocalDisplayCutout provides cutout) { content() } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt index 519c0a9c4c7c..6629a2598587 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt @@ -39,8 +39,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.Layout import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView @@ -49,9 +51,11 @@ import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.ValueKey import com.android.compose.animation.scene.animateSharedFloatAsState import com.android.settingslib.Utils -import com.android.systemui.res.R import com.android.systemui.battery.BatteryMeterView import com.android.systemui.battery.BatteryMeterViewController +import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation +import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout +import com.android.systemui.res.R import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.statusbar.phone.StatusBarIconController import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager @@ -97,38 +101,100 @@ fun SceneScope.CollapsedShadeHeader( val useExpandedFormat by remember(formatProgress) { derivedStateOf { formatProgress.value > 0.5f } } - Row( - modifier = - modifier - .element(ShadeHeader.Elements.CollapsedContent) - .fillMaxWidth() - .defaultMinSize(minHeight = ShadeHeader.Dimensions.CollapsedHeight), - ) { - AndroidView( - factory = { context -> - Clock(ContextThemeWrapper(context, R.style.TextAppearance_QS_Status), null) - }, - modifier = Modifier.align(Alignment.CenterVertically), - ) - Spacer(modifier = Modifier.width(5.dp)) - VariableDayDate( - viewModel = viewModel, - modifier = Modifier.widthIn(max = 90.dp).align(Alignment.CenterVertically), - ) - Spacer(modifier = Modifier.weight(1f)) - SystemIconContainer { - StatusIcons( - viewModel = viewModel, - createTintedIconManager = createTintedIconManager, - statusBarIconController = statusBarIconController, - useExpandedFormat = useExpandedFormat, - modifier = Modifier.align(Alignment.CenterVertically).padding(end = 6.dp), - ) - BatteryIcon( - createBatteryMeterViewController = createBatteryMeterViewController, - useExpandedFormat = useExpandedFormat, - modifier = Modifier.align(Alignment.CenterVertically), - ) + val cutoutWidth = LocalDisplayCutout.current.width() + val cutoutLocation = LocalDisplayCutout.current.location + + // This layout assumes it is globally positioned at (0, 0) and is the + // same size as the screen. + Layout( + modifier = modifier.element(ShadeHeader.Elements.CollapsedContent), + contents = + listOf( + { + Row { + AndroidView( + factory = { context -> + Clock( + ContextThemeWrapper(context, R.style.TextAppearance_QS_Status), + null + ) + }, + modifier = Modifier.align(Alignment.CenterVertically), + ) + Spacer(modifier = Modifier.width(5.dp)) + VariableDayDate( + viewModel = viewModel, + modifier = Modifier.align(Alignment.CenterVertically), + ) + } + }, + { + Row(horizontalArrangement = Arrangement.End) { + SystemIconContainer { + StatusIcons( + viewModel = viewModel, + createTintedIconManager = createTintedIconManager, + statusBarIconController = statusBarIconController, + useExpandedFormat = useExpandedFormat, + modifier = + Modifier.align(Alignment.CenterVertically) + .padding(end = 6.dp) + .weight(1f, fill = false) + ) + BatteryIcon( + createBatteryMeterViewController = createBatteryMeterViewController, + useExpandedFormat = useExpandedFormat, + modifier = Modifier.align(Alignment.CenterVertically), + ) + } + } + }, + ), + ) { measurables, constraints -> + check(constraints.hasBoundedWidth) + check(measurables.size == 2) + check(measurables[0].size == 1) + check(measurables[1].size == 1) + + val screenWidth = constraints.maxWidth + val cutoutWidthPx = cutoutWidth.roundToPx() + val height = ShadeHeader.Dimensions.CollapsedHeight.roundToPx() + val childConstraints = Constraints.fixed((screenWidth - cutoutWidthPx) / 2, height) + + val startMeasurable = measurables[0][0] + val endMeasurable = measurables[1][0] + + val startPlaceable = startMeasurable.measure(childConstraints) + val endPlaceable = endMeasurable.measure(childConstraints) + + layout(screenWidth, height) { + when (cutoutLocation) { + CutoutLocation.NONE, + CutoutLocation.RIGHT -> { + startPlaceable.placeRelative(x = 0, y = 0) + endPlaceable.placeRelative( + x = startPlaceable.width, + y = 0, + ) + } + CutoutLocation.CENTER -> { + startPlaceable.placeRelative(x = 0, y = 0) + endPlaceable.placeRelative( + x = startPlaceable.width + cutoutWidthPx, + y = 0, + ) + } + CutoutLocation.LEFT -> { + startPlaceable.placeRelative( + x = cutoutWidthPx, + y = 0, + ) + endPlaceable.placeRelative( + x = startPlaceable.width + cutoutWidthPx, + y = 0, + ) + } + } } } } @@ -201,7 +267,10 @@ fun SceneScope.ExpandedShadeHeader( createTintedIconManager = createTintedIconManager, statusBarIconController = statusBarIconController, useExpandedFormat = useExpandedFormat, - modifier = Modifier.align(Alignment.CenterVertically).padding(end = 6.dp), + modifier = + Modifier.align(Alignment.CenterVertically) + .padding(end = 6.dp) + .weight(1f, fill = false), ) BatteryIcon( useExpandedFormat = useExpandedFormat, 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 8832a119dbfd..13ebdf9c4a7c 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 @@ -63,6 +63,7 @@ object Shade { object Dimensions { val ScrimCornerSize = 32.dp + val HorizontalPadding = 16.dp } object Shapes { @@ -138,7 +139,11 @@ private fun SceneScope.ShadeScene( modifier = Modifier.fillMaxSize() .clickable(onClick = { viewModel.onContentClicked() }) - .padding(start = 16.dp, end = 16.dp, bottom = 48.dp) + .padding( + start = Shade.Dimensions.HorizontalPadding, + end = Shade.Dimensions.HorizontalPadding, + bottom = 48.dp + ) ) { CollapsedShadeHeader( viewModel = viewModel.shadeHeaderViewModel, diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt index 85f31e5e6b5a..1a6f7e13cf68 100644 --- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt +++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt @@ -19,6 +19,7 @@ package com.android.systemui.compose import android.content.Context import android.view.View +import android.view.WindowInsets import androidx.activity.ComponentActivity import androidx.lifecycle.LifecycleOwner import com.android.systemui.people.ui.viewmodel.PeopleViewModel @@ -26,6 +27,8 @@ import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow /** * A facade to interact with Compose, when it is available. @@ -63,8 +66,10 @@ interface BaseComposeFacade { /** Create a [View] to represent [viewModel] on screen. */ fun createSceneContainerView( + scope: CoroutineScope, context: Context, viewModel: SceneContainerViewModel, + windowInsets: StateFlow<WindowInsets?>, sceneByKey: Map<SceneKey, Scene>, ): View } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt index cdf50bab6b42..7fc409445f50 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt @@ -3,9 +3,11 @@ package com.android.systemui.scene.ui.view import android.content.Context import android.util.AttributeSet import android.view.View +import android.view.WindowInsets import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel +import kotlinx.coroutines.flow.MutableStateFlow /** A root view of the main SysUI window that supports scenes. */ class SceneWindowRootView( @@ -19,6 +21,9 @@ class SceneWindowRootView( private lateinit var viewModel: SceneContainerViewModel + // TODO(b/298525212): remove once Compose exposes window inset bounds. + private val windowInsets: MutableStateFlow<WindowInsets?> = MutableStateFlow(null) + fun init( viewModel: SceneContainerViewModel, containerConfig: SceneContainerConfig, @@ -30,6 +35,7 @@ class SceneWindowRootView( SceneWindowRootViewBinder.bind( view = this@SceneWindowRootView, viewModel = viewModel, + windowInsets = windowInsets, containerConfig = containerConfig, scenes = scenes, onVisibilityChangedInternal = { isVisible -> @@ -42,4 +48,11 @@ class SceneWindowRootView( // Do nothing. We don't want external callers to invoke this. Instead, we drive our own // visibility from our view-binder. } + + // TODO(b/298525212): remove once Compose exposes window inset bounds. + override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets? { + val insets = super.onApplyWindowInsets(windowInsets) + this.windowInsets.value = insets + return insets + } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt index 3fb886d46890..17d6c9d319e8 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt @@ -19,6 +19,7 @@ package com.android.systemui.scene.ui.view import android.view.Gravity import android.view.View import android.view.ViewGroup +import android.view.WindowInsets import android.widget.FrameLayout import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcherOwner @@ -27,14 +28,15 @@ import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.res.R import com.android.systemui.compose.ComposeFacade import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import java.time.Instant +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch object SceneWindowRootViewBinder { @@ -43,6 +45,7 @@ object SceneWindowRootViewBinder { fun bind( view: ViewGroup, viewModel: SceneContainerViewModel, + windowInsets: StateFlow<WindowInsets?>, containerConfig: SceneContainerConfig, scenes: Set<Scene>, onVisibilityChangedInternal: (isVisible: Boolean) -> Unit, @@ -77,8 +80,10 @@ object SceneWindowRootViewBinder { view.addView( ComposeFacade.createSceneContainerView( + scope = this, context = view.context, viewModel = viewModel, + windowInsets = windowInsets, sceneByKey = sortedSceneByKey, ) ) |