summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt38
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt125
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt12
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