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

Commit b7b68024 authored by Ahmed Mehfooz's avatar Ahmed Mehfooz Committed by Android (Google) Code Review
Browse files

Merge "[SB] Only show composable chips when there is enough space" into main

parents b0fa31bc e965fa16
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -1781,6 +1781,7 @@
    <dimen name="wallet_button_vertical_padding">8dp</dimen>

    <!-- Ongoing activity chip -->
    <dimen name="ongoing_activity_chip_min_text_width">12dp</dimen>
    <dimen name="ongoing_activity_chip_max_text_width">74dp</dimen>
    <dimen name="ongoing_activity_chip_margin_start">5dp</dimen>
    <!-- The activity chip side padding, used with the default phone icon. -->
+140 −15
Original line number Diff line number Diff line
@@ -16,12 +16,20 @@

package com.android.systemui.statusbar.chips.ui.compose

import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
@@ -29,12 +37,15 @@ import androidx.compose.ui.node.LayoutModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.constrain
import androidx.compose.ui.unit.dp
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.viewmodel.rememberChronometerState
import kotlin.math.min

@Composable
fun ChipContent(viewModel: OngoingActivityChipModel.Shown, modifier: Modifier = Modifier) {
@@ -43,6 +54,9 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Shown, modifier: Modifier =
    val hasEmbeddedIcon =
        viewModel.icon is OngoingActivityChipModel.ChipIcon.StatusBarView ||
            viewModel.icon is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon
    val textStyle = MaterialTheme.typography.labelLarge
    val textColor = Color(viewModel.colors.text(context))
    val maxTextWidth = dimensionResource(id = R.dimen.ongoing_activity_chip_max_text_width)
    val startPadding =
        if (isTextOnly || hasEmbeddedIcon) {
            0.dp
@@ -57,38 +71,69 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Shown, modifier: Modifier =
        } else {
            0.dp
        }
    val textStyle = MaterialTheme.typography.labelLarge
    val textColor = Color(viewModel.colors.text(context))
    val textMeasurer = rememberTextMeasurer()
    when (viewModel) {
        is OngoingActivityChipModel.Shown.Timer -> {
            val timerState = rememberChronometerState(startTimeMillis = viewModel.startTimeMs)
            val text = timerState.currentTimeText
            Text(
                text = timerState.currentTimeText,
                text = text,
                style = textStyle,
                color = textColor,
                softWrap = false,
                modifier =
                    modifier.padding(start = startPadding, end = endPadding).neverDecreaseWidth(),
                    modifier
                        .customTextContentLayout(
                            maxTextWidth = maxTextWidth,
                            startPadding = startPadding,
                            endPadding = endPadding,
                        ) { constraintWidth ->
                            val intrinsicWidth =
                                textMeasurer.measure(text, textStyle, softWrap = false).size.width
                            intrinsicWidth <= constraintWidth
                        }
                        .neverDecreaseWidth(),
            )
        }

        is OngoingActivityChipModel.Shown.Countdown -> {
            ChipText(
                text = viewModel.secondsUntilStarted.toString(),
            val text = viewModel.secondsUntilStarted.toString()
            Text(
                text = text,
                style = textStyle,
                color = textColor,
                modifier =
                    modifier.padding(start = startPadding, end = endPadding).neverDecreaseWidth(),
                backgroundColor = Color(viewModel.colors.background(context).defaultColor),
                softWrap = false,
                modifier = modifier.neverDecreaseWidth(),
            )
        }

        is OngoingActivityChipModel.Shown.Text -> {
            ChipText(
                text = viewModel.text,
                style = textStyle,
            var hasOverflow by remember { mutableStateOf(false) }
            val text = viewModel.text
            Text(
                text = text,
                color = textColor,
                modifier = modifier.padding(start = startPadding, end = endPadding),
                backgroundColor = Color(viewModel.colors.background(context).defaultColor),
                style = textStyle,
                softWrap = false,
                modifier =
                    modifier
                        .customTextContentLayout(
                            maxTextWidth = maxTextWidth,
                            startPadding = startPadding,
                            endPadding = endPadding,
                        ) { constraintWidth ->
                            val intrinsicWidth =
                                textMeasurer.measure(text, textStyle, softWrap = false).size.width
                            hasOverflow = intrinsicWidth > constraintWidth
                            constraintWidth.toFloat() / intrinsicWidth.toFloat() > 0.5f
                        }
                        .overflowFadeOut(
                            hasOverflow = { hasOverflow },
                            fadeLength =
                                dimensionResource(
                                    id = R.dimen.ongoing_activity_chip_text_fading_edge_length
                                ),
                        ),
            )
        }

@@ -133,3 +178,83 @@ private class NeverDecreaseWidthNode : Modifier.Node(), LayoutModifierNode {
        return layout(width, height) { placeable.place(0, 0) }
    }
}

/**
 * A custom layout modifier for text that ensures its text is only visible if a provided
 * [shouldShow] callback returns true. Imposes a provided [maxTextWidthPx]. Also, accounts for
 * provided padding values if provided and ensures its text is placed with the provided padding
 * included around it.
 */
private fun Modifier.customTextContentLayout(
    maxTextWidth: Dp,
    startPadding: Dp = 0.dp,
    endPadding: Dp = 0.dp,
    shouldShow: (constraintWidth: Int) -> Boolean,
): Modifier {
    return this.then(
        CustomTextContentLayoutElement(maxTextWidth, startPadding, endPadding, shouldShow)
    )
}

private data class CustomTextContentLayoutElement(
    val maxTextWidth: Dp,
    val startPadding: Dp,
    val endPadding: Dp,
    val shouldShow: (constrainedWidth: Int) -> Boolean,
) : ModifierNodeElement<CustomTextContentLayoutNode>() {
    override fun create(): CustomTextContentLayoutNode {
        return CustomTextContentLayoutNode(maxTextWidth, startPadding, endPadding, shouldShow)
    }

    override fun update(node: CustomTextContentLayoutNode) {
        node.shouldShow = shouldShow
        node.maxTextWidth = maxTextWidth
        node.startPadding = startPadding
        node.endPadding = endPadding
    }
}

private class CustomTextContentLayoutNode(
    var maxTextWidth: Dp,
    var startPadding: Dp,
    var endPadding: Dp,
    var shouldShow: (constrainedWidth: Int) -> Boolean,
) : Modifier.Node(), LayoutModifierNode {
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints,
    ): MeasureResult {
        val horizontalPadding = startPadding + endPadding
        val maxWidth =
            min(maxTextWidth.roundToPx(), (constraints.maxWidth - horizontalPadding.roundToPx()))
                .coerceAtLeast(constraints.minWidth)
        val placeable = measurable.measure(constraints.copy(maxWidth = maxWidth))

        val height = placeable.height
        val width = placeable.width
        return if (shouldShow(maxWidth)) {
            layout(width + horizontalPadding.roundToPx(), height) {
                placeable.place(startPadding.roundToPx(), 0)
            }
        } else {
            layout(0, 0) {}
        }
    }
}

private fun Modifier.overflowFadeOut(hasOverflow: () -> Boolean, fadeLength: Dp): Modifier {
    return graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen).drawWithCache {
        val width = size.width
        val start = (width - fadeLength.toPx()).coerceAtLeast(0f)
        val gradient =
            Brush.horizontalGradient(
                colors = listOf(Color.Black, Color.Transparent),
                startX = start,
                endX = width,
            )
        onDrawWithContent {
            drawContent()
            if (hasOverflow()) drawRect(brush = gradient, blendMode = BlendMode.DstIn)
        }
    }
}
+0 −114
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.statusbar.chips.ui.compose

import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer
import com.android.systemui.res.R

/**
 * Renders text within a status bar chip. The text is only displayed if more than 50% of its width
 * can fit inside the bounds of the chip. If there is any overflow,
 * [R.dimen.ongoing_activity_chip_text_fading_edge_length] is used to fade out the edge of the text.
 */
@Composable
fun ChipText(
    text: String,
    backgroundColor: Color,
    modifier: Modifier = Modifier,
    color: Color = Color.Unspecified,
    style: TextStyle = LocalTextStyle.current,
    minimumVisibleRatio: Float = 0.5f,
) {
    val density = LocalDensity.current
    val textMeasurer = rememberTextMeasurer()

    val textFadeLength =
        dimensionResource(id = R.dimen.ongoing_activity_chip_text_fading_edge_length)
    val maxTextWidthDp = dimensionResource(id = R.dimen.ongoing_activity_chip_max_text_width)
    val maxTextWidthPx = with(density) { maxTextWidthDp.toPx() }

    val textLayoutResult = remember(text, style) { textMeasurer.measure(text, style) }
    val willOverflowWidth = textLayoutResult.size.width > maxTextWidthPx

    if (isSufficientlyVisible(maxTextWidthPx, minimumVisibleRatio, textLayoutResult)) {
        Text(
            text = text,
            style = style,
            softWrap = false,
            color = color,
            modifier =
                modifier
                    .sizeIn(maxWidth = maxTextWidthDp)
                    .then(
                        if (willOverflowWidth) {
                            Modifier.overflowFadeOut(
                                with(density) { textFadeLength.roundToPx() },
                                backgroundColor,
                            )
                        } else {
                            Modifier
                        }
                    ),
        )
    }
}

private fun Modifier.overflowFadeOut(fadeLength: Int, color: Color): Modifier = drawWithContent {
    drawContent()

    val brush =
        Brush.horizontalGradient(
            colors = listOf(Color.Transparent, color),
            startX = size.width - fadeLength,
            endX = size.width,
        )
    drawRect(
        brush = brush,
        topLeft = Offset(size.width - fadeLength, 0f),
        size = Size(fadeLength.toFloat(), size.height),
    )
}

/**
 * Returns `true` if at least [minimumVisibleRatio] of the text width fits within the given
 * [maxAvailableWidthPx].
 */
@Composable
private fun isSufficientlyVisible(
    maxAvailableWidthPx: Float,
    minimumVisibleRatio: Float,
    textLayoutResult: TextLayoutResult,
): Boolean {
    val widthPx = textLayoutResult.size.width

    return (maxAvailableWidthPx / widthPx) > minimumVisibleRatio
}
+29 −16
Original line number Diff line number Diff line
@@ -35,6 +35,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.semantics.contentDescription
@@ -42,6 +43,7 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.android.compose.animation.Expandable
import com.android.compose.modifiers.thenIf
import com.android.systemui.animation.Expandable
import com.android.systemui.common.ui.compose.Icon
import com.android.systemui.common.ui.compose.load
@@ -79,12 +81,11 @@ fun OngoingActivityChip(model: OngoingActivityChipModel.Shown, modifier: Modifie
private fun ChipBody(
    model: OngoingActivityChipModel.Shown,
    modifier: Modifier = Modifier,
    onClick: () -> Unit = {},
    onClick: (() -> Unit)? = null,
) {
    val context = LocalContext.current
    val isClickable = onClick != {}
    val isClickable = onClick != null
    val hasEmbeddedIcon = model.icon is OngoingActivityChipModel.ChipIcon.StatusBarView

    val contentDescription =
        when (val icon = model.icon) {
            is OngoingActivityChipModel.ChipIcon.StatusBarView -> icon.contentDescription.load()
@@ -93,13 +94,24 @@ private fun ChipBody(
            is OngoingActivityChipModel.ChipIcon.SingleColorIcon -> null
            null -> null
        }

    val chipSidePadding = dimensionResource(id = R.dimen.ongoing_activity_chip_side_padding)
    val minWidth =
        if (isClickable) {
            dimensionResource(id = R.dimen.min_clickable_item_size)
        } else if (model.icon != null) {
            dimensionResource(id = R.dimen.ongoing_activity_chip_icon_size) + chipSidePadding
        } else {
            dimensionResource(id = R.dimen.ongoing_activity_chip_min_text_width) + chipSidePadding
        }
    // Use a Box with `fillMaxHeight` to create a larger click surface for the chip. The visible
    // height of the chip is determined by the height of the background of the Row below.
    Box(
        contentAlignment = Alignment.Center,
        modifier =
            modifier.fillMaxHeight().clickable(enabled = isClickable, onClick = onClick).semantics {
            modifier
                .fillMaxHeight()
                .clickable(enabled = isClickable, onClick = onClick ?: {})
                .semantics {
                    if (contentDescription != null) {
                        this.contentDescription = contentDescription
                    }
@@ -115,14 +127,15 @@ private fun ChipBody(
                        )
                    )
                    .height(dimensionResource(R.dimen.ongoing_appops_chip_height))
                    .widthIn(
                        min =
                            if (isClickable) {
                                dimensionResource(id = R.dimen.min_clickable_item_size)
                            } else {
                                0.dp
                    .thenIf(isClickable) { Modifier.widthIn(min = minWidth) }
                    .layout { measurable, constraints ->
                        val placeable = measurable.measure(constraints)
                        layout(placeable.width, placeable.height) {
                            if (constraints.maxWidth >= minWidth.roundToPx()) {
                                placeable.place(0, 0)
                            }
                        }
                    }
                    )
                    .background(Color(model.colors.background(context).defaultColor))
                    .padding(
                        horizontal =