Loading packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt +9 −9 Original line number Diff line number Diff line Loading @@ -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 ] Loading @@ -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 ] Loading @@ -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 ] Loading @@ -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 ] Loading @@ -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 ] Loading @@ -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) Loading packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt +24 −7 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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) Loading Loading @@ -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 Loading @@ -126,7 +142,7 @@ fun Modifier.dragAndDropTileList( } targetItem?.let { dragAndDropState.onMoved(it.index, insertAfter(it, relativeDragOffset)) dragAndDropState.onTargeting(it.index, insertAfter(it, relativeDragOffset)) } } Loading @@ -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 { Loading @@ -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) Loading @@ -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 Loading packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt +27 −11 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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() Loading Loading @@ -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) { Loading @@ -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() Loading packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +100 −25 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading Loading @@ -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) } } Loading @@ -223,7 +240,7 @@ fun DefaultEditTileGrid( AnimatedContent( targetState = listState.dragInProgress, modifier = Modifier.wrapContentSize(), label = "", label = "QSEditHeader", ) { dragIsInProgress -> EditGridHeader(Modifier.dragAndDropRemoveZone(listState, onRemoveTile)) { if (dragIsInProgress) { Loading @@ -243,12 +260,21 @@ fun DefaultEditTileGrid( onSetTiles, ) // Hide available tiles when dragging AnimatedVisibility( // Sets a minimum height to be used when available tiles are hidden Box( Modifier.fillMaxWidth() .requiredHeightIn(AvailableTilesGridMinHeight) .animateContentSize() .dragAndDropRemoveZone(listState, onRemoveTile) ) { // 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(), ) { // Hide available tiles when dragging Column( verticalArrangement = spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), Loading @@ -261,15 +287,56 @@ fun DefaultEditTileGrid( 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), ) } } } Loading Loading @@ -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 { Loading Loading @@ -596,6 +663,7 @@ private fun TileGridCell( .dragAndDropTileSource( SizedTileImpl(cell.tile, cell.width), dragAndDropState, DragType.Move, selectionState::unSelect, ) .tileBackground(colors.background) Loading Loading @@ -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) Loading Loading @@ -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 = Loading packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt +6 −6 Original line number Diff line number Diff line Loading @@ -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() Loading @@ -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 Loading @@ -140,7 +140,7 @@ class DragAndDropTest : SysuiTestCase() { } composeRule.waitForIdle() listState.onStarted(TestEditTiles[0]) listState.onStarted(TestEditTiles[0], DragType.Add) listState.movedOutOfBounds() listState.onDrop() Loading @@ -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 Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt +9 −9 Original line number Diff line number Diff line Loading @@ -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 ] Loading @@ -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 ] Loading @@ -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 ] Loading @@ -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 ] Loading @@ -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 ] Loading @@ -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) Loading
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt +24 −7 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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) Loading Loading @@ -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 Loading @@ -126,7 +142,7 @@ fun Modifier.dragAndDropTileList( } targetItem?.let { dragAndDropState.onMoved(it.index, insertAfter(it, relativeDragOffset)) dragAndDropState.onTargeting(it.index, insertAfter(it, relativeDragOffset)) } } Loading @@ -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 { Loading @@ -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) Loading @@ -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 Loading
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt +27 −11 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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() Loading Loading @@ -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) { Loading @@ -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() Loading
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +100 −25 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading Loading @@ -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) } } Loading @@ -223,7 +240,7 @@ fun DefaultEditTileGrid( AnimatedContent( targetState = listState.dragInProgress, modifier = Modifier.wrapContentSize(), label = "", label = "QSEditHeader", ) { dragIsInProgress -> EditGridHeader(Modifier.dragAndDropRemoveZone(listState, onRemoveTile)) { if (dragIsInProgress) { Loading @@ -243,12 +260,21 @@ fun DefaultEditTileGrid( onSetTiles, ) // Hide available tiles when dragging AnimatedVisibility( // Sets a minimum height to be used when available tiles are hidden Box( Modifier.fillMaxWidth() .requiredHeightIn(AvailableTilesGridMinHeight) .animateContentSize() .dragAndDropRemoveZone(listState, onRemoveTile) ) { // 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(), ) { // Hide available tiles when dragging Column( verticalArrangement = spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), Loading @@ -261,15 +287,56 @@ fun DefaultEditTileGrid( 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), ) } } } Loading Loading @@ -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 { Loading Loading @@ -596,6 +663,7 @@ private fun TileGridCell( .dragAndDropTileSource( SizedTileImpl(cell.tile, cell.width), dragAndDropState, DragType.Move, selectionState::unSelect, ) .tileBackground(colors.background) Loading Loading @@ -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) Loading Loading @@ -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 = Loading
packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt +6 −6 Original line number Diff line number Diff line Loading @@ -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() Loading @@ -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 Loading @@ -140,7 +140,7 @@ class DragAndDropTest : SysuiTestCase() { } composeRule.waitForIdle() listState.onStarted(TestEditTiles[0]) listState.onStarted(TestEditTiles[0], DragType.Add) listState.movedOutOfBounds() listState.onDrop() Loading @@ -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 Loading