diff options
13 files changed, 289 insertions, 153 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt index e58cf152be93..79a303db079a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt @@ -85,12 +85,12 @@ class IconTilesInteractorTest : SysuiTestCase() { runCurrent() // Assert that the tile is removed from the large tiles after resizing - underTest.resize(largeTile) + underTest.resize(largeTile, toIcon = true) runCurrent() assertThat(latest).doesNotContain(largeTile) // Assert that the tile is added to the large tiles after resizing - underTest.resize(largeTile) + underTest.resize(largeTile, toIcon = false) runCurrent() assertThat(latest).contains(largeTile) } @@ -122,7 +122,7 @@ class IconTilesInteractorTest : SysuiTestCase() { val newTile = TileSpec.create("newTile") // Remove the large tile from the current tiles - underTest.resize(newTile) + underTest.resize(newTile, toIcon = false) runCurrent() // Assert that it's still small diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt index 484a8ff973c1..3910903af4aa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt @@ -24,7 +24,6 @@ import com.android.systemui.common.shared.model.Icon import com.android.systemui.qs.panels.shared.model.SizedTile import com.android.systemui.qs.panels.shared.model.SizedTileImpl 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 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.pipeline.shared.TileSpec @@ -39,13 +38,6 @@ class EditTileListStateTest : SysuiTestCase() { private val underTest = EditTileListState(TestEditTiles, 4) @Test - fun noDrag_listUnchanged() { - underTest.tiles.forEach { assertThat(it).isNotInstanceOf(SpacerGridCell::class.java) } - assertThat(underTest.tiles.map { (it as TileGridCell).tile.tileSpec }) - .containsExactly(*TestEditTiles.map { it.tile.tileSpec }.toTypedArray()) - } - - @Test fun startDrag_listHasSpacers() { underTest.onStarted(TestEditTiles[0]) @@ -109,16 +101,6 @@ class EditTileListStateTest : SysuiTestCase() { } @Test - fun droppedNewTile_spacersDisappear() { - underTest.onStarted(TestEditTiles[0]) - underTest.onDrop() - - assertThat(underTest.tiles.toStrings()).isEqualTo(listOf("a", "b", "c", "d", "e")) - assertThat(underTest.isMoving(TestEditTiles[0].tile.tileSpec)).isFalse() - assertThat(underTest.dragInProgress).isFalse() - } - - @Test fun movedTileOutOfBounds_tileDisappears() { underTest.onStarted(TestEditTiles[0]) underTest.movedOutOfBounds() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt index fa72d740b2f0..4acf3ee7878b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt @@ -27,23 +27,25 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class MutableSelectionStateTest : SysuiTestCase() { - private val underTest = MutableSelectionState() + private val underTest = MutableSelectionState({}, {}) @Test fun selectTile_isCorrectlySelected() { - assertThat(underTest.isSelected(TEST_SPEC)).isFalse() + assertThat(underTest.selection?.tileSpec).isNotEqualTo(TEST_SPEC) - underTest.select(TEST_SPEC) - assertThat(underTest.isSelected(TEST_SPEC)).isTrue() + underTest.select(TEST_SPEC, manual = true) + assertThat(underTest.selection?.tileSpec).isEqualTo(TEST_SPEC) + assertThat(underTest.selection?.manual).isTrue() underTest.unSelect() - assertThat(underTest.isSelected(TEST_SPEC)).isFalse() + assertThat(underTest.selection).isNull() val newSpec = TileSpec.create("newSpec") - underTest.select(TEST_SPEC) - underTest.select(newSpec) - assertThat(underTest.isSelected(TEST_SPEC)).isFalse() - assertThat(underTest.isSelected(newSpec)).isTrue() + underTest.select(TEST_SPEC, manual = true) + underTest.select(newSpec, manual = false) + assertThat(underTest.selection?.tileSpec).isNotEqualTo(TEST_SPEC) + assertThat(underTest.selection?.tileSpec).isEqualTo(newSpec) + assertThat(underTest.selection?.manual).isFalse() } @Test @@ -51,12 +53,12 @@ class MutableSelectionStateTest : SysuiTestCase() { assertThat(underTest.resizingState).isNull() // Resizing starts but no tile is selected - underTest.onResizingDragStart(TileWidths(0, 0, 1)) {} + underTest.onResizingDragStart(TileWidths(0, 0, 1)) assertThat(underTest.resizingState).isNull() // Resizing starts with a selected tile - underTest.select(TEST_SPEC) - underTest.onResizingDragStart(TileWidths(0, 0, 1)) {} + underTest.select(TEST_SPEC, manual = true) + underTest.onResizingDragStart(TileWidths(0, 0, 1)) assertThat(underTest.resizingState).isNotNull() } @@ -66,8 +68,8 @@ class MutableSelectionStateTest : SysuiTestCase() { val spec = TileSpec.create("testSpec") // Resizing starts with a selected tile - underTest.select(spec) - underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) {} + underTest.select(spec, manual = true) + underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) assertThat(underTest.resizingState).isNotNull() underTest.onResizingDragEnd() @@ -77,8 +79,8 @@ class MutableSelectionStateTest : SysuiTestCase() { @Test fun unselect_clearsResizingState() { // Resizing starts with a selected tile - underTest.select(TEST_SPEC) - underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) {} + underTest.select(TEST_SPEC, manual = true) + underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) assertThat(underTest.resizingState).isNotNull() underTest.unSelect() @@ -88,8 +90,8 @@ class MutableSelectionStateTest : SysuiTestCase() { @Test fun onResizingDrag_updatesResizingState() { // Resizing starts with a selected tile - underTest.select(TEST_SPEC) - underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) {} + underTest.select(TEST_SPEC, manual = true) + underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) assertThat(underTest.resizingState).isNotNull() underTest.onResizingDrag(5f) @@ -105,11 +107,15 @@ class MutableSelectionStateTest : SysuiTestCase() { @Test fun onResizingDrag_receivesResizeCallback() { var resized = false - val onResize: () -> Unit = { resized = !resized } + val onResize: (TileSpec) -> Unit = { + assertThat(it).isEqualTo(TEST_SPEC) + resized = !resized + } + val underTest = MutableSelectionState(onResize = onResize, {}) // Resizing starts with a selected tile - underTest.select(TEST_SPEC) - underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10), onResize) + underTest.select(TEST_SPEC, true) + underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) assertThat(underTest.resizingState).isNotNull() // Drag under the threshold @@ -125,6 +131,37 @@ class MutableSelectionStateTest : SysuiTestCase() { assertThat(resized).isFalse() } + @Test + fun onResizingEnded_receivesResizeEndCallback() { + var resizeEnded = false + val onResizeEnd: (TileSpec) -> Unit = { resizeEnded = true } + val underTest = MutableSelectionState({}, onResizeEnd = onResizeEnd) + + // Resizing starts with a selected tile + underTest.select(TEST_SPEC, true) + underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) + + underTest.onResizingDragEnd() + assertThat(resizeEnded).isTrue() + } + + @Test + fun onResizingEnded_setsSelectionAutomatically() { + val underTest = MutableSelectionState({}, {}) + + // Resizing starts with a selected tile + underTest.select(TEST_SPEC, manual = true) + underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) + + // Assert the selection was manual + assertThat(underTest.selection?.manual).isTrue() + + underTest.onResizingDragEnd() + + // Assert the selection is no longer manual due to the resizing + assertThat(underTest.selection?.manual).isFalse() + } + companion object { private val TEST_SPEC = TileSpec.create("testSpec") } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt index 02a607db0a64..fc59a50e88ad 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt @@ -40,7 +40,7 @@ constructor( private val currentTilesInteractor: CurrentTilesInteractor, private val preferencesInteractor: QSPreferencesInteractor, @PanelsLog private val logBuffer: LogBuffer, - @Application private val applicationScope: CoroutineScope + @Application private val applicationScope: CoroutineScope, ) { val largeTilesSpecs = @@ -64,14 +64,15 @@ constructor( fun isIconTile(spec: TileSpec): Boolean = !largeTilesSpecs.value.contains(spec) - fun resize(spec: TileSpec) { + fun resize(spec: TileSpec, toIcon: Boolean) { if (!isCurrent(spec)) { return } - if (largeTilesSpecs.value.contains(spec)) { + val isIcon = !largeTilesSpecs.value.contains(spec) + if (toIcon && !isIcon) { preferencesInteractor.setLargeTilesSpecs(largeTilesSpecs.value - spec) - } else { + } else if (!toIcon && isIcon) { preferencesInteractor.setLargeTilesSpecs(largeTilesSpecs.value + spec) } } @@ -85,7 +86,7 @@ constructor( LOG_BUFFER_LARGE_TILES_SPECS_CHANGE_TAG, LogLevel.DEBUG, { str1 = specs.toString() }, - { "Large tiles change: $str1" } + { "Large tiles change: $str1" }, ) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt index a4f977b08b70..770fd785723a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt @@ -60,10 +60,37 @@ class EditTileListState(tiles: List<SizedTile<EditTileViewModel>>, private val c return _tiles.filterIsInstance<TileGridCell>().map { it.tile.tileSpec } } - fun indexOf(tileSpec: TileSpec): Int { + private fun indexOf(tileSpec: TileSpec): Int { return _tiles.indexOfFirst { it is TileGridCell && it.tile.tileSpec == tileSpec } } + /** + * Whether the tile with this [TileSpec] is currently an icon in the [EditTileListState] + * + * @return true if the tile is an icon, false if it's large, null if the tile isn't in the list + */ + fun isIcon(tileSpec: TileSpec): Boolean? { + val index = indexOf(tileSpec) + return if (index != -1) { + val cell = _tiles[index] + cell as TileGridCell + return cell.isIcon + } else { + null + } + } + + /** Toggle the size of the tile corresponding to the [TileSpec] */ + fun toggleSize(tileSpec: TileSpec) { + val fromIndex = indexOf(tileSpec) + if (fromIndex != -1) { + val cell = _tiles.removeAt(fromIndex) + cell as TileGridCell + _tiles.add(fromIndex, cell.copy(width = if (cell.isIcon) 2 else 1)) + regenerateGrid(fromIndex) + } + } + override fun isMoving(tileSpec: TileSpec): Boolean { return _draggedCell.value?.let { it.tile.tileSpec == tileSpec } ?: false } @@ -71,8 +98,8 @@ class EditTileListState(tiles: List<SizedTile<EditTileViewModel>>, private val c override fun onStarted(cell: SizedTile<EditTileViewModel>) { _draggedCell.value = cell - // Add visible spacers to the grid to indicate where the user can move a tile - regenerateGrid(includeSpacers = true) + // Add spacers to the grid to indicate where the user can move a tile + regenerateGrid() } override fun onMoved(target: Int, insertAfter: Boolean) { @@ -86,7 +113,7 @@ class EditTileListState(tiles: List<SizedTile<EditTileViewModel>>, private val c val insertionIndex = if (insertAfter) target + 1 else target if (fromIndex != -1) { val cell = _tiles.removeAt(fromIndex) - regenerateGrid(includeSpacers = true) + regenerateGrid() _tiles.add(insertionIndex.coerceIn(0, _tiles.size), cell) } else { // Add the tile with a temporary row which will get reassigned when @@ -94,7 +121,7 @@ class EditTileListState(tiles: List<SizedTile<EditTileViewModel>>, private val c _tiles.add(insertionIndex.coerceIn(0, _tiles.size), TileGridCell(draggedTile, 0)) } - regenerateGrid(includeSpacers = true) + regenerateGrid() } override fun movedOutOfBounds() { @@ -109,12 +136,27 @@ class EditTileListState(tiles: List<SizedTile<EditTileViewModel>>, private val c _draggedCell.value = null // Remove the spacers - regenerateGrid(includeSpacers = false) + regenerateGrid() + } + + /** Regenerate the list of [GridCell] with their new potential rows */ + private fun regenerateGrid() { + _tiles.filterIsInstance<TileGridCell>().toGridCells(columns).let { + _tiles.clear() + _tiles.addAll(it) + } } - private fun regenerateGrid(includeSpacers: Boolean) { - _tiles.filterIsInstance<TileGridCell>().toGridCells(columns, includeSpacers).let { + /** + * Regenerate the list of [GridCell] with their new potential rows from [fromIndex], leaving + * cells before that untouched. + */ + private fun regenerateGrid(fromIndex: Int) { + val fromRow = _tiles[fromIndex].row + val (pre, post) = _tiles.partition { it.row < fromRow } + post.filterIsInstance<TileGridCell>().toGridCells(columns, startingRow = fromRow).let { _tiles.clear() + _tiles.addAll(pre) _tiles.addAll(it) } } 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 0e76e18fab8e..30bafaece923 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 @@ -132,15 +132,23 @@ object TileType @Composable fun DefaultEditTileGrid( - currentListState: EditTileListState, + listState: EditTileListState, otherTiles: List<SizedTile<EditTileViewModel>>, columns: Int, modifier: Modifier, onRemoveTile: (TileSpec) -> Unit, onSetTiles: (List<TileSpec>) -> Unit, - onResize: (TileSpec) -> Unit, + onResize: (TileSpec, toIcon: Boolean) -> Unit, ) { - val selectionState = rememberSelectionState() + val currentListState by rememberUpdatedState(listState) + val selectionState = + rememberSelectionState( + onResize = { currentListState.toggleSize(it) }, + onResizeEnd = { spec -> + // Commit the size currently in the list + currentListState.isIcon(spec)?.let { onResize(spec, it) } + }, + ) CompositionLocalProvider(LocalOverscrollConfiguration provides null) { Column( @@ -149,11 +157,11 @@ fun DefaultEditTileGrid( modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()), ) { AnimatedContent( - targetState = currentListState.dragInProgress, + targetState = listState.dragInProgress, modifier = Modifier.wrapContentSize(), label = "", ) { dragIsInProgress -> - EditGridHeader(Modifier.dragAndDropRemoveZone(currentListState, onRemoveTile)) { + EditGridHeader(Modifier.dragAndDropRemoveZone(listState, onRemoveTile)) { if (dragIsInProgress) { RemoveTileTarget() } else { @@ -162,11 +170,11 @@ fun DefaultEditTileGrid( } } - CurrentTilesGrid(currentListState, selectionState, columns, onResize, onSetTiles) + CurrentTilesGrid(listState, selectionState, columns, onResize, onSetTiles) // Hide available tiles when dragging AnimatedVisibility( - visible = !currentListState.dragInProgress, + visible = !listState.dragInProgress, enter = fadeIn(), exit = fadeOut(), ) { @@ -177,7 +185,7 @@ fun DefaultEditTileGrid( ) { EditGridHeader { Text(text = "Hold and drag to add tiles.") } - AvailableTileGrid(otherTiles, selectionState, columns, currentListState) + AvailableTileGrid(otherTiles, selectionState, columns, listState) } } @@ -186,7 +194,7 @@ fun DefaultEditTileGrid( modifier = Modifier.fillMaxWidth() .weight(1f) - .dragAndDropRemoveZone(currentListState, onRemoveTile) + .dragAndDropRemoveZone(listState, onRemoveTile) ) } } @@ -229,7 +237,7 @@ private fun CurrentTilesGrid( listState: EditTileListState, selectionState: MutableSelectionState, columns: Int, - onResize: (TileSpec) -> Unit, + onResize: (TileSpec, toIcon: Boolean) -> Unit, onSetTiles: (List<TileSpec>) -> Unit, ) { val currentListState by rememberUpdatedState(listState) @@ -242,19 +250,6 @@ private fun CurrentTilesGrid( ) val gridState = rememberLazyGridState() var gridContentOffset by remember { mutableStateOf(Offset(0f, 0f)) } - var droppedSpec by remember { mutableStateOf<TileSpec?>(null) } - - // Select the tile that was dropped. A delay is introduced to avoid clipping issues on the - // selected border and resizing handle, as well as letting the selection animation play. - LaunchedEffect(droppedSpec) { - droppedSpec?.let { - delay(200) - selectionState.select(it) - - // Reset droppedSpec in case a tile is dropped twice in a row - droppedSpec = null - } - } TileLazyGrid( state = gridState, @@ -270,14 +265,17 @@ private fun CurrentTilesGrid( ) .dragAndDropTileList(gridState, { gridContentOffset }, listState) { spec -> onSetTiles(currentListState.tileSpecs()) - droppedSpec = spec + selectionState.select(spec, manual = false) } .onGloballyPositioned { coordinates -> gridContentOffset = coordinates.positionInRoot() } .testTag(CURRENT_TILES_GRID_TEST_TAG), ) { - EditTiles(listState.tiles, listState, selectionState, onResize) + EditTiles(listState.tiles, listState, selectionState) { spec -> + // Toggle the current size of the tile + currentListState.isIcon(spec)?.let { onResize(spec, !it) } + } } } @@ -348,11 +346,19 @@ private fun GridCell.key(index: Int, dragAndDropState: DragAndDropState): Any { } } +/** + * Adds a list of [GridCell] to the lazy grid + * + * @param cells the list of [GridCell] + * @param dragAndDropState the [DragAndDropState] for this grid + * @param selectionState the [MutableSelectionState] for this grid + * @param onToggleSize the callback when a tile's size is toggled + */ fun LazyGridScope.EditTiles( cells: List<GridCell>, dragAndDropState: DragAndDropState, selectionState: MutableSelectionState, - onResize: (TileSpec) -> Unit, + onToggleSize: (spec: TileSpec) -> Unit, ) { items( count = cells.size, @@ -378,7 +384,7 @@ fun LazyGridScope.EditTiles( index = index, dragAndDropState = dragAndDropState, selectionState = selectionState, - onResize = onResize, + onToggleSize = onToggleSize, ) } is SpacerGridCell -> SpacerGridCell() @@ -392,16 +398,28 @@ private fun LazyGridItemScope.TileGridCell( index: Int, dragAndDropState: DragAndDropState, selectionState: MutableSelectionState, - onResize: (TileSpec) -> Unit, + onToggleSize: (spec: TileSpec) -> Unit, ) { - val selected = selectionState.isSelected(cell.tile.tileSpec) val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1) + var selected by remember { mutableStateOf(false) } val selectionAlpha by animateFloatAsState( targetValue = if (selected) 1f else 0f, label = "QSEditTileSelectionAlpha", ) + LaunchedEffect(selectionState.selection?.tileSpec) { + selectionState.selection?.let { + // A delay is introduced on automatic selections such as dragged tiles or reflow caused + // by resizing. This avoids clipping issues on the border and resizing handle, as well + // as letting the selection animation play correctly. + if (!it.manual) { + delay(250) + } + } + selected = selectionState.selection?.tileSpec == cell.tile.tileSpec + } + val modifier = Modifier.animateItem() .semantics(mergeDescendants = true) { @@ -411,7 +429,7 @@ private fun LazyGridItemScope.TileGridCell( listOf( // TODO(b/367748260): Add final accessibility actions CustomAccessibilityAction("Toggle size") { - onResize(cell.tile.tileSpec) + onToggleSize(cell.tile.tileSpec) true } ) @@ -438,11 +456,9 @@ private fun LazyGridItemScope.TileGridCell( if (selected) { SelectedTile( - tileSpec = cell.tile.tileSpec, isIcon = cell.isIcon, selectionAlpha = { selectionAlpha }, selectionState = selectionState, - onResize = onResize, modifier = modifier.zIndex(2f), // 2f to display this tile over neighbors when dragged content = content, ) @@ -458,11 +474,9 @@ private fun LazyGridItemScope.TileGridCell( @Composable private fun SelectedTile( - tileSpec: TileSpec, isIcon: Boolean, selectionAlpha: () -> Float, selectionState: MutableSelectionState, - onResize: (TileSpec) -> Unit, modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { @@ -492,9 +506,7 @@ private fun SelectedTile( selectionState = selectionState, transition = selectionAlpha, tileWidths = { tileWidths }, - ) { - onResize(tileSpec) - } + ) } Layout(contents = listOf(content, handle)) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt index 4946c0194d34..542d0ef7f627 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt @@ -94,7 +94,7 @@ constructor( val (currentTiles, otherTiles) = sizedTiles.partition { it.tile.isCurrent } val currentListState = rememberEditListState(currentTiles, columns) DefaultEditTileGrid( - currentListState = currentListState, + listState = currentListState, otherTiles = otherTiles, columns = columns, modifier = modifier, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt index 2ea32e640984..441d96289d86 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt @@ -27,28 +27,39 @@ import com.android.systemui.qs.pipeline.shared.TileSpec /** Creates the state of the current selected tile that is remembered across compositions. */ @Composable -fun rememberSelectionState(): MutableSelectionState { - return remember { MutableSelectionState() } +fun rememberSelectionState( + onResize: (TileSpec) -> Unit, + onResizeEnd: (TileSpec) -> Unit, +): MutableSelectionState { + return remember { MutableSelectionState(onResize, onResizeEnd) } } +/** + * Holds the selected [TileSpec] and whether the selection was manual, i.e. caused by a tap from the + * user. + */ +data class Selection(val tileSpec: TileSpec, val manual: Boolean) + /** Holds the state of the current selection. */ -class MutableSelectionState { - private var _selectedTile = mutableStateOf<TileSpec?>(null) +class MutableSelectionState( + val onResize: (TileSpec) -> Unit, + private val onResizeEnd: (TileSpec) -> Unit, +) { + private var _selection = mutableStateOf<Selection?>(null) private var _resizingState = mutableStateOf<ResizingState?>(null) + /** The [Selection] if a tile is selected, null if not. */ + val selection by _selection + /** The [ResizingState] of the selected tile is currently being resized, null if not. */ val resizingState by _resizingState - fun isSelected(tileSpec: TileSpec): Boolean { - return _selectedTile.value?.let { it == tileSpec } ?: false - } - - fun select(tileSpec: TileSpec) { - _selectedTile.value = tileSpec + fun select(tileSpec: TileSpec, manual: Boolean) { + _selection.value = Selection(tileSpec, manual) } fun unSelect() { - _selectedTile.value = null + _selection.value = null onResizingDragEnd() } @@ -56,14 +67,21 @@ class MutableSelectionState { _resizingState.value?.onDrag(offset) } - fun onResizingDragStart(tileWidths: TileWidths, onResize: () -> Unit) { - if (_selectedTile.value == null) return - - _resizingState.value = ResizingState(tileWidths, onResize) + fun onResizingDragStart(tileWidths: TileWidths) { + _selection.value?.let { + _resizingState.value = ResizingState(tileWidths) { onResize(it.tileSpec) } + } } fun onResizingDragEnd() { _resizingState.value = null + _selection.value?.let { + onResizeEnd(it.tileSpec) + + // Mark the selection as automatic in case the tile ends up moving to a different + // row with its new size. + _selection.value = it.copy(manual = false) + } } } @@ -76,10 +94,10 @@ fun Modifier.selectableTile(tileSpec: TileSpec, selectionState: MutableSelection return pointerInput(Unit) { detectTapGestures( onTap = { - if (selectionState.isSelected(tileSpec)) { + if (selectionState.selection?.tileSpec == tileSpec) { selectionState.unSelect() } else { - selectionState.select(tileSpec) + selectionState.select(tileSpec, manual = true) } } ) 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 e3acf3863254..7c62e5995ce8 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 @@ -45,7 +45,6 @@ fun ResizingHandle( selectionState: MutableSelectionState, transition: () -> Float, tileWidths: () -> TileWidths? = { null }, - onResize: () -> Unit = {}, ) { if (enabled) { // Manually creating the touch target around the resizing dot to ensure that the next tile @@ -56,9 +55,7 @@ fun ResizingHandle( Modifier.size(minTouchTargetSize).pointerInput(Unit) { detectHorizontalDragGestures( onHorizontalDrag = { _, offset -> selectionState.onResizingDrag(offset) }, - onDragStart = { - tileWidths()?.let { selectionState.onResizingDragStart(it, onResize) } - }, + onDragStart = { tileWidths()?.let { selectionState.onResizingDragStart(it) } }, onDragEnd = selectionState::onResizingDragEnd, onDragCancel = selectionState::onResizingDragEnd, ) 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 b16a7075607f..b1841c4c5ffa 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 @@ -27,6 +27,7 @@ import com.android.systemui.qs.shared.model.CategoryAndName sealed interface GridCell { val row: Int val span: GridItemSpan + val s: String } /** @@ -39,6 +40,7 @@ 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", ) : GridCell, SizedTile<EditTileViewModel>, CategoryAndName by tile { val key: String = "${tile.tileSpec.spec}-$row" @@ -53,22 +55,30 @@ data class TileGridCell( data class SpacerGridCell( override val row: Int, override val span: GridItemSpan = GridItemSpan(1), + override val s: String = "spacer", ) : GridCell +/** + * Generates a list of [GridCell] from a list of [SizedTile] + * + * Builds rows based on the tiles' widths, and fill each hole with a [SpacerGridCell] + * + * @param startingRow The row index the grid is built from, used in cases where only end rows need + * to be regenerated + */ fun List<SizedTile<EditTileViewModel>>.toGridCells( columns: Int, - includeSpacers: Boolean = false, + startingRow: Int = 0, ): List<GridCell> { return splitInRowsSequence(this, columns) .flatMapIndexed { rowIndex, sizedTiles -> - val row: List<GridCell> = sizedTiles.map { TileGridCell(it, rowIndex) } + val correctedRowIndex = rowIndex + startingRow + val row: List<GridCell> = sizedTiles.map { TileGridCell(it, correctedRowIndex) } - if (includeSpacers) { - // Fill the incomplete rows with spacers - val numSpacers = columns - sizedTiles.sumOf { it.width } - row.toMutableList().apply { repeat(numSpacers) { add(SpacerGridCell(rowIndex)) } } - } else { - row + // Fill the incomplete rows with spacers + val numSpacers = columns - sizedTiles.sumOf { it.width } + row.toMutableList().apply { + repeat(numSpacers) { add(SpacerGridCell(correctedRowIndex)) } } } .toList() diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt index b604e18b1e76..4e698edf4e34 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt @@ -27,7 +27,7 @@ interface IconTilesViewModel { fun isIconTile(spec: TileSpec): Boolean - fun resize(spec: TileSpec) + fun resize(spec: TileSpec, toIcon: Boolean) } @SysUISingleton @@ -37,5 +37,5 @@ class IconTilesViewModelImpl @Inject constructor(private val interactor: IconTil override fun isIconTile(spec: TileSpec): Boolean = interactor.isIconTile(spec) - override fun resize(spec: TileSpec) = interactor.resize(spec) + override fun resize(spec: TileSpec, toIcon: Boolean) = interactor.resize(spec, toIcon) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt index 6423d25cb88a..8d060e936cd9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt @@ -60,13 +60,13 @@ class DragAndDropTest : SysuiTestCase() { onSetTiles: (List<TileSpec>) -> Unit, ) { DefaultEditTileGrid( - currentListState = listState, + listState = listState, otherTiles = listOf(), columns = 4, modifier = Modifier.fillMaxSize(), onRemoveTile = {}, onSetTiles = onSetTiles, - onResize = {}, + onResize = { _, _ -> }, ) } 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 682ed92cc593..ee1c0e99d6ac 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,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performCustomAccessibilityActionWithLabel +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeLeft +import androidx.compose.ui.test.swipeRight import androidx.compose.ui.text.AnnotatedString import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -43,15 +47,19 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +@OptIn(ExperimentalTestApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class ResizingTest : SysuiTestCase() { @get:Rule val composeRule = createComposeRule() @Composable - private fun EditTileGridUnderTest(listState: EditTileListState, onResize: (TileSpec) -> Unit) { + private fun EditTileGridUnderTest( + listState: EditTileListState, + onResize: (TileSpec, Boolean) -> Unit, + ) { DefaultEditTileGrid( - currentListState = listState, + listState = listState, otherTiles = listOf(), columns = 4, modifier = Modifier.fillMaxSize(), @@ -61,22 +69,12 @@ class ResizingTest : SysuiTestCase() { ) } - @OptIn(ExperimentalTestApi::class) @Test - fun resizedIcon_shouldBeLarge() { + fun toggleIconTile_shouldBeLarge() { var tiles by mutableStateOf(TestEditTiles) val listState = EditTileListState(tiles, 4) composeRule.setContent { - EditTileGridUnderTest(listState) { spec -> - tiles = - tiles.map { - if (it.tile.tileSpec == spec) { - toggleWidth(it) - } else { - it - } - } - } + EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) } } composeRule.waitForIdle() @@ -87,35 +85,74 @@ class ResizingTest : SysuiTestCase() { assertThat(tiles.find { it.tile.tileSpec.spec == "tileA" }?.width).isEqualTo(2) } - @OptIn(ExperimentalTestApi::class) + @Test + fun toggleLargeTile_shouldBeIcon() { + var tiles by mutableStateOf(TestEditTiles) + val listState = EditTileListState(tiles, 4) + composeRule.setContent { + EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) } + } + composeRule.waitForIdle() + + composeRule + .onNodeWithContentDescription("tileD_large") + .performCustomAccessibilityActionWithLabel("Toggle size") + + assertThat(tiles.find { it.tile.tileSpec.spec == "tileD_large" }?.width).isEqualTo(1) + } + @Test fun resizedLarge_shouldBeIcon() { var tiles by mutableStateOf(TestEditTiles) val listState = EditTileListState(tiles, 4) composeRule.setContent { - EditTileGridUnderTest(listState) { spec -> - tiles = - tiles.map { - if (it.tile.tileSpec == spec) { - toggleWidth(it) - } else { - it - } - } + EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) } + } + composeRule.waitForIdle() + + composeRule + .onNodeWithContentDescription("tileA") + .performClick() // Select + .performTouchInput { // Resize down + swipeRight() } + composeRule.waitForIdle() + + assertThat(tiles.find { it.tile.tileSpec.spec == "tileA" }?.width).isEqualTo(1) + } + + @Test + fun resizedIcon_shouldBeLarge() { + var tiles by mutableStateOf(TestEditTiles) + val listState = EditTileListState(tiles, 4) + composeRule.setContent { + EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) } } composeRule.waitForIdle() composeRule .onNodeWithContentDescription("tileD_large") - .performCustomAccessibilityActionWithLabel("Toggle size") + .performClick() // Select + .performTouchInput { // Resize down + swipeLeft() + } + composeRule.waitForIdle() assertThat(tiles.find { it.tile.tileSpec.spec == "tileD_large" }?.width).isEqualTo(1) } companion object { - private fun toggleWidth(tile: SizedTile<EditTileViewModel>): SizedTile<EditTileViewModel> { - return SizedTileImpl(tile.tile, width = if (tile.isIcon) 2 else 1) + private fun List<SizedTile<EditTileViewModel>>.resize( + spec: TileSpec, + toIcon: Boolean, + ): List<SizedTile<EditTileViewModel>> { + return map { + if (it.tile.tileSpec == spec) { + SizedTileImpl(it.tile, width = if (toIcon) 1 else 2) + } else { + it + } + } } private fun createEditTile(tileSpec: String): SizedTile<EditTileViewModel> { |