diff options
| author | 2023-09-18 15:37:02 -0700 | |
|---|---|---|
| committer | 2023-10-02 12:46:20 -0700 | |
| commit | a16ae9a2fa688199d1fca10a822db4d52b29d3c0 (patch) | |
| tree | 7647101694f27d87d0a0b42c6c386abf413e391b | |
| parent | c79c20337222d701c39f3ff6e39e1a7f7924b71e (diff) | |
Added display cutout handling to Shade Header in Flexiglass
The collapsed Shade Header is now split into two halves that are repositioned around the display cutout, like the old impl. Compose does not yet expose the actual bounding rectangle for the display cutout, so this needs to be derived from android.view.DisplayCutout and piped in from the root view for now. The display cutout bounds are provided via CompositionLocal to the entire Compose tree.
Bug: 298525212
Test: Manually verified header resizes and truncates content properly with different font/display sizes and cutout locations.
Change-Id: I8fa7fef39dcc920ce01c4ebb29e45aae310cdd87
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, ) ) |