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

Commit c13470d5 authored by Anton Potapov's avatar Anton Potapov Committed by Android (Google) Code Review
Browse files

Merge "Rework layout so the zIndex only affects media carousel and not the QuickSettings" into main

parents 6c1b6f87 3245c33d
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
@@ -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,
@@ -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 =
@@ -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,
@@ -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)
@@ -600,6 +572,7 @@ private fun SceneScope.SplitShade(

                            ShadeMediaCarousel(
                                isVisible = isMediaVisible,
                                isInRow = false,
                                mediaHost = mediaHost,
                                mediaOffsetProvider = mediaOffsetProvider,
                                modifier =
@@ -657,6 +630,7 @@ private fun SceneScope.SplitShade(
@Composable
private fun SceneScope.ShadeMediaCarousel(
    isVisible: Boolean,
    isInRow: Boolean,
    mediaHost: MediaHost,
    carouselController: MediaCarouselController,
    mediaOffsetProvider: ShadeMediaOffsetProvider,
@@ -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 }
+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))
        }
    }
}