diff options
6 files changed, 127 insertions, 51 deletions
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 6ff1240c5e60..3702710f4f24 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2593,6 +2593,9 @@ <!-- Accessibility description indicating the currently selected tile's position. Only used for tiles that are currently in use [CHAR LIMIT=NONE] --> <string name="accessibility_qs_edit_position">Position <xliff:g id="position" example="5">%1$d</xliff:g></string> + <!-- Accessibility description indicating the currently selected tile is already added [CHAR LIMIT=NONE] --> + <string name="accessibility_qs_edit_tile_already_added">Tile already added</string> + <!-- Accessibility announcement after a tile has been added [CHAR LIMIT=NONE] --> <string name="accessibility_qs_edit_tile_added">Tile added</string> diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt index ddadb8879f07..6e6c0b61d85b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt @@ -22,6 +22,7 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -78,6 +79,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -140,6 +142,7 @@ import com.android.systemui.qs.panels.ui.compose.selection.TileState import com.android.systemui.qs.panels.ui.compose.selection.rememberResizingState import com.android.systemui.qs.panels.ui.compose.selection.rememberSelectionState import com.android.systemui.qs.panels.ui.compose.selection.selectableTile +import com.android.systemui.qs.panels.ui.model.AvailableTileGridCell import com.android.systemui.qs.panels.ui.model.GridCell import com.android.systemui.qs.panels.ui.model.SpacerGridCell import com.android.systemui.qs.panels.ui.model.TileGridCell @@ -290,8 +293,19 @@ fun DefaultEditTileGrid( Text(text = stringResource(id = R.string.drag_to_add_tiles)) } + val availableTiles = remember { + mutableStateListOf<AvailableTileGridCell>().apply { + addAll(toAvailableTiles(listState.tiles, otherTiles)) + } + } + LaunchedEffect(listState.tiles, otherTiles) { + availableTiles.apply { + clear() + addAll(toAvailableTiles(listState.tiles, otherTiles)) + } + } AvailableTileGrid( - otherTiles, + availableTiles, selectionState, columns, onAddTile, @@ -444,7 +458,7 @@ private fun CurrentTilesGrid( @Composable private fun AvailableTileGrid( - tiles: List<SizedTile<EditTileViewModel>>, + tiles: List<AvailableTileGridCell>, selectionState: MutableSelectionState, columns: Int, onAddTile: (TileSpec) -> Unit, @@ -453,7 +467,7 @@ private fun AvailableTileGrid( // Available tiles aren't visible during drag and drop, so the row/col isn't needed val groupedTiles = remember(tiles.fastMap { it.tile.category }, tiles.fastMap { it.tile.label }) { - groupAndSort(tiles.fastMap { TileGridCell(it, 0, 0) }) + groupAndSort(tiles) } val labelColors = EditModeTileDefaults.editTileColors() @@ -478,11 +492,10 @@ private fun AvailableTileGrid( horizontalArrangement = spacedBy(TileArrangementPadding), modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Max), ) { - row.forEachIndexed { index, tileGridCell -> - key(tileGridCell.tile.tileSpec) { + row.forEach { tileGridCell -> + key(tileGridCell.key) { AvailableTileGridCell( cell = tileGridCell, - index = index, dragAndDropState = dragAndDropState, selectionState = selectionState, onAddTile = onAddTile, @@ -505,10 +518,7 @@ fun gridHeight(rows: Int, tileHeight: Dp, tilePadding: Dp, gridPadding: Dp): Dp } private fun GridCell.key(index: Int): Any { - return when (this) { - is TileGridCell -> key - is SpacerGridCell -> index - } + return if (this is TileGridCell) key else index } /** @@ -687,41 +697,44 @@ private fun TileGridCell( @Composable private fun AvailableTileGridCell( - cell: TileGridCell, - index: Int, + cell: AvailableTileGridCell, dragAndDropState: DragAndDropState, selectionState: MutableSelectionState, onAddTile: (TileSpec) -> Unit, modifier: Modifier = Modifier, ) { - val onClickActionName = stringResource(id = R.string.accessibility_qs_edit_tile_add_action) - val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1) + val stateDescription: String? = + if (cell.isAvailable) null + else stringResource(R.string.accessibility_qs_edit_tile_already_added) + + val alpha by animateFloatAsState(if (cell.isAvailable) 1f else .38f) val colors = EditModeTileDefaults.editTileColors() - val onClick = { - onAddTile(cell.tile.tileSpec) - selectionState.select(cell.tile.tileSpec) - } // Displays the tile as an icon tile with the label underneath Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = spacedBy(CommonTileDefaults.TilePadding, Alignment.Top), - modifier = modifier, + modifier = + modifier + .graphicsLayer { this.alpha = alpha } + .semantics(mergeDescendants = true) { + stateDescription?.let { this.stateDescription = it } + }, ) { Box(Modifier.fillMaxWidth().height(TileHeight)) { - Box( - Modifier.fillMaxSize() - .clickable(onClick = onClick, onClickLabel = onClickActionName) - .semantics(mergeDescendants = true) { this.stateDescription = stateDescription } - .dragAndDropTileSource( + val draggableModifier = + if (cell.isAvailable) { + Modifier.dragAndDropTileSource( SizedTileImpl(cell.tile, cell.width), dragAndDropState, DragType.Add, ) { selectionState.unSelect() } - .tileBackground(colors.background) - ) { + } else { + Modifier + } + Box(draggableModifier.fillMaxSize().tileBackground(colors.background)) { // Icon SmallTileContent( iconProvider = { cell.tile.icon }, @@ -733,9 +746,13 @@ private fun AvailableTileGridCell( StaticTileBadge( icon = Icons.Default.Add, - contentDescription = onClickActionName, - onClick = onClick, - ) + contentDescription = + stringResource(id = R.string.accessibility_qs_edit_tile_add_action), + enabled = cell.isAvailable, + ) { + onAddTile(cell.tile.tileSpec) + selectionState.select(cell.tile.tileSpec) + } } Box(Modifier.fillMaxSize()) { Text( @@ -819,6 +836,15 @@ fun EditTile( } } +private fun toAvailableTiles( + currentTiles: List<GridCell>, + otherTiles: List<SizedTile<EditTileViewModel>>, +): List<AvailableTileGridCell> { + return currentTiles.filterIsInstance<TileGridCell>().fastMap { + AvailableTileGridCell(it.tile, isAvailable = false) + } + otherTiles.fastMap { AvailableTileGridCell(it.tile) } +} + private fun MeasureScope.iconHorizontalCenter(containerSize: Int): Float { return (containerSize - ToggleTargetSize.roundToPx()) / 2f - CommonTileDefaults.TilePadding.toPx() diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt index 699e5f6b77e9..153238fc91c9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt @@ -19,6 +19,7 @@ package com.android.systemui.qs.panels.ui.compose.selection import androidx.compose.animation.animateColor import androidx.compose.animation.core.Transition import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateOffset import androidx.compose.animation.core.animateSize import androidx.compose.animation.core.updateTransition @@ -61,6 +62,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toSize import androidx.compose.ui.zIndex import com.android.compose.modifiers.size +import com.android.compose.modifiers.thenIf import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BADGE_ANGLE_RAD import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeSize @@ -184,18 +186,37 @@ private fun Modifier.selectionBorder( } } +/** + * Draws a clickable badge in the top end corner of the parent composable. + * + * The badge will fade in and fade out based on whether or not it's enabled. + * + * @param icon the [ImageVector] to display in the badge + * @param contentDescription the content description for the icon + * @param enabled Whether the badge should be visible and clickable + * @param onClick the callback when the badge is clicked + */ @Composable -fun StaticTileBadge(icon: ImageVector, contentDescription: String?, onClick: () -> Unit) { +fun StaticTileBadge( + icon: ImageVector, + contentDescription: String?, + enabled: Boolean, + onClick: () -> Unit, +) { val offset = with(LocalDensity.current) { Offset(BadgeXOffset.toPx(), BadgeYOffset.toPx()) } + val alpha by animateFloatAsState(if (enabled) 1f else 0f) MinimumInteractiveSizeComponent(angle = { BADGE_ANGLE_RAD }, offset = { offset }) { Box( Modifier.fillMaxSize() - .clickable( - interactionSource = null, - indication = null, - onClickLabel = contentDescription, - onClick = onClick, - ) + .graphicsLayer { this.alpha = alpha } + .thenIf(enabled) { + Modifier.clickable( + interactionSource = null, + indication = null, + onClickLabel = contentDescription, + onClick = onClick, + ) + } ) { val secondaryColor = MaterialTheme.colorScheme.secondary Icon( @@ -214,7 +235,8 @@ fun StaticTileBadge(icon: ImageVector, contentDescription: String?, onClick: () private fun MinimumInteractiveSizeComponent( angle: () -> Float, offset: () -> Offset, - content: @Composable BoxScope.() -> Unit, + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit = {}, ) { // Use a higher zIndex than the tile to draw over it, and manually create the touch target // as we're drawing over neighbor tiles as well. @@ -222,7 +244,8 @@ private fun MinimumInteractiveSizeComponent( Box( contentAlignment = Alignment.Center, modifier = - Modifier.zIndex(2f) + modifier + .zIndex(2f) .systemGestureExclusion { Rect(Offset.Zero, it.size.toSize()) } .layout { measurable, constraints -> val size = minTouchTargetSize.roundToPx() diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt index c0441f8a38a1..78fd8c0168dd 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt @@ -21,13 +21,13 @@ import androidx.compose.runtime.Immutable import com.android.systemui.qs.panels.shared.model.SizedTile import com.android.systemui.qs.panels.shared.model.splitInRowsSequence import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel +import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.shared.model.CategoryAndName /** Represents an item from a grid associated with a row and a span */ sealed interface GridCell { val row: Int val span: GridItemSpan - val s: String } /** @@ -40,7 +40,6 @@ data class TileGridCell( override val row: Int, override val width: Int, override val span: GridItemSpan = GridItemSpan(width), - override val s: String = "${tile.tileSpec.spec}-$row-$width", val column: Int, ) : GridCell, SizedTile<EditTileViewModel>, CategoryAndName by tile { val key: String = "${tile.tileSpec.spec}-$row" @@ -52,12 +51,23 @@ data class TileGridCell( ) : this(tile = sizedTile.tile, row = row, column = column, width = sizedTile.width) } +/** + * Represents a [EditTileViewModel] from the edit mode available tiles grid and whether it is + * available to add or not. + */ +@Immutable +data class AvailableTileGridCell( + override val tile: EditTileViewModel, + override val width: Int = 1, + val isAvailable: Boolean = true, + val key: TileSpec = tile.tileSpec, +) : SizedTile<EditTileViewModel>, CategoryAndName by tile + /** Represents an empty space used to fill incomplete rows. Will always display as a 1x1 tile */ @Immutable data class SpacerGridCell( override val row: Int, override val span: GridItemSpan = GridItemSpan(1), - override val s: String = "spacer", ) : GridCell /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt index 8c09b81744d7..e76be82c9340 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt @@ -25,6 +25,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick @@ -80,7 +82,9 @@ class EditModeTest : SysuiTestCase() { composeRule.assertCurrentTilesGridContainsExactly( listOf("tileA", "tileB", "tileC", "tileD_large", "tileE", "tileF") ) - composeRule.assertAvailableTilesGridContainsExactly(listOf("tileG_large")) + composeRule.assertAvailableTilesGridContainsExactly( + TestEditTiles.map { it.tile.tileSpec.spec } + ) } @Test @@ -88,7 +92,8 @@ class EditModeTest : SysuiTestCase() { composeRule.setContent { EditTileGridUnderTest() } composeRule.waitForIdle() - composeRule.onNodeWithContentDescription("tileA").performClick() // Selects + // Selects first "tileA", i.e. the one in the current grid + composeRule.onAllNodesWithText("tileA").onFirst().performClick() composeRule.onNodeWithText("Remove").performClick() // Removes composeRule.waitForIdle() @@ -96,7 +101,9 @@ class EditModeTest : SysuiTestCase() { composeRule.assertCurrentTilesGridContainsExactly( listOf("tileB", "tileC", "tileD_large", "tileE") ) - composeRule.assertAvailableTilesGridContainsExactly(listOf("tileA", "tileF", "tileG_large")) + composeRule.assertAvailableTilesGridContainsExactly( + TestEditTiles.map { it.tile.tileSpec.spec } + ) } private fun ComposeContentTestRule.assertCurrentTilesGridContainsExactly(specs: List<String>) = diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt index bd4c5f50eee7..021be3235ee1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt @@ -25,7 +25,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.click import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performCustomAccessibilityActionWithLabel import androidx.compose.ui.test.performTouchInput @@ -85,7 +86,8 @@ class ResizingTest : SysuiTestCase() { composeRule.waitForIdle() composeRule - .onNodeWithContentDescription("tileA") + .onAllNodesWithText("tileA") + .onFirst() .performCustomAccessibilityActionWithLabel( context.getString(R.string.accessibility_qs_edit_toggle_tile_size_action) ) @@ -103,7 +105,8 @@ class ResizingTest : SysuiTestCase() { composeRule.waitForIdle() composeRule - .onNodeWithContentDescription("tileD_large") + .onAllNodesWithText("tileD_large") + .onFirst() .performCustomAccessibilityActionWithLabel( context.getString(R.string.accessibility_qs_edit_toggle_tile_size_action) ) @@ -121,7 +124,8 @@ class ResizingTest : SysuiTestCase() { composeRule.waitForIdle() composeRule - .onNodeWithContentDescription("tileA") + .onAllNodesWithText("tileA") + .onFirst() .performClick() // Select .performTouchInput { // Tap on resizing handle click(centerRight) @@ -141,7 +145,8 @@ class ResizingTest : SysuiTestCase() { composeRule.waitForIdle() composeRule - .onNodeWithContentDescription("tileD_large") + .onAllNodesWithText("tileD_large") + .onFirst() .performClick() // Select .performTouchInput { // Tap on resizing handle click(centerRight) @@ -161,7 +166,8 @@ class ResizingTest : SysuiTestCase() { composeRule.waitForIdle() composeRule - .onNodeWithContentDescription("tileA") + .onAllNodesWithText("tileA") + .onFirst() .performClick() // Select .performTouchInput { // Resize up swipeRight(startX = right, endX = right * 2) @@ -181,7 +187,8 @@ class ResizingTest : SysuiTestCase() { composeRule.waitForIdle() composeRule - .onNodeWithContentDescription("tileD_large") + .onAllNodesWithText("tileD_large") + .onFirst() .performClick() // Select .performTouchInput { // Resize down swipeLeft() |