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

Commit b61b1388 authored by Shawn Lee's avatar Shawn Lee Committed by Android (Google) Code Review
Browse files

Merge "[flexiglass] Rewrite Shade Header in Compose and migrate it to flexiglass" into main

parents 04cd01c8 7f4a2552
Loading
Loading
Loading
Loading
+34 −9
Original line number Diff line number Diff line
@@ -16,15 +16,19 @@

package com.android.systemui.qs.ui.composable

import android.view.ViewGroup
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.SceneScope
import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.qs.footer.ui.compose.QuickSettings
import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
@@ -33,6 +37,10 @@ import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.scene.shared.model.UserAction
import com.android.systemui.scene.ui.composable.ComposableScene
import com.android.systemui.shade.ui.composable.ExpandedShadeHeader
import com.android.systemui.statusbar.phone.StatusBarIconController
import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager
import com.android.systemui.statusbar.phone.StatusBarLocation
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -44,6 +52,9 @@ class QuickSettingsScene
@Inject
constructor(
    private val viewModel: QuickSettingsSceneViewModel,
    private val tintedIconManagerFactory: TintedIconManager.Factory,
    private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
    private val statusBarIconController: StatusBarIconController,
) : ComposableScene {
    override val key = SceneKey.QuickSettings

@@ -61,6 +72,9 @@ constructor(
    ) {
        QuickSettingsScene(
            viewModel = viewModel,
            createTintedIconManager = tintedIconManagerFactory::create,
            createBatteryMeterViewController = batteryMeterViewControllerFactory::create,
            statusBarIconController = statusBarIconController,
            modifier = modifier,
        )
    }
@@ -69,16 +83,27 @@ constructor(
@Composable
private fun SceneScope.QuickSettingsScene(
    viewModel: QuickSettingsSceneViewModel,
    createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
    createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
    statusBarIconController: StatusBarIconController,
    modifier: Modifier = Modifier,
) {
    // TODO(b/280887232): implement the real UI.

    Box(
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier =
            modifier
                .fillMaxSize()
                .clickable(onClick = { viewModel.onContentClicked() })
            .padding(horizontal = 16.dp, vertical = 48.dp)
                .padding(start = 16.dp, end = 16.dp, bottom = 48.dp)
    ) {
        QuickSettings(modifier = Modifier.fillMaxHeight())
        ExpandedShadeHeader(
            viewModel = viewModel.shadeHeaderViewModel,
            createTintedIconManager = createTintedIconManager,
            createBatteryMeterViewController = createBatteryMeterViewController,
            statusBarIconController = statusBarIconController,
        )
        Spacer(modifier = Modifier.height(16.dp))
        QuickSettings()
    }
}
+8 −0
Original line number Diff line number Diff line
@@ -5,10 +5,18 @@ import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.TransitionBuilder
import com.android.systemui.notifications.ui.composable.Notifications
import com.android.systemui.qs.footer.ui.compose.QuickSettings
import com.android.systemui.shade.ui.composable.ShadeHeader

fun TransitionBuilder.shadeToQuickSettingsTransition() {
    spec = tween(durationMillis = 500)

    translate(Notifications.Elements.Notifications, Edge.Bottom)
    timestampRange(endMillis = 83) { fade(QuickSettings.Elements.FooterActions) }

    translate(ShadeHeader.Elements.CollapsedContent, y = ShadeHeader.Dimensions.CollapsedHeight)
    translate(ShadeHeader.Elements.ExpandedContent, y = (-ShadeHeader.Dimensions.ExpandedHeight))

    fractionRange(end = .14f) { fade(ShadeHeader.Elements.CollapsedContent) }

    fractionRange(start = .58f) { fade(ShadeHeader.Elements.ExpandedContent) }
}
+314 −0
Original line number Diff line number Diff line
/*
 * 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.shade.ui.composable

import android.view.ContextThemeWrapper
import android.view.ViewGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
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.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.android.compose.animation.scene.ElementKey
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.R
import com.android.systemui.battery.BatteryMeterView
import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
import com.android.systemui.statusbar.phone.StatusBarIconController
import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager
import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.statusbar.phone.StatusIconContainer
import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel
import com.android.systemui.statusbar.policy.Clock

object ShadeHeader {
    object Elements {
        val FormatPlaceholder = ElementKey("ShadeHeaderFormatPlaceholder")
        val ExpandedContent = ElementKey("ShadeHeaderExpandedContent")
        val CollapsedContent = ElementKey("ShadeHeaderCollapsedContent")
    }

    object Keys {
        val transitionProgress = ValueKey("ShadeHeaderTransitionProgress")
    }

    object Dimensions {
        val CollapsedHeight = 48.dp
        val ExpandedHeight = 120.dp
    }
}

@Composable
fun SceneScope.CollapsedShadeHeader(
    viewModel: ShadeHeaderViewModel,
    createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
    createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
    statusBarIconController: StatusBarIconController,
    modifier: Modifier = Modifier,
) {
    // TODO(b/298153892): Remove this once animateSharedFloatAsState.element can be null.
    Spacer(Modifier.element(ShadeHeader.Elements.FormatPlaceholder))
    val formatProgress =
        animateSharedFloatAsState(
            0.0f,
            ShadeHeader.Keys.transitionProgress,
            ShadeHeader.Elements.FormatPlaceholder
        )
    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),
            )
        }
    }
}

@Composable
fun SceneScope.ExpandedShadeHeader(
    viewModel: ShadeHeaderViewModel,
    createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
    createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
    statusBarIconController: StatusBarIconController,
    modifier: Modifier = Modifier,
) {
    // TODO(b/298153892): Remove this once animateSharedFloatAsState.element can be null.
    Spacer(Modifier.element(ShadeHeader.Elements.FormatPlaceholder))
    val formatProgress =
        animateSharedFloatAsState(
            1.0f,
            ShadeHeader.Keys.transitionProgress,
            ShadeHeader.Elements.FormatPlaceholder
        )
    val useExpandedFormat by
        remember(formatProgress) { derivedStateOf { formatProgress.value > 0.5f } }

    Column(
        verticalArrangement = Arrangement.Bottom,
        modifier =
            modifier
                .element(ShadeHeader.Elements.ExpandedContent)
                .fillMaxWidth()
                .defaultMinSize(minHeight = ShadeHeader.Dimensions.ExpandedHeight)
    ) {
        Row {
            AndroidView(
                factory = { context ->
                    Clock(ContextThemeWrapper(context, R.style.TextAppearance_QS_Status), null)
                },
                modifier =
                    Modifier.align(Alignment.CenterVertically)
                        // use graphicsLayer instead of Modifier.scale to anchor transform to
                        // top left corner
                        .graphicsLayer(
                            scaleX = 2.57f,
                            scaleY = 2.57f,
                            transformOrigin = TransformOrigin(0f, 0.5f)
                        ),
            )
            Spacer(modifier = Modifier.weight(1f))
            ShadeCarrierGroup(
                viewModel = viewModel,
                modifier = Modifier.align(Alignment.CenterVertically),
            )
        }
        Spacer(modifier = Modifier.width(5.dp))
        Row {
            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(
                    useExpandedFormat = useExpandedFormat,
                    createBatteryMeterViewController = createBatteryMeterViewController,
                    modifier = Modifier.align(Alignment.CenterVertically),
                )
            }
        }
    }
}

@Composable
private fun BatteryIcon(
    createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
    useExpandedFormat: Boolean,
    modifier: Modifier = Modifier,
) {
    AndroidView(
        factory = { context ->
            val batteryIcon = BatteryMeterView(context, null)
            batteryIcon.setPercentShowMode(BatteryMeterView.MODE_ON)

            val batteryMaterViewController =
                createBatteryMeterViewController(batteryIcon, StatusBarLocation.QS)
            batteryMaterViewController.init()
            batteryMaterViewController.ignoreTunerUpdates()

            batteryIcon
        },
        update = { batteryIcon ->
            // TODO(b/298525212): use MODE_ESTIMATE in collapsed view when the screen
            //  has no center cutout. See [QsBatteryModeController.getBatteryMode]
            batteryIcon.setPercentShowMode(
                if (useExpandedFormat) {
                    BatteryMeterView.MODE_ESTIMATE
                } else {
                    BatteryMeterView.MODE_ON
                }
            )
        },
        modifier = modifier,
    )
}

@Composable
private fun ShadeCarrierGroup(
    viewModel: ShadeHeaderViewModel,
    modifier: Modifier = Modifier,
) {
    Row(modifier = modifier) {
        val subIds by viewModel.mobileSubIds.collectAsState()

        for (subId in subIds) {
            Spacer(modifier = Modifier.width(5.dp))
            AndroidView(
                factory = { context ->
                    ModernShadeCarrierGroupMobileView.constructAndBind(
                        context = context,
                        logger = viewModel.mobileIconsViewModel.logger,
                        slot = "mobile_carrier_shade_group",
                        viewModel =
                            (viewModel.mobileIconsViewModel.viewModelForSub(
                                subId,
                                StatusBarLocation.SHADE_CARRIER_GROUP
                            ) as ShadeCarrierGroupMobileIconViewModel),
                    )
                },
            )
        }
    }
}

@Composable
private fun StatusIcons(
    viewModel: ShadeHeaderViewModel,
    createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
    statusBarIconController: StatusBarIconController,
    useExpandedFormat: Boolean,
    modifier: Modifier = Modifier,
) {
    val carrierIconSlots =
        listOf(stringResource(id = com.android.internal.R.string.status_bar_mobile))
    val isSingleCarrier by viewModel.isSingleCarrier.collectAsState()
    val isTransitioning by viewModel.isTransitioning.collectAsState()

    AndroidView(
        factory = { context ->
            val iconContainer = StatusIconContainer(context, null)
            val iconManager = createTintedIconManager(iconContainer, StatusBarLocation.QS)
            iconManager.setTint(
                Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary)
            )
            statusBarIconController.addIconGroup(iconManager)

            iconContainer
        },
        update = { iconContainer ->
            iconContainer.setQsExpansionTransitioning(isTransitioning)
            if (isSingleCarrier || !useExpandedFormat) {
                iconContainer.removeIgnoredSlots(carrierIconSlots)
            } else {
                iconContainer.addIgnoredSlots(carrierIconSlots)
            }
        },
        modifier = modifier,
    )
}

@Composable
private fun SystemIconContainer(
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    // TODO(b/298524053): add hover state for this container
    Row(
        modifier = modifier.height(ShadeHeader.Dimensions.CollapsedHeight),
        content = content,
    )
}
+28 −5
Original line number Diff line number Diff line
@@ -16,9 +16,9 @@

package com.android.systemui.shade.ui.composable

import android.view.ViewGroup
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -33,6 +33,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.SceneScope
import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.notifications.ui.composable.Notifications
@@ -43,6 +44,9 @@ import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.scene.shared.model.UserAction
import com.android.systemui.scene.ui.composable.ComposableScene
import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel
import com.android.systemui.statusbar.phone.StatusBarIconController
import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager
import com.android.systemui.statusbar.phone.StatusBarLocation
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
@@ -77,6 +81,9 @@ class ShadeScene
constructor(
    @Application private val applicationScope: CoroutineScope,
    private val viewModel: ShadeSceneViewModel,
    private val tintedIconManagerFactory: TintedIconManager.Factory,
    private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
    private val statusBarIconController: StatusBarIconController,
) : ComposableScene {
    override val key = SceneKey.Shade

@@ -92,7 +99,14 @@ constructor(
    @Composable
    override fun SceneScope.Content(
        modifier: Modifier,
    ) = ShadeScene(viewModel, modifier)
    ) =
        ShadeScene(
            viewModel = viewModel,
            createTintedIconManager = tintedIconManagerFactory::create,
            createBatteryMeterViewController = batteryMeterViewControllerFactory::create,
            statusBarIconController = statusBarIconController,
            modifier = modifier,
        )

    private fun destinationScenes(
        up: SceneKey,
@@ -107,6 +121,9 @@ constructor(
@Composable
private fun SceneScope.ShadeScene(
    viewModel: ShadeSceneViewModel,
    createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
    createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
    statusBarIconController: StatusBarIconController,
    modifier: Modifier = Modifier,
) {
    Box(modifier.element(Shade.Elements.Scrim)) {
@@ -116,16 +133,22 @@ private fun SceneScope.ShadeScene(
                    .fillMaxSize()
                    .background(MaterialTheme.colorScheme.scrim, shape = Shade.Shapes.Scrim)
        )

        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(16.dp),
            modifier =
                Modifier.fillMaxSize()
                    .clickable(onClick = { viewModel.onContentClicked() })
                    .padding(horizontal = 16.dp, vertical = 48.dp)
                    .padding(start = 16.dp, end = 16.dp, bottom = 48.dp)
        ) {
            CollapsedShadeHeader(
                viewModel = viewModel.shadeHeaderViewModel,
                createTintedIconManager = createTintedIconManager,
                createBatteryMeterViewController = createBatteryMeterViewController,
                statusBarIconController = statusBarIconController,
            )
            Spacer(modifier = Modifier.height(16.dp))
            QuickSettings(modifier = Modifier.height(160.dp))
            Spacer(modifier = Modifier.height(16.dp))
            Notifications(modifier = Modifier.weight(1f))
        }
    }
+64 −0
Original line number Diff line number Diff line
package com.android.systemui.shade.ui.composable

import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel

@Composable
fun VariableDayDate(
    viewModel: ShadeHeaderViewModel,
    modifier: Modifier = Modifier,
) {
    val longerText = viewModel.longerDateText.collectAsState()
    val shorterText = viewModel.shorterDateText.collectAsState()

    Layout(
        contents =
            listOf(
                {
                    Text(
                        text = longerText.value,
                        style = MaterialTheme.typography.titleSmall,
                        color = MaterialTheme.colorScheme.onBackground,
                        maxLines = 1,
                    )
                },
                {
                    Text(
                        text = shorterText.value,
                        style = MaterialTheme.typography.titleSmall,
                        color = MaterialTheme.colorScheme.onBackground,
                        maxLines = 1,
                    )
                },
            ),
        modifier = modifier,
    ) { measureables, constraints ->
        check(measureables.size == 2)
        check(measureables[0].size == 1)
        check(measureables[1].size == 1)

        val longerMeasurable = measureables[0][0]
        val shorterMeasurable = measureables[1][0]

        val longerPlaceable = longerMeasurable.measure(constraints)
        val shorterPlaceable = shorterMeasurable.measure(constraints)

        // If width < maxWidth (and not <=), we can assume that the text fits.
        val placeable =
            when {
                longerPlaceable.width < constraints.maxWidth &&
                    longerPlaceable.height <= constraints.maxHeight -> longerPlaceable
                shorterPlaceable.width < constraints.maxWidth &&
                    shorterPlaceable.height <= constraints.maxHeight -> shorterPlaceable
                else -> null
            }

        layout(placeable?.width ?: 0, placeable?.height ?: 0) { placeable?.placeRelative(0, 0) }
    }
}
Loading