diff options
3 files changed, 242 insertions, 158 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt index e1b21ef99f42..9233e76f09e6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt @@ -16,7 +16,6 @@ package com.android.systemui.qs.panels.ui.compose -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -40,6 +39,13 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.addOutline +import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp @@ -268,10 +274,9 @@ class PartitionedGridLayout @Inject constructor(private val viewModel: Partition private fun CurrentTilesContainer(content: @Composable () -> Unit) { Box( Modifier.fillMaxWidth() - .border( - width = 1.dp, - color = MaterialTheme.colorScheme.onBackground, - shape = RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius)) + .dashedBorder( + color = MaterialTheme.colorScheme.onBackground.copy(alpha = .5f), + shape = Dimensions.ContainerShape, ) .padding(dimensionResource(R.dimen.qs_tile_margin_vertical)) ) { @@ -286,7 +291,7 @@ class PartitionedGridLayout @Inject constructor(private val viewModel: Partition .background( color = MaterialTheme.colorScheme.background, alpha = { 1f }, - shape = RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius)) + shape = Dimensions.ContainerShape, ) .padding(dimensionResource(R.dimen.qs_tile_margin_vertical)) ) { @@ -305,4 +310,27 @@ class PartitionedGridLayout @Inject constructor(private val viewModel: Partition item(span = { GridItemSpan(maxCurrentLineSpan) }) { Spacer(Modifier) } } } + + private fun Modifier.dashedBorder( + color: Color, + shape: Shape, + ): Modifier { + return this.drawWithContent { + val outline = shape.createOutline(size, layoutDirection, this) + val path = Path() + path.addOutline(outline) + val stroke = + Stroke( + width = 1.dp.toPx(), + pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f)) + ) + this.drawContent() + drawPath(path = path, style = stroke, color = color) + } + } + + private object Dimensions { + // Corner radius is half the height of a tile + padding + val ContainerShape = RoundedCornerShape(48.dp) + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt index e4fbb4bb7c61..bbb98d3ca277 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalFoundationApi::class) + package com.android.systemui.qs.panels.ui.compose import android.graphics.drawable.Animatable @@ -27,17 +29,17 @@ import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter import androidx.compose.animation.graphics.vector.AnimatedImageVector import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.basicMarquee -import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -46,11 +48,6 @@ import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Remove -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -68,7 +65,6 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription @@ -78,9 +74,11 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.Expandable +import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.compose.Icon import com.android.systemui.common.ui.compose.load +import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.panels.ui.viewmodel.AvailableEditActions import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.TileUiState @@ -90,13 +88,14 @@ import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.res.R +import java.util.function.Supplier import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.mapLatest object TileType -@OptIn(ExperimentalCoroutinesApi::class, ExperimentalFoundationApi::class) +@OptIn(ExperimentalCoroutinesApi::class) @Composable fun Tile( tile: TileViewModel, @@ -110,46 +109,143 @@ fun Tile( .collectAsStateWithLifecycle(tile.currentState.toUiState()) val colors = TileDefaults.getColorForState(state.state) - val context = LocalContext.current - - Expandable( - color = colors.background, - shape = RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius)), + TileContainer( + colors = colors, + showLabels = showLabels, + label = state.label.toString(), + iconOnly = iconOnly, + onClick = tile::onClick, + onLongClick = tile::onLongClick, + modifier = modifier, ) { - Row( - modifier = - modifier - .combinedClickable( - onClick = { tile.onClick(it) }, - onLongClick = { tile.onLongClick(it) } - ) - .tileModifier(colors), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = tileHorizontalArrangement(iconOnly), - ) { - val icon = - remember(state.icon) { - state.icon.get().let { - if (it is QSTileImpl.ResourceIcon) { - Icon.Resource(it.resId, null) - } else { - Icon.Loaded(it.getDrawable(context), null) - } - } - } - TileContent( + val icon = getTileIcon(icon = state.icon) + if (iconOnly) { + TileIcon(icon = icon, color = colors.icon, modifier = Modifier.align(Alignment.Center)) + } else { + LargeTileContent( label = state.label.toString(), secondaryLabel = state.secondaryLabel.toString(), icon = icon, colors = colors, - iconOnly = iconOnly, - showLabels = showLabels, + onClick = tile::onSecondaryClick, + onLongClick = tile::onLongClick, ) } } } @Composable +private fun TileContainer( + colors: TileColors, + showLabels: Boolean, + label: String, + iconOnly: Boolean, + clickEnabled: Boolean = true, + onClick: (Expandable) -> Unit = {}, + onLongClick: (Expandable) -> Unit = {}, + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = + spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin), Alignment.Top), + modifier = modifier, + ) { + val backgroundColor = + if (iconOnly) { + colors.iconBackground + } else { + colors.background + } + Expandable( + color = backgroundColor, + shape = TileDefaults.TileShape, + modifier = + Modifier.height(dimensionResource(id = R.dimen.qs_tile_height)) + .clip(TileDefaults.TileShape) + ) { + Box( + modifier = + Modifier.fillMaxSize() + .combinedClickable( + enabled = clickEnabled, + onClick = { onClick(it) }, + onLongClick = { onLongClick(it) } + ) + .tilePadding(), + ) { + content() + } + } + + if (showLabels && iconOnly) { + Text( + label, + maxLines = 2, + color = colors.label, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + ) + } + } +} + +@Composable +private fun LargeTileContent( + label: String, + secondaryLabel: String?, + icon: Icon, + colors: TileColors, + clickEnabled: Boolean = true, + onClick: (Expandable) -> Unit = {}, + onLongClick: (Expandable) -> Unit = {}, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = tileHorizontalArrangement() + ) { + Expandable( + color = colors.iconBackground, + shape = TileDefaults.TileShape, + modifier = Modifier.fillMaxHeight().aspectRatio(1f) + ) { + Box( + modifier = + Modifier.fillMaxSize() + .clip(TileDefaults.TileShape) + .combinedClickable( + enabled = clickEnabled, + onClick = { onClick(it) }, + onLongClick = { onLongClick(it) } + ) + ) { + TileIcon( + icon = icon, + color = colors.icon, + modifier = Modifier.align(Alignment.Center) + ) + } + } + + Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) { + Text( + label, + color = colors.label, + modifier = Modifier.basicMarquee(), + ) + if (!TextUtils.isEmpty(secondaryLabel)) { + Text( + secondaryLabel ?: "", + color = colors.secondaryLabel, + modifier = Modifier.basicMarquee(), + ) + } + } + } +} + +@Composable fun TileLazyGrid( modifier: Modifier = Modifier, columns: GridCells, @@ -247,41 +343,19 @@ fun LazyGridScope.editTiles( "" } - Box( + val iconOnly = isIconOnly(viewModel.tileSpec) + val tileHeight = tileHeight(iconOnly && showLabels) + EditTile( + tileViewModel = viewModel, + iconOnly = iconOnly, + showLabels = showLabels, + clickEnabled = canClick, + onClick = { onClick.invoke(viewModel.tileSpec) }, modifier = - Modifier.clickable(enabled = canClick) { onClick.invoke(viewModel.tileSpec) } - .animateItem() - .semantics { - onClick(onClickActionName) { false } - this.stateDescription = stateDescription - } - ) { - val iconOnly = isIconOnly(viewModel.tileSpec) - val tileHeight = tileHeight(iconOnly && showLabels) - EditTile( - tileViewModel = viewModel, - iconOnly = iconOnly, - showLabels = showLabels, - modifier = Modifier.height(tileHeight) - ) - if (canClick) { - Badge(clickAction, Modifier.align(Alignment.TopEnd)) - } - } - } -} - -@Composable -fun Badge(action: ClickAction, modifier: Modifier = Modifier) { - Box(modifier = modifier.size(16.dp).background(Color.Cyan, shape = CircleShape)) { - Icon( - imageVector = - when (action) { - ClickAction.ADD -> Icons.Filled.Add - ClickAction.REMOVE -> Icons.Filled.Remove - }, - "", - tint = Color.Black, + Modifier.height(tileHeight).animateItem().semantics { + onClick(onClickActionName) { false } + this.stateDescription = stateDescription + } ) } } @@ -291,25 +365,40 @@ fun EditTile( tileViewModel: EditTileViewModel, iconOnly: Boolean, showLabels: Boolean, + clickEnabled: Boolean, + onClick: () -> Unit, modifier: Modifier = Modifier, ) { val label = tileViewModel.label.load() ?: tileViewModel.tileSpec.spec val colors = TileDefaults.inactiveTileColors() - Row( - modifier = modifier.tileModifier(colors).semantics { this.contentDescription = label }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = tileHorizontalArrangement(iconOnly) + TileContainer( + colors = colors, + showLabels = showLabels, + label = label, + iconOnly = iconOnly, + clickEnabled = clickEnabled, + onClick = { onClick() }, + onLongClick = { onClick() }, + modifier = modifier, ) { - TileContent( - label = label, - secondaryLabel = tileViewModel.appName?.load(), - colors = colors, - icon = tileViewModel.icon, - iconOnly = iconOnly, - showLabels = showLabels, - animateIconToEnd = true, - ) + if (iconOnly) { + TileIcon( + icon = tileViewModel.icon, + color = colors.icon, + modifier = Modifier.align(Alignment.Center) + ) + } else { + LargeTileContent( + label = label, + secondaryLabel = tileViewModel.appName?.load(), + icon = tileViewModel.icon, + colors = colors, + clickEnabled = clickEnabled, + onClick = { onClick() }, + onLongClick = { onClick() }, + ) + } } } @@ -318,14 +407,27 @@ enum class ClickAction { REMOVE, } +@Composable +private fun getTileIcon(icon: Supplier<QSTile.Icon>): Icon { + val context = LocalContext.current + return icon.get().let { + if (it is QSTileImpl.ResourceIcon) { + Icon.Resource(it.resId, null) + } else { + Icon.Loaded(it.getDrawable(context), null) + } + } +} + @OptIn(ExperimentalAnimationGraphicsApi::class) @Composable private fun TileIcon( icon: Icon, color: Color, animateToEnd: Boolean = false, + modifier: Modifier = Modifier, ) { - val modifier = Modifier.size(dimensionResource(id = R.dimen.qs_icon_size)) + val iconModifier = modifier.size(dimensionResource(id = R.dimen.qs_icon_size)) val context = LocalContext.current val loadedDrawable = remember(icon, context) { @@ -338,7 +440,7 @@ private fun TileIcon( Icon( icon = icon, tint = color, - modifier = modifier, + modifier = iconModifier, ) } else if (icon is Icon.Resource) { val image = AnimatedImageVector.animatedVectorResource(id = icon.res) @@ -357,80 +459,25 @@ private fun TileIcon( painter = painter, contentDescription = null, colorFilter = ColorFilter.tint(color = color), - modifier = modifier + modifier = iconModifier ) } } @Composable -private fun Modifier.tileModifier(colors: TileColors): Modifier { - return fillMaxWidth() - .clip(RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius))) - .background(colors.background) - .padding(horizontal = dimensionResource(id = R.dimen.qs_label_container_margin)) +private fun Modifier.tilePadding(): Modifier { + return padding(dimensionResource(id = R.dimen.qs_label_container_margin)) } @Composable -private fun tileHorizontalArrangement(iconOnly: Boolean): Arrangement.Horizontal { - val horizontalAlignment = - if (iconOnly) { - Alignment.CenterHorizontally - } else { - Alignment.Start - } +private fun tileHorizontalArrangement(): Arrangement.Horizontal { return spacedBy( space = dimensionResource(id = R.dimen.qs_label_container_margin), - alignment = horizontalAlignment + alignment = Alignment.Start ) } @Composable -private fun TileContent( - label: String, - secondaryLabel: String?, - icon: Icon, - colors: TileColors, - iconOnly: Boolean, - showLabels: Boolean = false, - animateIconToEnd: Boolean = false, -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxHeight() - ) { - TileIcon(icon, colors.icon, animateIconToEnd) - - if (iconOnly && showLabels) { - Text( - label, - maxLines = 2, - color = colors.label, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, - ) - } - } - - if (!iconOnly) { - Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) { - Text( - label, - color = colors.label, - modifier = Modifier.basicMarquee(), - ) - if (!TextUtils.isEmpty(secondaryLabel)) { - Text( - secondaryLabel ?: "", - color = colors.secondaryLabel, - modifier = Modifier.basicMarquee(), - ) - } - } - } -} - -@Composable fun tileHeight(iconWithLabel: Boolean = false): Dp { return if (iconWithLabel) { TileDefaults.IconTileWithLabelHeight @@ -441,20 +488,23 @@ fun tileHeight(iconWithLabel: Boolean = false): Dp { private data class TileColors( val background: Color, + val iconBackground: Color, val label: Color, val secondaryLabel: Color, val icon: Color, ) private object TileDefaults { - val IconTileWithLabelHeight = 100.dp + val TileShape = CircleShape + val IconTileWithLabelHeight = 140.dp @Composable fun activeTileColors(): TileColors = TileColors( - background = MaterialTheme.colorScheme.primary, - label = MaterialTheme.colorScheme.onPrimary, - secondaryLabel = MaterialTheme.colorScheme.onPrimary, + background = MaterialTheme.colorScheme.surfaceVariant, + iconBackground = MaterialTheme.colorScheme.primary, + label = MaterialTheme.colorScheme.onSurfaceVariant, + secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant, icon = MaterialTheme.colorScheme.onPrimary, ) @@ -462,6 +512,7 @@ private object TileDefaults { fun inactiveTileColors(): TileColors = TileColors( background = MaterialTheme.colorScheme.surfaceVariant, + iconBackground = MaterialTheme.colorScheme.surfaceVariant, label = MaterialTheme.colorScheme.onSurfaceVariant, secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant, icon = MaterialTheme.colorScheme.onSurfaceVariant, @@ -471,6 +522,7 @@ private object TileDefaults { fun unavailableTileColors(): TileColors = TileColors( background = MaterialTheme.colorScheme.surface, + iconBackground = MaterialTheme.colorScheme.surface, label = MaterialTheme.colorScheme.onSurface, secondaryLabel = MaterialTheme.colorScheme.onSurface, icon = MaterialTheme.colorScheme.onSurface, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt index a6cfa75a7583..7505b90ee844 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt @@ -48,6 +48,10 @@ class TileViewModel(private val tile: QSTile, val spec: TileSpec) { tile.longClick(expandable) } + fun onSecondaryClick(expandable: Expandable?) { + tile.secondaryClick(expandable) + } + fun startListening(token: Any) = tile.setListening(token, true) fun stopListening(token: Any) = tile.setListening(token, false) |