Loading packages/SystemUI/res/values/dimens.xml +1 −0 Original line number Diff line number Diff line Loading @@ -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. --> Loading packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt +140 −15 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) { Loading @@ -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 Loading @@ -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 ), ), ) } Loading Loading @@ -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) } } } packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipText.ktdeleted 100644 → 0 +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 } packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt +29 −16 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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() Loading @@ -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 } Loading @@ -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 = Loading Loading
packages/SystemUI/res/values/dimens.xml +1 −0 Original line number Diff line number Diff line Loading @@ -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. --> Loading
packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt +140 −15 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) { Loading @@ -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 Loading @@ -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 ), ), ) } Loading Loading @@ -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) } } }
packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipText.ktdeleted 100644 → 0 +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 }
packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt +29 −16 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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() Loading @@ -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 } Loading @@ -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 = Loading