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

Commit 3245c33d authored by Anton Potapov's avatar Anton Potapov
Browse files

Rework layout so the zIndex only affects media carousel and not the

QuickSettings

Flag: EXEMPT bugfix
Test: manual on foldable in portrait and landscape
Fixes: 359876437
Change-Id: I6588397ad2fcb68073764c40cc2cbcaea6acf923
parent 98f28c99
Loading
Loading
Loading
Loading
+70 −96
Original line number Diff line number Diff line
@@ -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,33 +334,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,
@@ -353,63 +361,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)
@@ -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 }
+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))
        }
    }
}