diff options
5 files changed, 166 insertions, 58 deletions
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 ae7c44e9b146..8b9ae9a0606d 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 @@ -39,7 +39,7 @@ class EditTileListStateTest : SysuiTestCase() { @Test fun startDrag_listHasSpacers() { - underTest.onStarted(TestEditTiles[0]) + underTest.onStarted(TestEditTiles[0], DragType.Add) // [ a ] [ b ] [ c ] [ X ] // [ Large D ] [ e ] [ X ] @@ -51,8 +51,8 @@ class EditTileListStateTest : SysuiTestCase() { @Test fun moveDrag_listChanges() { - underTest.onStarted(TestEditTiles[4]) - underTest.onMoved(3, false) + underTest.onStarted(TestEditTiles[4], DragType.Add) + underTest.onTargeting(3, false) // Tile E goes to index 3 // [ a ] [ b ] [ c ] [ e ] @@ -65,8 +65,8 @@ class EditTileListStateTest : SysuiTestCase() { fun moveDragOnSidesOfLargeTile_listChanges() { val draggedCell = TestEditTiles[4] - underTest.onStarted(draggedCell) - underTest.onMoved(4, true) + underTest.onStarted(draggedCell, DragType.Add) + underTest.onTargeting(4, true) // Tile E goes to the right side of tile D, list is unchanged // [ a ] [ b ] [ c ] [ X ] @@ -74,7 +74,7 @@ class EditTileListStateTest : SysuiTestCase() { assertThat(underTest.tiles.toStrings()) .isEqualTo(listOf("a", "b", "c", "spacer", "d", "e", "spacer")) - underTest.onMoved(4, false) + underTest.onTargeting(4, false) // Tile E goes to the left side of tile D, they swap positions // [ a ] [ b ] [ c ] [ e ] @@ -87,8 +87,8 @@ class EditTileListStateTest : SysuiTestCase() { fun moveNewTile_tileIsAdded() { val newTile = createEditTile("newTile", 2) - underTest.onStarted(newTile) - underTest.onMoved(5, false) + underTest.onStarted(newTile, DragType.Add) + underTest.onTargeting(5, false) // New tile goes to index 5 // [ a ] [ b ] [ c ] [ X ] @@ -102,7 +102,7 @@ class EditTileListStateTest : SysuiTestCase() { @Test fun movedTileOutOfBounds_tileDisappears() { - underTest.onStarted(TestEditTiles[0]) + underTest.onStarted(TestEditTiles[0], DragType.Add) underTest.movedOutOfBounds() assertThat(underTest.tiles.toStrings()).doesNotContain(TestEditTiles[0].tile.tileSpec.spec) 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 35faa97db2fe..405ce8a8e5e0 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 @@ -44,19 +44,28 @@ import com.android.systemui.qs.pipeline.shared.TileSpec /** Holds the [TileSpec] of the tile being moved and receives drag and drop events. */ interface DragAndDropState { val draggedCell: SizedTile<EditTileViewModel>? + val draggedPosition: Offset val dragInProgress: Boolean + val dragType: DragType? fun isMoving(tileSpec: TileSpec): Boolean - fun onStarted(cell: SizedTile<EditTileViewModel>) + fun onStarted(cell: SizedTile<EditTileViewModel>, dragType: DragType) - fun onMoved(target: Int, insertAfter: Boolean) + fun onTargeting(target: Int, insertAfter: Boolean) + + fun onMoved(offset: Offset) fun movedOutOfBounds() fun onDrop() } +enum class DragType { + Add, + Move, +} + /** * Registers a composable as a [DragAndDropTarget] to receive drop events. Use this outside the tile * grid to catch out of bounds drops. @@ -72,6 +81,10 @@ fun Modifier.dragAndDropRemoveZone( val target = remember(dragAndDropState) { object : DragAndDropTarget { + override fun onMoved(event: DragAndDropEvent) { + dragAndDropState.onMoved(event.toOffset()) + } + override fun onDrop(event: DragAndDropEvent): Boolean { return dragAndDropState.draggedCell?.let { onDrop(it.tile.tileSpec) @@ -117,8 +130,11 @@ fun Modifier.dragAndDropTileList( } override fun onMoved(event: DragAndDropEvent) { + val offset = event.toOffset() + dragAndDropState.onMoved(offset) + // Drag offset relative to the list's top left corner - val relativeDragOffset = event.dragOffsetRelativeTo(contentOffset()) + val relativeDragOffset = offset - contentOffset() val targetItem = gridState.layoutInfo.visibleItemsInfo.firstOrNull { item -> // Check if the drag is on this item @@ -126,7 +142,7 @@ fun Modifier.dragAndDropTileList( } targetItem?.let { - dragAndDropState.onMoved(it.index, insertAfter(it, relativeDragOffset)) + dragAndDropState.onTargeting(it.index, insertAfter(it, relativeDragOffset)) } } @@ -147,8 +163,8 @@ fun Modifier.dragAndDropTileList( ) } -private fun DragAndDropEvent.dragOffsetRelativeTo(offset: Offset): Offset { - return toAndroidDragEvent().run { Offset(x, y) } - offset +private fun DragAndDropEvent.toOffset(): Offset { + return toAndroidDragEvent().run { Offset(x, y) } } private fun insertAfter(item: LazyGridItemInfo, offset: Offset): Boolean { @@ -163,6 +179,7 @@ private fun insertAfter(item: LazyGridItemInfo, offset: Offset): Boolean { fun Modifier.dragAndDropTileSource( sizedTile: SizedTile<EditTileViewModel>, dragAndDropState: DragAndDropState, + dragType: DragType, onDragStart: () -> Unit, ): Modifier { val dragState by rememberUpdatedState(dragAndDropState) @@ -172,7 +189,7 @@ fun Modifier.dragAndDropTileSource( detectDragGesturesAfterLongPress( onDrag = { _, _ -> }, onDragStart = { - dragState.onStarted(sizedTile) + dragState.onStarted(sizedTile, dragType) onDragStart() // The tilespec from the ClipData transferred isn't actually needed as we're 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 14abfa2313d8..114e3c48a7a0 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 @@ -17,10 +17,13 @@ package com.android.systemui.qs.panels.ui.compose import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.geometry.Offset import com.android.systemui.qs.panels.shared.model.SizedTile import com.android.systemui.qs.panels.ui.model.GridCell import com.android.systemui.qs.panels.ui.model.TileGridCell @@ -48,12 +51,17 @@ class EditTileListState( private val columns: Int, private val largeTilesSpan: Int, ) : DragAndDropState { - private val _draggedCell = mutableStateOf<SizedTile<EditTileViewModel>?>(null) - override val draggedCell - get() = _draggedCell.value + override var draggedCell by mutableStateOf<SizedTile<EditTileViewModel>?>(null) + private set + + override var draggedPosition by mutableStateOf(Offset.Unspecified) + private set + + override var dragType by mutableStateOf<DragType?>(null) + private set override val dragInProgress: Boolean - get() = _draggedCell.value != null + get() = draggedCell != null private val _tiles: SnapshotStateList<GridCell> = tiles.toGridCells(columns).toMutableStateList() @@ -83,18 +91,19 @@ class EditTileListState( } override fun isMoving(tileSpec: TileSpec): Boolean { - return _draggedCell.value?.let { it.tile.tileSpec == tileSpec } ?: false + return draggedCell?.let { it.tile.tileSpec == tileSpec } ?: false } - override fun onStarted(cell: SizedTile<EditTileViewModel>) { - _draggedCell.value = cell + override fun onStarted(cell: SizedTile<EditTileViewModel>, dragType: DragType) { + draggedCell = cell + this.dragType = dragType // Add spacers to the grid to indicate where the user can move a tile regenerateGrid() } - override fun onMoved(target: Int, insertAfter: Boolean) { - val draggedTile = _draggedCell.value ?: return + override fun onTargeting(target: Int, insertAfter: Boolean) { + val draggedTile = draggedCell ?: return val fromIndex = indexOf(draggedTile.tile.tileSpec) if (fromIndex == target) { @@ -115,16 +124,23 @@ class EditTileListState( regenerateGrid() } + override fun onMoved(offset: Offset) { + draggedPosition = offset + } + override fun movedOutOfBounds() { - val draggedTile = _draggedCell.value ?: return + val draggedTile = draggedCell ?: return _tiles.removeIf { cell -> cell is TileGridCell && cell.tile.tileSpec == draggedTile.tile.tileSpec } + draggedPosition = Offset.Unspecified } override fun onDrop() { - _draggedCell.value = null + draggedCell = null + draggedPosition = Offset.Unspecified + dragType = null // Remove the spacers regenerateGrid() 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 a05747dd3ba2..d975f104d538 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 @@ -20,12 +20,16 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility +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 import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.LocalOverscrollFactory +import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clipScrollableContainer @@ -43,6 +47,7 @@ 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.requiredHeightIn import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize @@ -69,6 +74,7 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -80,6 +86,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.isSpecified import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.MeasureScope @@ -111,6 +118,7 @@ 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.compose.BounceableInfo import com.android.systemui.qs.panels.ui.compose.DragAndDropState +import com.android.systemui.qs.panels.ui.compose.DragType import com.android.systemui.qs.panels.ui.compose.EditTileListState import com.android.systemui.qs.panels.ui.compose.bounceableInfo import com.android.systemui.qs.panels.ui.compose.dragAndDropRemoveZone @@ -120,6 +128,9 @@ import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TileArrangementPadding import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TileHeight import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.ToggleTargetSize +import com.android.systemui.qs.panels.ui.compose.infinitegrid.EditModeTileDefaults.AUTO_SCROLL_DISTANCE +import com.android.systemui.qs.panels.ui.compose.infinitegrid.EditModeTileDefaults.AUTO_SCROLL_SPEED +import com.android.systemui.qs.panels.ui.compose.infinitegrid.EditModeTileDefaults.AvailableTilesGridMinHeight import com.android.systemui.qs.panels.ui.compose.infinitegrid.EditModeTileDefaults.CurrentTilesGridPadding import com.android.systemui.qs.panels.ui.compose.selection.MutableSelectionState import com.android.systemui.qs.panels.ui.compose.selection.ResizableTileContainer @@ -139,8 +150,10 @@ import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.shared.model.groupAndSort import com.android.systemui.res.R +import kotlin.math.abs import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay object TileType @@ -201,8 +214,12 @@ fun DefaultEditTileGrid( ) { innerPadding -> CompositionLocalProvider(LocalOverscrollFactory provides null) { val scrollState = rememberScrollState() - LaunchedEffect(listState.dragInProgress) { - if (listState.dragInProgress) { + + AutoScrollGrid(listState, scrollState, innerPadding) + + LaunchedEffect(listState.dragType) { + // Only scroll to the top when adding a new tile, not when reordering existing ones + if (listState.dragInProgress && listState.dragType == DragType.Add) { scrollState.animateScrollTo(0) } } @@ -223,7 +240,7 @@ fun DefaultEditTileGrid( AnimatedContent( targetState = listState.dragInProgress, modifier = Modifier.wrapContentSize(), - label = "", + label = "QSEditHeader", ) { dragIsInProgress -> EditGridHeader(Modifier.dragAndDropRemoveZone(listState, onRemoveTile)) { if (dragIsInProgress) { @@ -243,34 +260,84 @@ fun DefaultEditTileGrid( onSetTiles, ) - // Hide available tiles when dragging - AnimatedVisibility( - visible = !listState.dragInProgress, - enter = fadeIn(), - exit = fadeOut(), + // Sets a minimum height to be used when available tiles are hidden + Box( + Modifier.fillMaxWidth() + .requiredHeightIn(AvailableTilesGridMinHeight) + .animateContentSize() + .dragAndDropRemoveZone(listState, onRemoveTile) ) { - Column( - verticalArrangement = - spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), - modifier = modifier.fillMaxSize(), + // Using the fully qualified name here as a workaround for AnimatedVisibility + // not being available from a Box + androidx.compose.animation.AnimatedVisibility( + visible = !listState.dragInProgress, + enter = fadeIn(), + exit = fadeOut(), ) { - EditGridHeader { - Text(text = stringResource(id = R.string.drag_to_add_tiles)) - } + // Hide available tiles when dragging + Column( + verticalArrangement = + spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), + modifier = modifier.fillMaxSize(), + ) { + EditGridHeader { + Text(text = stringResource(id = R.string.drag_to_add_tiles)) + } - AvailableTileGrid(otherTiles, selectionState, columns, listState) + AvailableTileGrid(otherTiles, selectionState, columns, listState) + } } } + } + } + } +} - // Drop zone to remove tiles dragged out of the tile grid - Spacer( - modifier = - Modifier.fillMaxWidth() - .weight(1f) - .dragAndDropRemoveZone(listState, onRemoveTile) - ) +@OptIn(ExperimentalCoroutinesApi::class) +@Composable +private fun AutoScrollGrid( + listState: EditTileListState, + scrollState: ScrollState, + padding: PaddingValues, +) { + val density = LocalDensity.current + val (top, bottom) = + remember(density) { + with(density) { + padding.calculateTopPadding().roundToPx() to + padding.calculateBottomPadding().roundToPx() + } + } + val scrollTarget by + remember(listState, scrollState, top, bottom) { + derivedStateOf { + val position = listState.draggedPosition + if (position.isSpecified) { + // Return the scroll target needed based on the position of the drag movement, + // or null if we don't need to scroll + val y = position.y.roundToInt() + when { + y < AUTO_SCROLL_DISTANCE + top -> 0 + y > scrollState.viewportSize - bottom - AUTO_SCROLL_DISTANCE -> + scrollState.maxValue + else -> null + } + } else { + null + } } } + LaunchedEffect(scrollTarget) { + scrollTarget?.let { + // Change the duration of the animation based on the distance to maintain the + // same scrolling speed + val distance = abs(it - scrollState.value) + scrollState.animateScrollTo( + it, + animationSpec = + tween(durationMillis = distance * AUTO_SCROLL_SPEED, easing = LinearEasing), + ) + } } } @@ -423,7 +490,7 @@ private fun AvailableTileGrid( } fun gridHeight(rows: Int, tileHeight: Dp, tilePadding: Dp, gridPadding: Dp): Dp { - return ((tileHeight + tilePadding) * rows) - tilePadding + gridPadding * 2 + return ((tileHeight + tilePadding) * rows) + gridPadding * 2 } private fun GridCell.key(index: Int, dragAndDropState: DragAndDropState): Any { @@ -596,6 +663,7 @@ private fun TileGridCell( .dragAndDropTileSource( SizedTileImpl(cell.tile, cell.width), dragAndDropState, + DragType.Move, selectionState::unSelect, ) .tileBackground(colors.background) @@ -631,7 +699,11 @@ private fun AvailableTileGridCell( onClick(onClickActionName) { false } this.stateDescription = stateDescription } - .dragAndDropTileSource(SizedTileImpl(cell.tile, cell.width), dragAndDropState) { + .dragAndDropTileSource( + SizedTileImpl(cell.tile, cell.width), + dragAndDropState, + DragType.Add, + ) { selectionState.unSelect() } .tileBackground(colors.background) @@ -739,7 +811,10 @@ private fun Modifier.tileBackground(color: Color): Modifier { private object EditModeTileDefaults { const val PLACEHOLDER_ALPHA = .3f + const val AUTO_SCROLL_DISTANCE = 100 + const val AUTO_SCROLL_SPEED = 2 // 2ms per pixel val CurrentTilesGridPadding = 8.dp + val AvailableTilesGridMinHeight = 200.dp @Composable fun editTileColors(): TileColors = 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 d2317e4f533d..fc720b836f72 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 @@ -87,7 +87,7 @@ class DragAndDropTest : SysuiTestCase() { } composeRule.waitForIdle() - listState.onStarted(TestEditTiles[0]) + listState.onStarted(TestEditTiles[0], DragType.Add) // Tile is being dragged, it should be replaced with a placeholder composeRule.onNodeWithContentDescription("tileA").assertDoesNotExist() @@ -113,8 +113,8 @@ class DragAndDropTest : SysuiTestCase() { } composeRule.waitForIdle() - listState.onStarted(TestEditTiles[0]) - listState.onMoved(1, false) + listState.onStarted(TestEditTiles[0], DragType.Add) + listState.onTargeting(1, false) listState.onDrop() // Available tiles should re-appear @@ -140,7 +140,7 @@ class DragAndDropTest : SysuiTestCase() { } composeRule.waitForIdle() - listState.onStarted(TestEditTiles[0]) + listState.onStarted(TestEditTiles[0], DragType.Add) listState.movedOutOfBounds() listState.onDrop() @@ -165,11 +165,11 @@ class DragAndDropTest : SysuiTestCase() { } composeRule.waitForIdle() - listState.onStarted(createEditTile("newTile")) + listState.onStarted(createEditTile("newTile"), DragType.Add) // Insert after tileD, which is at index 4 // [ a ] [ b ] [ c ] [ empty ] // [ tile d ] [ e ] - listState.onMoved(4, insertAfter = true) + listState.onTargeting(4, insertAfter = true) listState.onDrop() // Available tiles should re-appear |