Merge "Implement UI for dual target tiles" into main
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 e1b21ef..9233e76 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.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 @@
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 @@
.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 @@
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 e4fbb4b..bbb98d3 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.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.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.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.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.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 @@
.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 @@
""
}
- 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 a6cfa75..7505b90 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 @@
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)