diff options
8 files changed, 345 insertions, 60 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt index 1c3021ef5839..73a00395606e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt @@ -82,6 +82,20 @@ class DragAndDropStateTest : SysuiTestCase() { TestEditTiles.forEach { assertThat(underTest.isMoving(it.tileSpec)).isFalse() } } + @Test + fun onMoveOutOfBounds_removeMovingTileFromCurrentList() { + val movingTileSpec = TestEditTiles[0].tileSpec + + // Start the drag movement + underTest.onStarted(movingTileSpec) + + // Move the tile outside of the list + underTest.movedOutOfBounds() + + // Asserts the moving tile is not current + assertThat(listState.tiles.first { it.tileSpec == movingTileSpec }.isCurrent).isFalse() + } + companion object { private fun createEditTile(tileSpec: String): EditTileViewModel { return EditTileViewModel( diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt index 295a998e7500..782fb2ad25a0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt @@ -48,6 +48,9 @@ class DragAndDropState( val sourceSpec: MutableState<TileSpec?>, private val listState: EditTileListState ) { + val dragInProgress: Boolean + get() = sourceSpec.value != null + /** Returns index of the dragged tile if it's present in the list. Returns -1 if not. */ fun currentPosition(): Int { return sourceSpec.value?.let { listState.indexOf(it) } ?: -1 @@ -65,6 +68,12 @@ class DragAndDropState( sourceSpec.value?.let { listState.move(it, targetSpec) } } + fun movedOutOfBounds() { + // Removing the tiles from the current tile grid if it moves out of bounds. This clears + // the spacer and makes it apparent that dropping the tile at that point would remove it. + sourceSpec.value?.let { listState.removeFromCurrent(it) } + } + fun onDrop() { sourceSpec.value = null } @@ -112,6 +121,42 @@ fun Modifier.dragAndDropTile( } /** + * Registers a composable as a [DragAndDropTarget] to receive drop events. Use this outside the tile + * grid to catch out of bounds drops. + * + * @param dragAndDropState The [DragAndDropState] using the tiles list + * @param onDrop Action to be executed when a [TileSpec] is dropped on the composable + */ +@Composable +fun Modifier.dragAndDropRemoveZone( + dragAndDropState: DragAndDropState, + onDrop: (TileSpec) -> Unit, +): Modifier { + val target = + remember(dragAndDropState) { + object : DragAndDropTarget { + override fun onDrop(event: DragAndDropEvent): Boolean { + return dragAndDropState.sourceSpec.value?.let { + onDrop(it) + dragAndDropState.onDrop() + true + } ?: false + } + + override fun onEntered(event: DragAndDropEvent) { + dragAndDropState.movedOutOfBounds() + } + } + } + return dragAndDropTarget( + shouldStartDragAndDrop = { event -> + event.mimeTypes().contains(QsDragAndDrop.TILESPEC_MIME_TYPE) + }, + target = target, + ) +} + +/** * Registers a tile list as a [DragAndDropTarget] to receive drop events. Use this on list * containers to catch drops outside of tiles. * @@ -128,6 +173,10 @@ fun Modifier.dragAndDropTileList( val target = remember(dragAndDropState) { object : DragAndDropTarget { + override fun onEnded(event: DragAndDropEvent) { + dragAndDropState.onDrop() + } + override fun onDrop(event: DragAndDropEvent): Boolean { return dragAndDropState.sourceSpec.value?.let { onDrop(it, dragAndDropState.currentPosition()) 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 482c498d37d7..34876c4cf7f3 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 @@ -46,6 +46,18 @@ class EditTileListState(tiles: List<EditTileViewModel>) { tiles.apply { add(toIndex, removeAt(fromIndex).copy(isCurrent = isMovingToCurrent)) } } + /** + * Sets the [TileSpec] as a non-current tile. Use this when a tile is dragged out of the current + * tile grid. + */ + fun removeFromCurrent(tileSpec: TileSpec) { + val fromIndex = indexOf(tileSpec) + if (fromIndex >= 0 && fromIndex < tiles.size) { + // Mark the moving tile as non-current + tiles[fromIndex] = tiles[fromIndex].copy(isCurrent = false) + } + } + fun indexOf(tileSpec: TileSpec): Int { return tiles.indexOfFirst { it.tileSpec == tileSpec } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt index ada774db1c6e..add830e9760d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt @@ -84,7 +84,7 @@ constructor( DefaultEditTileGrid( tiles = tiles, isIconOnly = isIcon, - columns = GridCells.Fixed(columns), + columns = columns, modifier = modifier, onAddTile = onAddTile, onRemoveTile = onRemoveTile, 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 8ca91d89439b..6c84eddb5e38 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 @@ -305,9 +305,9 @@ class PartitionedGridLayout @Inject constructor(private val viewModel: Partition largeTiles, ClickAction.ADD, addTileToEnd, - onDoubleTap, isIconOnly, dragAndDropState, + onDoubleTap = onDoubleTap, acceptDrops = { true }, onDrop = onDrop, ) @@ -318,10 +318,10 @@ class PartitionedGridLayout @Inject constructor(private val viewModel: Partition smallTiles, ClickAction.ADD, addTileToEnd, - onDoubleTap, isIconOnly, + dragAndDropState, + onDoubleTap = onDoubleTap, showLabels = showLabels, - dragAndDropState = dragAndDropState, acceptDrops = { true }, onDrop = onDrop, ) @@ -332,10 +332,10 @@ class PartitionedGridLayout @Inject constructor(private val viewModel: Partition tilesCustom, ClickAction.ADD, addTileToEnd, - onDoubleTap, isIconOnly, + dragAndDropState, + onDoubleTap = onDoubleTap, showLabels = showLabels, - dragAndDropState = dragAndDropState, acceptDrops = { true }, onDrop = onDrop, ) @@ -372,11 +372,6 @@ class PartitionedGridLayout @Inject constructor(private val viewModel: Partition } } - private fun gridHeight(nTiles: Int, tileHeight: Dp, columns: Int, padding: Dp): Dp { - val rows = (nTiles + columns - 1) / columns - return ((tileHeight + padding) * rows) - padding - } - /** Fill up the rest of the row if it's not complete. */ private fun LazyGridScope.fillUpRow(nTiles: Int, columns: Int) { if (nTiles % columns != 0) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt index 770d44124025..3e48245a3d36 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt @@ -100,7 +100,7 @@ constructor( DefaultEditTileGrid( tiles = tiles, isIconOnly = iconTilesViewModel::isIconTile, - columns = GridCells.Fixed(columns), + columns = columns, modifier = modifier, onAddTile = onAddTile, onRemoveTile = onRemoveTile, 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 3fdd7f769cf3..bd7956d7d3e1 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 @@ -23,13 +23,19 @@ import android.service.quicksettings.Tile.STATE_ACTIVE import android.service.quicksettings.Tile.STATE_INACTIVE import android.text.TextUtils import androidx.appcompat.content.res.AppCompatResources +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi import androidx.compose.animation.graphics.res.animatedVectorResource import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter import androidx.compose.animation.graphics.vector.AnimatedImageVector import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image +import androidx.compose.foundation.LocalOverscrollConfiguration import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.border import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement.spacedBy @@ -37,20 +43,31 @@ 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.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -80,6 +97,8 @@ 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.shared.model.SizedTile +import com.android.systemui.qs.panels.shared.model.TileRow import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel import com.android.systemui.qs.panels.ui.viewmodel.toUiState @@ -269,7 +288,7 @@ fun TileLazyGrid( fun DefaultEditTileGrid( tiles: List<EditTileViewModel>, isIconOnly: (TileSpec) -> Boolean, - columns: GridCells, + columns: Int, modifier: Modifier, onAddTile: (TileSpec, Int) -> Unit, onRemoveTile: (TileSpec) -> Unit, @@ -277,84 +296,264 @@ fun DefaultEditTileGrid( ) { val currentListState = rememberEditListState(tiles) val dragAndDropState = rememberDragAndDropState(currentListState) - val (currentTiles, otherTiles) = currentListState.tiles.partition { it.isCurrent } - val (otherTilesStock, otherTilesCustom) = - otherTiles - .filter { !dragAndDropState.isMoving(it.tileSpec) } - .partition { it.appName == null } + val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState { onAddTile(it, CurrentTilesInteractor.POSITION_AT_END) } - val onDropAdd: (TileSpec, Int) -> Unit by rememberUpdatedState { tileSpec, position -> onAddTile(tileSpec, position) } - val onDropRemove: (TileSpec, Int) -> Unit by rememberUpdatedState { tileSpec, _ -> - onRemoveTile(tileSpec) - } val onDoubleTap: (TileSpec) -> Unit by rememberUpdatedState { tileSpec -> onResize(tileSpec, !isIconOnly(tileSpec)) } + val tilePadding = dimensionResource(R.dimen.qs_tile_margin_vertical) - TileLazyGrid( - modifier = modifier.dragAndDropTileList(dragAndDropState, { true }, onDropAdd), - columns = columns + CompositionLocalProvider(LocalOverscrollConfiguration provides null) { + Column( + verticalArrangement = + spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), + modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()) + ) { + AnimatedContent( + targetState = dragAndDropState.dragInProgress, + modifier = Modifier.wrapContentSize() + ) { dragIsInProgress -> + EditGridHeader(Modifier.dragAndDropRemoveZone(dragAndDropState, onRemoveTile)) { + if (dragIsInProgress) { + RemoveTileTarget() + } else { + Text(text = "Hold and drag to rearrange tiles.") + } + } + } + + CurrentTilesGrid( + currentTiles, + columns, + tilePadding, + isIconOnly, + onRemoveTile, + onDoubleTap, + dragAndDropState, + onDropAdd, + ) + + // Hide available tiles when dragging + AnimatedVisibility( + visible = !dragAndDropState.dragInProgress, + enter = fadeIn(), + exit = fadeOut() + ) { + Column( + verticalArrangement = + spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), + modifier = modifier.fillMaxSize() + ) { + EditGridHeader { Text(text = "Hold and drag to add tiles.") } + + AvailableTileGrid( + otherTiles, + columns, + tilePadding, + addTileToEnd, + dragAndDropState, + ) + } + } + + // Drop zone to remove tiles dragged out of the tile grid + Spacer( + modifier = + Modifier.fillMaxWidth() + .weight(1f) + .dragAndDropRemoveZone(dragAndDropState, onRemoveTile) + ) + } + } +} + +@Composable +private fun EditGridHeader( + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit +) { + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colorScheme.onBackground.copy(alpha = .5f) ) { - // These Text are just placeholders to see the different sections. Not final UI. - item(span = { GridItemSpan(maxLineSpan) }) { Text("Current tiles", color = Color.White) } + Box( + contentAlignment = Alignment.Center, + modifier = modifier.fillMaxWidth().height(TileDefaults.EditGridHeaderHeight) + ) { + content() + } + } +} - editTiles( - currentTiles, - ClickAction.REMOVE, - onRemoveTile, - onDoubleTap, - isIconOnly, - indicatePosition = true, - dragAndDropState = dragAndDropState, - acceptDrops = { true }, - onDrop = onDropAdd, - ) +@Composable +private fun RemoveTileTarget() { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = tileHorizontalArrangement(), + modifier = + Modifier.fillMaxHeight() + .border(1.dp, LocalContentColor.current, shape = TileDefaults.TileShape) + .padding(10.dp) + ) { + Icon(imageVector = Icons.Default.Clear, contentDescription = null) + Text(text = "Remove") + } +} - item(span = { GridItemSpan(maxLineSpan) }) { Text("Tiles to add", color = Color.White) } +@Composable +private fun CurrentTilesContainer(content: @Composable () -> Unit) { + Box( + Modifier.fillMaxWidth() + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = .5f), + shape = RoundedCornerShape(48.dp), + ) + .padding(dimensionResource(R.dimen.qs_tile_margin_vertical)) + ) { + content() + } +} + +@Composable +private fun CurrentTilesGrid( + tiles: List<EditTileViewModel>, + columns: Int, + tilePadding: Dp, + isIconOnly: (TileSpec) -> Boolean, + onClick: (TileSpec) -> Unit, + onDoubleTap: (TileSpec) -> Unit, + dragAndDropState: DragAndDropState, + onDrop: (TileSpec, Int) -> Unit +) { + val tileHeight = tileHeight() + val currentRows = + remember(tiles) { + calculateRows( + tiles.map { + SizedTile( + it, + if (isIconOnly(it.tileSpec)) { + 1 + } else { + 2 + } + ) + }, + columns + ) + } + val currentGridHeight = gridHeight(currentRows, tileHeight, tilePadding) + // Current tiles + CurrentTilesContainer { + TileLazyGrid( + modifier = + Modifier.height(currentGridHeight) + .dragAndDropTileList(dragAndDropState, { true }, onDrop), + columns = GridCells.Fixed(columns) + ) { + editTiles( + tiles, + ClickAction.REMOVE, + onClick, + isIconOnly, + dragAndDropState, + onDoubleTap = onDoubleTap, + indicatePosition = true, + acceptDrops = { true }, + onDrop = onDrop, + ) + } + } +} +@Composable +private fun AvailableTileGrid( + tiles: List<EditTileViewModel>, + columns: Int, + tilePadding: Dp, + onClick: (TileSpec) -> Unit, + dragAndDropState: DragAndDropState, +) { + val (otherTilesStock, otherTilesCustom) = + tiles.filter { !dragAndDropState.isMoving(it.tileSpec) }.partition { it.appName == null } + val availableTileHeight = tileHeight(true) + val availableGridHeight = gridHeight(tiles.size, availableTileHeight, columns, tilePadding) + + // Available tiles + TileLazyGrid( + modifier = + Modifier.height(availableGridHeight) + .dragAndDropTileList(dragAndDropState, { false }, { _, _ -> }), + columns = GridCells.Fixed(columns) + ) { editTiles( otherTilesStock, ClickAction.ADD, - addTileToEnd, - onDoubleTap, - isIconOnly, + onClick, + isIconOnly = { true }, dragAndDropState = dragAndDropState, - acceptDrops = { true }, - onDrop = onDropRemove, + acceptDrops = { false }, + showLabels = true, ) - - item(span = { GridItemSpan(maxLineSpan) }) { - Text("Custom tiles to add", color = Color.White) - } - editTiles( otherTilesCustom, ClickAction.ADD, - addTileToEnd, - onDoubleTap, - isIconOnly, + onClick, + isIconOnly = { true }, dragAndDropState = dragAndDropState, - acceptDrops = { true }, - onDrop = onDropRemove, + acceptDrops = { false }, + showLabels = true, ) } } +fun gridHeight(nTiles: Int, tileHeight: Dp, columns: Int, padding: Dp): Dp { + val rows = (nTiles + columns - 1) / columns + return gridHeight(rows, tileHeight, padding) +} + +fun gridHeight(rows: Int, tileHeight: Dp, padding: Dp): Dp { + return ((tileHeight + padding) * rows) - padding +} + +private fun calculateRows(tiles: List<SizedTile<EditTileViewModel>>, columns: Int): Int { + val row = TileRow<EditTileViewModel>(columns) + var count = 0 + + for (tile in tiles) { + if (row.maybeAddTile(tile)) { + if (row.isFull()) { + // Row is full, no need to stretch tiles + count += 1 + row.clear() + } + } else { + count += 1 + row.clear() + row.maybeAddTile(tile) + } + } + if (row.tiles.isNotEmpty()) { + count += 1 + } + return count +} + fun LazyGridScope.editTiles( tiles: List<EditTileViewModel>, clickAction: ClickAction, onClick: (TileSpec) -> Unit, - onDoubleTap: (TileSpec) -> Unit, isIconOnly: (TileSpec) -> Boolean, dragAndDropState: DragAndDropState, acceptDrops: (TileSpec) -> Boolean, - onDrop: (TileSpec, Int) -> Unit, + onDoubleTap: (TileSpec) -> Unit = {}, + onDrop: (TileSpec, Int) -> Unit = { _, _ -> }, showLabels: Boolean = false, indicatePosition: Boolean = false, ) { @@ -534,6 +733,7 @@ private data class TileColors( private object TileDefaults { val TileShape = CircleShape val IconTileWithLabelHeight = 140.dp + val EditGridHeaderHeight = 60.dp @Composable fun activeTileColors(): TileColors = diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt index 62bfc72f07f2..ef2c8bfe0e4c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt @@ -151,12 +151,27 @@ constructor( * present, it will be moved to the new position. */ fun addTile(tileSpec: TileSpec, position: Int = POSITION_AT_END) { - // Removing tile if it's already present to insert it at the new index. - if (currentTilesInteractor.currentTilesSpecs.contains(tileSpec)) { - removeTile(tileSpec) + val specs = currentTilesInteractor.currentTilesSpecs.toMutableList() + val currentPosition = specs.indexOf(tileSpec) + + if (currentPosition != -1) { + // No operation needed if the element is already in the list at the right position + if (currentPosition == position) { + return + } + // Removing tile if it's present at a different position to insert it at the new index. + specs.removeAt(currentPosition) + } + + if (position >= 0 && position < specs.size) { + specs.add(position, tileSpec) + } else { + specs.add(tileSpec) } - currentTilesInteractor.addTile(tileSpec, position) + // Setting the new tiles as one operation to avoid UI jank with tiles disappearing and + // reappearing + currentTilesInteractor.setTiles(specs) } /** Immediately removes [tileSpec] from the current tiles. */ |