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

Commit 31f9b781 authored by Caitlin Shkuratov's avatar Caitlin Shkuratov
Browse files

[SB][Notif] Hide text in chips if not *all* of the text fits.

Previously, we showed text with a fadeout at the end as long as at least
50% of the text fits. New POR is to only show the text if 100% of it
fits.

Bug: 394611253
Bug: 364653005
Flag: com.android.systemui.status_bar_notification_chips
Test: Trigger chip with shortCriticalText then increase & decrease font
size -> verify chip text hides & shows depending on whether the text
fits or not. Verify with chips_modernization both OFF and ON
Test: atest ChipTextTruncationHelperTest

Change-Id: Iba36a5ee82d0d0c70453750765800bdb69749a3e
parent afc6f814
Loading
Loading
Loading
Loading
+5 −16
Original line number Diff line number Diff line
@@ -52,24 +52,13 @@ class ChipTextTruncationHelperTest : SysuiTestCase() {
    }

    @Test
    fun shouldShowText_desiredSlightlyLargerThanMax_true() {
    fun shouldShowText_desiredMoreThanMax_false() {
        val result =
            underTest.shouldShowText(
                desiredTextWidthPx = (MAX_WIDTH * 1.1).toInt(),
                widthMeasureSpec = UNLIMITED_WIDTH_SPEC,
            )

        assertThat(result).isTrue()
    }

    @Test
    fun shouldShowText_desiredMoreThanTwiceMax_false() {
        val result =
            underTest.shouldShowText(
                desiredTextWidthPx = (MAX_WIDTH * 2.2).toInt(),
                widthMeasureSpec = UNLIMITED_WIDTH_SPEC,
            )

        assertThat(result).isFalse()
    }

@@ -80,8 +69,8 @@ class ChipTextTruncationHelperTest : SysuiTestCase() {
                View.MeasureSpec.makeMeasureSpec(MAX_WIDTH / 2, View.MeasureSpec.AT_MOST)
            )

        // WHEN desired is more than twice the smallerWidthSpec
        val desiredWidth = (MAX_WIDTH * 1.1).toInt()
        // WHEN desired is more than the smallerWidthSpec
        val desiredWidth = ((MAX_WIDTH / 2) * 1.1).toInt()

        val result =
            underTest.shouldShowText(
@@ -100,8 +89,8 @@ class ChipTextTruncationHelperTest : SysuiTestCase() {
                View.MeasureSpec.makeMeasureSpec(MAX_WIDTH * 3, View.MeasureSpec.AT_MOST)
            )

        // WHEN desired is more than twice the max
        val desiredWidth = (MAX_WIDTH * 2.2).toInt()
        // WHEN desired is more than the max
        val desiredWidth = (MAX_WIDTH * 1.1).toInt()

        val result =
            underTest.shouldShowText(
+2 −5
Original line number Diff line number Diff line
@@ -43,9 +43,6 @@
             ongoing_activity_chip_short_time_delta] will ever be shown at one time. -->

        <!-- Shows a timer, like 00:01. -->
        <!-- Don't use the LimitedWidth style for the timer because the end of the timer is often
             the most important value. ChipChronometer has the correct logic for when the timer is
             too large for the space allowed. -->
        <com.android.systemui.statusbar.chips.ui.view.ChipChronometer
            android:id="@+id/ongoing_activity_chip_time"
            style="@style/StatusBar.Chip.Text"
@@ -54,14 +51,14 @@
        <!-- Shows generic text. -->
        <com.android.systemui.statusbar.chips.ui.view.ChipTextView
            android:id="@+id/ongoing_activity_chip_text"
            style="@style/StatusBar.Chip.Text.LimitedWidth"
            style="@style/StatusBar.Chip.Text"
            android:visibility="gone"
            />

        <!-- Shows a time delta in short form, like "15min" or "1hr". -->
        <com.android.systemui.statusbar.chips.ui.view.ChipDateTimeView
            android:id="@+id/ongoing_activity_chip_short_time_delta"
            style="@style/StatusBar.Chip.Text.LimitedWidth"
            style="@style/StatusBar.Chip.Text"
            android:visibility="gone"
            />

+0 −9
Original line number Diff line number Diff line
@@ -93,15 +93,6 @@
        <item name="android:textColor">?android:attr/colorPrimary</item>
    </style>

    <!-- Style for a status bar chip text that has a maximum width. Since there's so little room in
         the status bar chip area, don't ellipsize the text and instead just fade it out a bit at
         the end. -->
    <style name="StatusBar.Chip.Text.LimitedWidth">
        <item name="android:ellipsize">none</item>
        <item name="android:requiresFadingEdge">horizontal</item>
        <item name="android:fadingEdgeLength">@dimen/ongoing_activity_chip_text_fading_edge_length</item>
    </style>

    <style name="Chipbar" />

    <style name="Chipbar.Text" parent="@*android:style/TextAppearance.DeviceDefault.Notification.Title">
+57 −68
Original line number Diff line number Diff line
@@ -20,16 +20,9 @@ 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
@@ -37,6 +30,8 @@ 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.TextMeasurer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
@@ -83,15 +78,14 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier =
                softWrap = false,
                modifier =
                    modifier
                        .customTextContentLayout(
                        .hideTextIfDoesNotFit(
                            text = text,
                            textStyle = textStyle,
                            textMeasurer = textMeasurer,
                            maxTextWidth = maxTextWidth,
                            startPadding = startPadding,
                            endPadding = endPadding,
                        ) { constraintWidth ->
                            val intrinsicWidth =
                                textMeasurer.measure(text, textStyle, softWrap = false).size.width
                            intrinsicWidth <= constraintWidth
                        }
                        )
                        .neverDecreaseWidth(),
            )
        }
@@ -108,7 +102,6 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier =
        }

        is OngoingActivityChipModel.Active.Text -> {
            var hasOverflow by remember { mutableStateOf(false) }
            val text = viewModel.text
            Text(
                text = text,
@@ -116,23 +109,13 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier =
                style = textStyle,
                softWrap = false,
                modifier =
                    modifier
                        .customTextContentLayout(
                    modifier.hideTextIfDoesNotFit(
                        text = text,
                        textStyle = textStyle,
                        textMeasurer = textMeasurer,
                        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
                                ),
                    ),
            )
        }
@@ -180,45 +163,67 @@ private class NeverDecreaseWidthNode : Modifier.Node(), LayoutModifierNode {
}

/**
 * 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.
 * A custom layout modifier for text that ensures the text is only visible if it completely fits
 * within the constrained bounds. 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(
private fun Modifier.hideTextIfDoesNotFit(
    text: String,
    textStyle: TextStyle,
    textMeasurer: TextMeasurer,
    maxTextWidth: Dp,
    startPadding: Dp = 0.dp,
    endPadding: Dp = 0.dp,
    shouldShow: (constraintWidth: Int) -> Boolean,
): Modifier {
    return this.then(
        CustomTextContentLayoutElement(maxTextWidth, startPadding, endPadding, shouldShow)
        HideTextIfDoesNotFitElement(
            text,
            textStyle,
            textMeasurer,
            maxTextWidth,
            startPadding,
            endPadding,
        )
    )
}

private data class CustomTextContentLayoutElement(
private data class HideTextIfDoesNotFitElement(
    val text: String,
    val textStyle: TextStyle,
    val textMeasurer: TextMeasurer,
    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)
) : ModifierNodeElement<HideTextIfDoesNotFitNode>() {
    override fun create(): HideTextIfDoesNotFitNode {
        return HideTextIfDoesNotFitNode(
            text,
            textStyle,
            textMeasurer,
            maxTextWidth,
            startPadding,
            endPadding,
        )
    }

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

private class CustomTextContentLayoutNode(
private class HideTextIfDoesNotFitNode(
    var text: String,
    var textStyle: TextStyle,
    var textMeasurer: TextMeasurer,
    var maxTextWidth: Dp,
    var startPadding: Dp,
    var endPadding: Dp,
    var shouldShow: (constrainedWidth: Int) -> Boolean,
) : Modifier.Node(), LayoutModifierNode {
    override fun MeasureScope.measure(
        measurable: Measurable,
@@ -230,9 +235,10 @@ private class CustomTextContentLayoutNode(
                .coerceAtLeast(constraints.minWidth)
        val placeable = measurable.measure(constraints.copy(maxWidth = maxWidth))

        val intrinsicWidth = textMeasurer.measure(text, textStyle, softWrap = false).size.width
        return if (intrinsicWidth <= maxWidth) {
            val height = placeable.height
            val width = placeable.width
        return if (shouldShow(maxWidth)) {
            layout(width + horizontalPadding.roundToPx(), height) {
                placeable.place(startPadding.roundToPx(), 0)
            }
@@ -241,20 +247,3 @@ private class CustomTextContentLayoutNode(
        }
    }
}

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)
        }
    }
}
+4 −6
Original line number Diff line number Diff line
@@ -51,9 +51,8 @@ class ChipTextTruncationHelper(private val view: View) {
    }

    /**
     * Returns true if this view should show the text because there's enough room for a substantial
     * amount of text, and returns false if this view should hide the text because the text is much
     * too long.
     * Returns true if this view should show the text because there's enough room for all the text,
     * and returns false if this view should hide the text because not all of it fits.
     *
     * @param desiredTextWidthPx should be calculated by having the view measure itself with
     *   [unlimitedWidthMeasureSpec] and then sending its `measuredWidth` to this method. (This
@@ -82,9 +81,8 @@ class ChipTextTruncationHelper(private val view: View) {
            enforcedTextWidth = maxWidthBasedOnDimension
        }

        // Only show the text if at least 50% of it can show. (Assume that if < 50% of the text will
        // be visible, the text will be more confusing than helpful.)
        return desiredTextWidthPx <= enforcedTextWidth * 2
        // Only show the text if all of it can show
        return desiredTextWidthPx <= enforcedTextWidth
    }

    private fun fetchMaxWidth() =