Loading packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt +34 −6 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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)) ) { Loading @@ -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)) ) { Loading @@ -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) } } packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt +204 −152 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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, Loading @@ -110,44 +109,141 @@ fun Tile( .collectAsStateWithLifecycle(tile.currentState.toUiState()) val colors = TileDefaults.getColorForState(state.state) val context = LocalContext.current TileContainer( colors = colors, showLabels = showLabels, label = state.label.toString(), iconOnly = iconOnly, onClick = tile::onClick, onLongClick = tile::onLongClick, modifier = modifier, ) { 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, 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 = colors.background, shape = RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius)), color = backgroundColor, shape = TileDefaults.TileShape, modifier = Modifier.height(dimensionResource(id = R.dimen.qs_tile_height)) .clip(TileDefaults.TileShape) ) { Row( Box( modifier = modifier Modifier.fillMaxSize() .combinedClickable( onClick = { tile.onClick(it) }, onLongClick = { tile.onLongClick(it) } enabled = clickEnabled, onClick = { onClick(it) }, onLongClick = { onLongClick(it) } ) .tileModifier(colors), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = tileHorizontalArrangement(iconOnly), .tilePadding(), ) { 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) content() } } if (showLabels && iconOnly) { Text( label, maxLines = 2, color = colors.label, overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, ) } TileContent( label = state.label.toString(), secondaryLabel = state.secondaryLabel.toString(), } } @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, colors = colors, iconOnly = iconOnly, showLabels = showLabels, 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( Loading Loading @@ -247,41 +343,19 @@ fun LazyGridScope.editTiles( "" } Box( 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)) } } } clickEnabled = canClick, onClick = { onClick.invoke(viewModel.tileSpec) }, modifier = Modifier.height(tileHeight).animateItem().semantics { onClick(onClickActionName) { false } this.stateDescription = stateDescription } @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, ) } } Loading @@ -291,41 +365,69 @@ 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( if (iconOnly) { TileIcon( icon = tileViewModel.icon, color = colors.icon, modifier = Modifier.align(Alignment.Center) ) } else { LargeTileContent( label = label, secondaryLabel = tileViewModel.appName?.load(), colors = colors, icon = tileViewModel.icon, iconOnly = iconOnly, showLabels = showLabels, animateIconToEnd = true, colors = colors, clickEnabled = clickEnabled, onClick = { onClick() }, onLongClick = { onClick() }, ) } } } enum class ClickAction { ADD, 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) { Loading @@ -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) Loading @@ -357,78 +459,23 @@ 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 ) } @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(), alignment = Alignment.Start ) } } } } @Composable fun tileHeight(iconWithLabel: Boolean = false): Dp { Loading @@ -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, ) Loading @@ -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, Loading @@ -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, Loading packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt +4 −0 Original line number Diff line number Diff line Loading @@ -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) Loading Loading
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt +34 −6 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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)) ) { Loading @@ -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)) ) { Loading @@ -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) } }
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt +204 −152 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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, Loading @@ -110,44 +109,141 @@ fun Tile( .collectAsStateWithLifecycle(tile.currentState.toUiState()) val colors = TileDefaults.getColorForState(state.state) val context = LocalContext.current TileContainer( colors = colors, showLabels = showLabels, label = state.label.toString(), iconOnly = iconOnly, onClick = tile::onClick, onLongClick = tile::onLongClick, modifier = modifier, ) { 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, 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 = colors.background, shape = RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius)), color = backgroundColor, shape = TileDefaults.TileShape, modifier = Modifier.height(dimensionResource(id = R.dimen.qs_tile_height)) .clip(TileDefaults.TileShape) ) { Row( Box( modifier = modifier Modifier.fillMaxSize() .combinedClickable( onClick = { tile.onClick(it) }, onLongClick = { tile.onLongClick(it) } enabled = clickEnabled, onClick = { onClick(it) }, onLongClick = { onLongClick(it) } ) .tileModifier(colors), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = tileHorizontalArrangement(iconOnly), .tilePadding(), ) { 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) content() } } if (showLabels && iconOnly) { Text( label, maxLines = 2, color = colors.label, overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, ) } TileContent( label = state.label.toString(), secondaryLabel = state.secondaryLabel.toString(), } } @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, colors = colors, iconOnly = iconOnly, showLabels = showLabels, 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( Loading Loading @@ -247,41 +343,19 @@ fun LazyGridScope.editTiles( "" } Box( 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)) } } } clickEnabled = canClick, onClick = { onClick.invoke(viewModel.tileSpec) }, modifier = Modifier.height(tileHeight).animateItem().semantics { onClick(onClickActionName) { false } this.stateDescription = stateDescription } @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, ) } } Loading @@ -291,41 +365,69 @@ 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( if (iconOnly) { TileIcon( icon = tileViewModel.icon, color = colors.icon, modifier = Modifier.align(Alignment.Center) ) } else { LargeTileContent( label = label, secondaryLabel = tileViewModel.appName?.load(), colors = colors, icon = tileViewModel.icon, iconOnly = iconOnly, showLabels = showLabels, animateIconToEnd = true, colors = colors, clickEnabled = clickEnabled, onClick = { onClick() }, onLongClick = { onClick() }, ) } } } enum class ClickAction { ADD, 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) { Loading @@ -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) Loading @@ -357,78 +459,23 @@ 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 ) } @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(), alignment = Alignment.Start ) } } } } @Composable fun tileHeight(iconWithLabel: Boolean = false): Dp { Loading @@ -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, ) Loading @@ -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, Loading @@ -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, Loading
packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt +4 −0 Original line number Diff line number Diff line Loading @@ -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) Loading