Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 80cb86ab authored by Nicolo' Mazzucato's avatar Nicolo' Mazzucato
Browse files

Fix ShadeHeader flickers due to insets and corner radius when the window moves display

This fixes a one-frame flicker when moving the shade window between displays.

There were 2 root causes:
1. Wrong insets for one frame
2. Wrong display corner radius for one frame

1. For the wrong insets, the issue was that we were reading insets values during composition (from LocalDisplayCutout). However, the updated insets (after the display change) were propagated to the root view only after composition.
In this cl insets are passed as a State instead of a Stateflow, and read during the layout phase in ShadeHeader.
A few adjustments have been made to have a single way to get the cutout.

2. Display corner radius were also propagated with a state flow, and were updated after a display change. This caused a one-frame delay between when they were set to the StateFlow and when they were used from the ScreenDecorProvider composable. Now ScreenDecorProvider just gets the corner radius itself, caching the value using the display unique id.

Bug: 417960167
Flag: com.android.systemui.shade_window_goes_around
Flag: com.android.systemui.scene_container
Test: Verify the shade header flicker when moving the window between displays is not there anymore
Change-Id: Ie5c4d5215a3abff6b45c865ee90f95ca312771fd
parent 580acc26
Loading
Loading
Loading
Loading
+55 −13
Original line number Diff line number Diff line
@@ -16,31 +16,43 @@

package com.android.systemui.common.ui.compose.windowinsets

import android.content.Context
import android.graphics.Point
import android.view.WindowInsets
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.flow.StateFlow
import com.android.internal.policy.ScreenDecorationsUtils

/** The bounds and [CutoutLocation] of the current display. */
val LocalDisplayCutout = staticCompositionLocalOf { DisplayCutout() }
/**
 * The bounds and [CutoutLocation] of the current display.
 *
 * This is provided as a [State] and not as a simple [DisplayCutout] as the cutout is calculated
 * from insets and can change after recomposition but before layout. If a plain DisplayCutout was
 * provided and the value was read during recomposition, it would result in a frame using the wrong
 * value after new insets are received.
 */
val LocalDisplayCutout: ProvidableCompositionLocal<() -> DisplayCutout> = staticCompositionLocalOf {
    { DisplayCutout() }
}

/** The corner radius in px of the current display. */
val LocalScreenCornerRadius = staticCompositionLocalOf { 0.dp }

@Composable
fun ScreenDecorProvider(
    displayCutout: StateFlow<DisplayCutout>,
    screenCornerRadius: StateFlow<Float>,
    content: @Composable () -> Unit,
) {
    val cutout by displayCutout.collectAsStateWithLifecycle()
    val screenCornerRadiusPx by screenCornerRadius.collectAsStateWithLifecycle()

fun ScreenDecorProvider(windowInsets: () -> WindowInsets?, content: @Composable () -> Unit) {
    val context = LocalContext.current
    val screenCornerRadiusPx =
        remember(context.display.uniqueId) { ScreenDecorationsUtils.getWindowCornerRadius(context) }
    val screenCornerRadiusDp = with(LocalDensity.current) { screenCornerRadiusPx.toDp() }
    val cutout = remember(windowInsets, context) { { windowInsets().toCutout(context) } }

    CompositionLocalProvider(
        LocalScreenCornerRadius provides screenCornerRadiusDp,
@@ -49,3 +61,33 @@ fun ScreenDecorProvider(
        content()
    }
}

private fun WindowInsets?.toCutout(context: Context): DisplayCutout {
    val boundingRect = this?.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
        }
    val viewDisplayCutout = this?.displayCutout
    return DisplayCutout(left, top, right, bottom, location, viewDisplayCutout)
}

// 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
}

// 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.toDp(context)
}
+2 −1
Original line number Diff line number Diff line
@@ -64,7 +64,8 @@ constructor(
    @Composable
    fun StatusBar(modifier: Modifier = Modifier) {
        val context = LocalContext.current
        val viewDisplayCutout = LocalDisplayCutout.current.viewDisplayCutoutKeyguardStatusBarView
        val viewDisplayCutout =
            LocalDisplayCutout.current().viewDisplayCutoutKeyguardStatusBarView

        @SuppressLint("InflateParams")
        val view =
+1 −2
Original line number Diff line number Diff line
@@ -64,7 +64,6 @@ import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.IntOffset
@@ -195,7 +194,7 @@ private fun ContentScope.QuickSettingsScene(
    shadeSession: SaveableSession,
    jankMonitor: InteractionJankMonitor,
) {
    val cutoutLocation = LocalDisplayCutout.current.location
    val cutoutLocation = LocalDisplayCutout.current().location
    val brightnessMirrorShowing by brightnessMirrorViewModel.isShowing.collectAsStateWithLifecycle()
    val contentAlpha by
        animateFloatAsState(
+10 −7
Original line number Diff line number Diff line
@@ -182,7 +182,7 @@ fun ContentScope.CollapsedShadeHeader(
    isSplitShade: Boolean,
    modifier: Modifier = Modifier,
) {
    val cutoutLocation = LocalDisplayCutout.current.location
    val cutoutLocation = LocalDisplayCutout.current().location
    val horizontalPadding =
        max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding)

@@ -459,16 +459,19 @@ private fun CutoutAwareShadeHeader(
    startContent: @Composable () -> Unit,
    endContent: @Composable () -> Unit,
) {
    val cutoutWidth = LocalDisplayCutout.current.width()
    val cutoutHeight = LocalDisplayCutout.current.height()
    val cutoutTop = LocalDisplayCutout.current.top
    val cutoutLocation = LocalDisplayCutout.current.location
    val cutoutProvider = LocalDisplayCutout.current
    val statusBarHeight = ShadeHeader.Dimensions.StatusBarHeight

    Layout(
        modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root),
        contents = listOf(startContent, endContent),
    ) { measurables, constraints ->
        val cutout = cutoutProvider()

        val cutoutWidth = cutout.width()
        val cutoutHeight = cutout.height()
        val cutoutTop = cutout.top
        val cutoutLocation = cutout.location

        check(constraints.hasBoundedWidth)
        check(measurables.size == 2)
        check(measurables[0].size == 1)
@@ -593,7 +596,7 @@ private fun BatteryIconLegacy(
    val inverseColor =
        Utils.getColorAttrDefaultColor(themedContext, android.R.attr.textColorPrimaryInverse)

    val cutoutLocation = LocalDisplayCutout.current.location
    val cutoutLocation = LocalDisplayCutout.current().location

    AndroidView(
        factory = { context ->
+2 −2
Original line number Diff line number Diff line
@@ -92,8 +92,8 @@ import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL
import com.android.systemui.notifications.ui.composable.NotificationScrollingStack
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.panels.ui.compose.QuickQuickSettings
import com.android.systemui.qs.ui.composable.BrightnessMirror
import com.android.systemui.qs.ui.composable.QuickSettings
import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaLandscapeTopOffset
import com.android.systemui.res.R
@@ -250,7 +250,7 @@ private fun ContentScope.SingleShade(
    shadeSession: SaveableSession,
    usingCollapsedLandscapeMedia: Boolean,
) {
    val cutoutLocation = LocalDisplayCutout.current.location
    val cutoutLocation = LocalDisplayCutout.current().location
    val cutoutInsets = WindowInsets.Companion.displayCutout
    mediaHost.expansion = if (usingCollapsedLandscapeMedia && isLandscape()) COLLAPSED else EXPANDED

Loading