Stop the tile grid from automatically reflowing during a resizing movement.
This avoids the situation of a large tile moving up a row while being resized. The reflow will happen at the end of the resizing movement.
Flag: com.android.systemui.qs_ui_refactor_compose_fragment
Test: MutableSelectionStateTest
Test: ResizingTest
Bug: 350984160
Change-Id: I0108663c7f3e4b13b215a2f9afb264d66ccda922
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 e58cf15..79a303d 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 @@
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 @@
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 484a8ff..3910903 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.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 @@
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 @@
}
@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 fa72d74..4acf3ee 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 @@
@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 @@
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 @@
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 @@
@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 @@
@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 @@
@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 @@
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 02a607d..fc59a50 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 @@
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 @@
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 @@
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 a4f977b..770fd78 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 @@
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 @@
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 @@
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 @@
_tiles.add(insertionIndex.coerceIn(0, _tiles.size), TileGridCell(draggedTile, 0))
}
- regenerateGrid(includeSpacers = true)
+ regenerateGrid()
}
override fun movedOutOfBounds() {
@@ -109,13 +136,28 @@
_draggedCell.value = null
// Remove the spacers
- regenerateGrid(includeSpacers = false)
+ regenerateGrid()
}
- private fun regenerateGrid(includeSpacers: Boolean) {
- _tiles.filterIsInstance<TileGridCell>().toGridCells(columns, includeSpacers).let {
+ /** Regenerate the list of [GridCell] with their new potential rows */
+ private fun regenerateGrid() {
+ _tiles.filterIsInstance<TileGridCell>().toGridCells(columns).let {
_tiles.clear()
_tiles.addAll(it)
}
}
+
+ /**
+ * 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 0e76e18..30bafae 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 @@
@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 @@
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 @@
}
}
- 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 @@
) {
EditGridHeader { Text(text = "Hold and drag to add tiles.") }
- AvailableTileGrid(otherTiles, selectionState, columns, currentListState)
+ AvailableTileGrid(otherTiles, selectionState, columns, listState)
}
}
@@ -186,7 +194,7 @@
modifier =
Modifier.fillMaxWidth()
.weight(1f)
- .dragAndDropRemoveZone(currentListState, onRemoveTile)
+ .dragAndDropRemoveZone(listState, onRemoveTile)
)
}
}
@@ -229,7 +237,7 @@
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 @@
)
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 @@
)
.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 @@
}
}
+/**
+ * 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 @@
index = index,
dragAndDropState = dragAndDropState,
selectionState = selectionState,
- onResize = onResize,
+ onToggleSize = onToggleSize,
)
}
is SpacerGridCell -> SpacerGridCell()
@@ -392,16 +398,28 @@
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 @@
listOf(
// TODO(b/367748260): Add final accessibility actions
CustomAccessibilityAction("Toggle size") {
- onResize(cell.tile.tileSpec)
+ onToggleSize(cell.tile.tileSpec)
true
}
)
@@ -438,11 +456,9 @@
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 @@
@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 @@
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 4946c01..542d0ef 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 @@
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 2ea32e6..441d962 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 @@
/** 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 @@
_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 @@
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 e3acf38..7c62e59 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 @@
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 @@
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 b16a707..b1841c4 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 @@
sealed interface GridCell {
val row: Int
val span: GridItemSpan
+ val s: String
}
/**
@@ -39,6 +40,7 @@
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 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 b604e18..4e698ed 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 @@
fun isIconTile(spec: TileSpec): Boolean
- fun resize(spec: TileSpec)
+ fun resize(spec: TileSpec, toIcon: Boolean)
}
@SysUISingleton
@@ -37,5 +37,5 @@
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 6423d25..8d060e9 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 @@
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 682ed92..ee1c0e9 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.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.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 @@
)
}
- @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,22 +85,12 @@
assertThat(tiles.find { it.tile.tileSpec.spec == "tileA" }?.width).isEqualTo(2)
}
- @OptIn(ExperimentalTestApi::class)
@Test
- fun resizedLarge_shouldBeIcon() {
+ fun toggleLargeTile_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()
@@ -113,9 +101,58 @@
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, 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")
+ .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> {