Loading packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt +1 −115 Original line number Diff line number Diff line Loading @@ -27,7 +27,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class MutableSelectionStateTest : SysuiTestCase() { private val underTest = MutableSelectionState({}, {}) private val underTest = MutableSelectionState() @Test fun selectTile_isCorrectlySelected() { Loading @@ -48,120 +48,6 @@ class MutableSelectionStateTest : SysuiTestCase() { assertThat(underTest.selection?.manual).isFalse() } @Test fun startResize_createsResizingState() { assertThat(underTest.resizingState).isNull() // Resizing starts but no tile is selected underTest.onResizingDragStart(TileWidths(0, 0, 1)) assertThat(underTest.resizingState).isNull() // Resizing starts with a selected tile underTest.select(TEST_SPEC, manual = true) underTest.onResizingDragStart(TileWidths(0, 0, 1)) assertThat(underTest.resizingState).isNotNull() } @Test fun endResize_clearsResizingState() { val spec = TileSpec.create("testSpec") // Resizing starts with a selected tile underTest.select(spec, manual = true) underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) assertThat(underTest.resizingState).isNotNull() underTest.onResizingDragEnd() assertThat(underTest.resizingState).isNull() } @Test fun unselect_clearsResizingState() { // Resizing starts with a selected tile underTest.select(TEST_SPEC, manual = true) underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) assertThat(underTest.resizingState).isNotNull() underTest.unSelect() assertThat(underTest.resizingState).isNull() } @Test fun onResizingDrag_updatesResizingState() { // Resizing starts with a selected tile underTest.select(TEST_SPEC, manual = true) underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) assertThat(underTest.resizingState).isNotNull() underTest.onResizingDrag(5f) assertThat(underTest.resizingState?.width).isEqualTo(5) underTest.onResizingDrag(2f) assertThat(underTest.resizingState?.width).isEqualTo(7) underTest.onResizingDrag(-6f) assertThat(underTest.resizingState?.width).isEqualTo(1) } @Test fun onResizingDrag_receivesResizeCallback() { var resized = false val onResize: (TileSpec) -> Unit = { assertThat(it).isEqualTo(TEST_SPEC) resized = !resized } val underTest = MutableSelectionState(onResize = onResize, {}) // Resizing starts with a selected tile underTest.select(TEST_SPEC, true) underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) assertThat(underTest.resizingState).isNotNull() // Drag under the threshold underTest.onResizingDrag(1f) assertThat(resized).isFalse() // Drag over the threshold underTest.onResizingDrag(5f) assertThat(resized).isTrue() // Drag back under the threshold underTest.onResizingDrag(-5f) assertThat(resized).isFalse() } @Test fun onResizingEnded_receivesResizeEndCallback() { var resizeEnded = false val onResizeEnd: (TileSpec) -> Unit = { resizeEnded = true } val underTest = MutableSelectionState({}, onResizeEnd = onResizeEnd) // Resizing starts with a selected tile underTest.select(TEST_SPEC, true) underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) underTest.onResizingDragEnd() assertThat(resizeEnded).isTrue() } @Test fun onResizingEnded_setsSelectionAutomatically() { val underTest = MutableSelectionState({}, {}) // Resizing starts with a selected tile underTest.select(TEST_SPEC, manual = true) underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) // Assert the selection was manual assertThat(underTest.selection?.manual).isTrue() underTest.onResizingDragEnd() // Assert the selection is no longer manual due to the resizing assertThat(underTest.selection?.manual).isFalse() } companion object { private val TEST_SPEC = TileSpec.create("testSpec") } Loading packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingStateTest.kt +21 −23 Original line number Diff line number Diff line Loading @@ -19,7 +19,9 @@ package com.android.systemui.qs.panels.ui.compose.selection import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.qs.pipeline.shared.TileSpec import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith Loading @@ -27,36 +29,32 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ResizingStateTest : SysuiTestCase() { @Test fun drag_updatesStateCorrectly() { var resized = false val underTest = ResizingState(TileWidths(base = 0, min = 0, max = 10)) { resized = !resized } assertThat(underTest.width).isEqualTo(0) private val underTest = ResizingState(TileSpec.create("a"), startsAsIcon = true).apply { updateAnchors(10f, 20f) } underTest.onDrag(2f) assertThat(underTest.width).isEqualTo(2) underTest.onDrag(1f) assertThat(underTest.width).isEqualTo(3) assertThat(resized).isTrue() @Test fun newResizingState_setInitialValueCorrectly() { assertThat(underTest.anchoredDraggableState.currentValue).isEqualTo(QSDragAnchor.Icon) } underTest.onDrag(-1f) assertThat(underTest.width).isEqualTo(2) assertThat(resized).isFalse() @Test fun updateAnchors_setBoundsCorrectly() { assertThat(underTest.bounds).isEqualTo(10f to 20f) } @Test fun dragOutOfBounds_isClampedCorrectly() { val underTest = ResizingState(TileWidths(base = 0, min = 0, max = 10)) {} fun dragOverThreshold_resizesToLarge() = runTest { underTest.anchoredDraggableState.anchoredDrag { dragTo(16f) } assertThat(underTest.width).isEqualTo(0) assertThat(underTest.temporaryResizeOperation.spec).isEqualTo(TileSpec.create("a")) assertThat(underTest.temporaryResizeOperation.toIcon).isFalse() } underTest.onDrag(100f) assertThat(underTest.width).isEqualTo(10) @Test fun dragUnderThreshold_staysIcon() = runTest { underTest.anchoredDraggableState.anchoredDrag { dragTo(12f) } underTest.onDrag(-200f) assertThat(underTest.width).isEqualTo(0) assertThat(underTest.temporaryResizeOperation.spec).isEqualTo(TileSpec.create("a")) assertThat(underTest.temporaryResizeOperation.toIcon).isTrue() } } packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt +8 −21 Original line number Diff line number Diff line Loading @@ -68,29 +68,16 @@ class EditTileListState( return _tiles.indexOfFirst { it is TileGridCell && it.tile.tileSpec == tileSpec } } /** * Whether the tile with this [TileSpec] is currently an icon in the [EditTileListState] * * @return true if the tile is an icon, false if it's large, null if the tile isn't in the list */ fun isIcon(tileSpec: TileSpec): Boolean? { val index = indexOf(tileSpec) return if (index != -1) { val cell = _tiles[index] cell as TileGridCell return cell.isIcon } else { null } } /** Toggle the size of the tile corresponding to the [TileSpec] */ fun toggleSize(tileSpec: TileSpec) { /** Resize the tile corresponding to the [TileSpec] to [toIcon] */ fun resizeTile(tileSpec: TileSpec, toIcon: Boolean) { val fromIndex = indexOf(tileSpec) if (fromIndex != -1) { val cell = _tiles.removeAt(fromIndex) cell as TileGridCell _tiles.add(fromIndex, cell.copy(width = if (cell.isIcon) largeTilesSpan else 1)) val cell = _tiles[fromIndex] as TileGridCell if (cell.isIcon == toIcon) return _tiles.removeAt(fromIndex) _tiles.add(fromIndex, cell.copy(width = if (toIcon) 1 else largeTilesSpan)) regenerateGrid(fromIndex) } } Loading packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +63 −52 Original line number Diff line number Diff line Loading @@ -20,7 +20,6 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn Loading Loading @@ -75,7 +74,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip Loading Loading @@ -124,8 +122,12 @@ import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults 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 import com.android.systemui.qs.panels.ui.compose.selection.TileWidths import com.android.systemui.qs.panels.ui.compose.selection.ResizingState import com.android.systemui.qs.panels.ui.compose.selection.ResizingState.ResizeOperation import com.android.systemui.qs.panels.ui.compose.selection.ResizingState.ResizeOperation.FinalResizeOperation import com.android.systemui.qs.panels.ui.compose.selection.ResizingState.ResizeOperation.TemporaryResizeOperation import com.android.systemui.qs.panels.ui.compose.selection.clearSelectionTile import com.android.systemui.qs.panels.ui.compose.selection.rememberResizingState import com.android.systemui.qs.panels.ui.compose.selection.rememberSelectionState import com.android.systemui.qs.panels.ui.compose.selection.selectableTile import com.android.systemui.qs.panels.ui.model.GridCell Loading @@ -136,10 +138,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.max import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest object TileType Loading Loading @@ -181,15 +183,7 @@ fun DefaultEditTileGrid( onStopEditing: () -> Unit, onReset: (() -> Unit)?, ) { val currentListState by rememberUpdatedState(listState) val selectionState = rememberSelectionState( onResize = { currentListState.toggleSize(it) }, onResizeEnd = { spec -> // Commit the size currently in the list currentListState.isIcon(spec)?.let { onResize(spec, it) } }, ) val selectionState = rememberSelectionState() val reset: (() -> Unit)? = if (onReset != null) { { Loading Loading @@ -349,10 +343,21 @@ private fun CurrentTilesGrid( } .testTag(CURRENT_TILES_GRID_TEST_TAG), ) { EditTiles(cells, columns, listState, selectionState, coroutineScope, largeTilesSpan) { spec -> // Toggle the current size of the tile currentListState.isIcon(spec)?.let { onResize(spec, !it) } EditTiles(cells, columns, listState, selectionState, coroutineScope, largeTilesSpan) { resizingOperation -> when (resizingOperation) { is TemporaryResizeOperation -> { currentListState.resizeTile(resizingOperation.spec, resizingOperation.toIcon) } is FinalResizeOperation -> { // Commit the new size of the tile onResize(resizingOperation.spec, resizingOperation.toIcon) // Mark the selection as automatic in case the tile ends up moving to a // different row with its new size. selectionState.select(resizingOperation.spec, manual = false) } } } } } Loading @@ -373,7 +378,7 @@ private fun AvailableTileGrid( // Available tiles Column( verticalArrangement = spacedBy(CommonTileDefaults.TileArrangementPadding), verticalArrangement = spacedBy(TileArrangementPadding), horizontalAlignment = Alignment.Start, modifier = Modifier.fillMaxWidth().wrapContentHeight().testTag(AVAILABLE_TILES_GRID_TEST_TAG), Loading @@ -387,7 +392,7 @@ private fun AvailableTileGrid( ) tiles.chunked(columns).forEach { row -> Row( horizontalArrangement = spacedBy(CommonTileDefaults.TileArrangementPadding), horizontalArrangement = spacedBy(TileArrangementPadding), modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Max), ) { row.forEachIndexed { index, tileGridCell -> Loading Loading @@ -436,7 +441,7 @@ fun LazyGridScope.EditTiles( selectionState: MutableSelectionState, coroutineScope: CoroutineScope, largeTilesSpan: Int, onToggleSize: (spec: TileSpec) -> Unit, onResize: (operation: ResizeOperation) -> Unit, ) { items( count = cells.size, Loading Loading @@ -464,7 +469,7 @@ fun LazyGridScope.EditTiles( index = index, dragAndDropState = dragAndDropState, selectionState = selectionState, onToggleSize = onToggleSize, onResize = onResize, coroutineScope = coroutineScope, bounceableInfo = cells.bounceableInfo(index, columns), largeTilesSpan = largeTilesSpan, Loading @@ -482,7 +487,7 @@ private fun TileGridCell( index: Int, dragAndDropState: DragAndDropState, selectionState: MutableSelectionState, onToggleSize: (spec: TileSpec) -> Unit, onResize: (operation: ResizeOperation) -> Unit, coroutineScope: CoroutineScope, largeTilesSpan: Int, bounceableInfo: BounceableInfo, Loading Loading @@ -511,28 +516,47 @@ private fun TileGridCell( selected = selectionState.selection?.tileSpec == cell.tile.tileSpec } // Current base, min and max width of this tile var tileWidths: TileWidths? by remember { mutableStateOf(null) } val padding = with(LocalDensity.current) { TileArrangementPadding.roundToPx() } val state = rememberResizingState(cell.tile.tileSpec, cell.isIcon) val progress: () -> Float = { if (selected) { // If selected, return the manual progress from the drag state.progress() } else { // Else, return the target progress for the tile format if (cell.isIcon) 0f else 1f } } if (!selected) { // Update the draggable anchor state when the tile's size is not manually toggled LaunchedEffect(cell.isIcon) { state.updateCurrentValue(cell.isIcon) } } else { // If the tile is selected, listen to new target values from the draggable anchor to toggle // the tile's size LaunchedEffect(state.temporaryResizeOperation) { onResize(state.temporaryResizeOperation) } LaunchedEffect(state.finalResizeOperation) { onResize(state.finalResizeOperation) } } val totalPadding = with(LocalDensity.current) { (largeTilesSpan - 1) * TileArrangementPadding.roundToPx() } ResizableTileContainer( selected = selected, selectionState = selectionState, state = state, selectionAlpha = { selectionAlpha }, selectionColor = selectionColor, tileWidths = { tileWidths }, modifier = modifier .height(TileHeight) .fillMaxWidth() .onSizeChanged { // Grab the size before the bounceable to get the idle width val totalPadding = (largeTilesSpan - 1) * padding val min = if (cell.isIcon) it.width else (it.width - totalPadding) / largeTilesSpan val max = if (cell.isIcon) (it.width * largeTilesSpan) + totalPadding else it.width tileWidths = TileWidths(it.width, min, max) state.updateAnchors(min.toFloat(), max.toFloat()) } .bounceable( bounceable = currentBounceableInfo.bounceable, Loading @@ -552,7 +576,7 @@ private fun TileGridCell( listOf( // TODO(b/367748260): Add final accessibility actions CustomAccessibilityAction("Toggle size") { onToggleSize(cell.tile.tileSpec) onResize(FinalResizeOperation(cell.tile.tileSpec, !cell.isIcon)) true } ) Loading @@ -567,24 +591,7 @@ private fun TileGridCell( ) .tileBackground(colors.background) ) { val targetValue = if (cell.isIcon) 0f else 1f val animatedProgress = remember { Animatable(targetValue) } val resizingState = selectionState.resizingState?.takeIf { selected } LaunchedEffect(targetValue, resizingState) { if (resizingState == null) { animatedProgress.animateTo(targetValue) } else { snapshotFlow { resizingState.progression } .collectLatest { animatedProgress.snapTo(it) } } } EditTile( tile = cell.tile, tileWidths = { tileWidths }, progress = { animatedProgress.value }, ) EditTile(tile = cell.tile, state = state, progress = progress) } } } Loading Loading @@ -651,7 +658,7 @@ private fun SpacerGridCell(modifier: Modifier = Modifier) { @Composable fun EditTile( tile: EditTileViewModel, tileWidths: () -> TileWidths?, state: ResizingState, progress: () -> Float, colors: TileColors = EditModeTileDefaults.editTileColors(), ) { Loading @@ -661,12 +668,16 @@ fun EditTile( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.layout { measurable, constraints -> val (min, max) = state.bounds val currentProgress = progress() // Always display the tile using the large size and trust the parent composable // to clip the content as needed. This stop the labels from being truncated. val width = tileWidths()?.max ?: constraints.maxWidth val width = max?.roundToInt()?.takeIf { it > constraints.maxWidth } ?: constraints.maxWidth val placeable = measurable.measure(constraints.copy(minWidth = width, maxWidth = width)) val currentProgress = progress() val startPadding = if (currentProgress == 0f) { // Find the center of the max width when the tile is icon only Loading @@ -675,7 +686,7 @@ fun EditTile( // Find the center of the minimum width to hold the same position as the // tile is resized. val basePadding = tileWidths()?.min?.let { iconHorizontalCenter(it) } ?: 0f min?.let { iconHorizontalCenter(it.roundToInt()) } ?: 0f // Large tiles, represented with a progress of 1f, have a 0.dp padding basePadding * (1f - currentProgress) } Loading packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt +3 −35 Original line number Diff line number Diff line Loading @@ -27,11 +27,8 @@ import com.android.systemui.qs.pipeline.shared.TileSpec /** Creates the state of the current selected tile that is remembered across compositions. */ @Composable fun rememberSelectionState( onResize: (TileSpec) -> Unit, onResizeEnd: (TileSpec) -> Unit, ): MutableSelectionState { return remember { MutableSelectionState(onResize, onResizeEnd) } fun rememberSelectionState(): MutableSelectionState { return remember { MutableSelectionState() } } /** Loading @@ -41,47 +38,18 @@ fun rememberSelectionState( data class Selection(val tileSpec: TileSpec, val manual: Boolean) /** Holds the state of the current selection. */ class MutableSelectionState( val onResize: (TileSpec) -> Unit, private val onResizeEnd: (TileSpec) -> Unit, ) { class MutableSelectionState { private var _selection = mutableStateOf<Selection?>(null) private var _resizingState = mutableStateOf<ResizingState?>(null) /** The [Selection] if a tile is selected, null if not. */ val selection by _selection /** The [ResizingState] of the selected tile is currently being resized, null if not. */ val resizingState by _resizingState fun select(tileSpec: TileSpec, manual: Boolean) { _selection.value = Selection(tileSpec, manual) } fun unSelect() { _selection.value = null onResizingDragEnd() } fun onResizingDrag(offset: Float) { _resizingState.value?.onDrag(offset) } fun onResizingDragStart(tileWidths: TileWidths) { _selection.value?.let { _resizingState.value = ResizingState(tileWidths) { onResize(it.tileSpec) } } } fun onResizingDragEnd() { _resizingState.value = null _selection.value?.let { onResizeEnd(it.tileSpec) // Mark the selection as automatic in case the tile ends up moving to a different // row with its new size. _selection.value = it.copy(manual = false) } } } Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt +1 −115 Original line number Diff line number Diff line Loading @@ -27,7 +27,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class MutableSelectionStateTest : SysuiTestCase() { private val underTest = MutableSelectionState({}, {}) private val underTest = MutableSelectionState() @Test fun selectTile_isCorrectlySelected() { Loading @@ -48,120 +48,6 @@ class MutableSelectionStateTest : SysuiTestCase() { assertThat(underTest.selection?.manual).isFalse() } @Test fun startResize_createsResizingState() { assertThat(underTest.resizingState).isNull() // Resizing starts but no tile is selected underTest.onResizingDragStart(TileWidths(0, 0, 1)) assertThat(underTest.resizingState).isNull() // Resizing starts with a selected tile underTest.select(TEST_SPEC, manual = true) underTest.onResizingDragStart(TileWidths(0, 0, 1)) assertThat(underTest.resizingState).isNotNull() } @Test fun endResize_clearsResizingState() { val spec = TileSpec.create("testSpec") // Resizing starts with a selected tile underTest.select(spec, manual = true) underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) assertThat(underTest.resizingState).isNotNull() underTest.onResizingDragEnd() assertThat(underTest.resizingState).isNull() } @Test fun unselect_clearsResizingState() { // Resizing starts with a selected tile underTest.select(TEST_SPEC, manual = true) underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) assertThat(underTest.resizingState).isNotNull() underTest.unSelect() assertThat(underTest.resizingState).isNull() } @Test fun onResizingDrag_updatesResizingState() { // Resizing starts with a selected tile underTest.select(TEST_SPEC, manual = true) underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) assertThat(underTest.resizingState).isNotNull() underTest.onResizingDrag(5f) assertThat(underTest.resizingState?.width).isEqualTo(5) underTest.onResizingDrag(2f) assertThat(underTest.resizingState?.width).isEqualTo(7) underTest.onResizingDrag(-6f) assertThat(underTest.resizingState?.width).isEqualTo(1) } @Test fun onResizingDrag_receivesResizeCallback() { var resized = false val onResize: (TileSpec) -> Unit = { assertThat(it).isEqualTo(TEST_SPEC) resized = !resized } val underTest = MutableSelectionState(onResize = onResize, {}) // Resizing starts with a selected tile underTest.select(TEST_SPEC, true) underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) assertThat(underTest.resizingState).isNotNull() // Drag under the threshold underTest.onResizingDrag(1f) assertThat(resized).isFalse() // Drag over the threshold underTest.onResizingDrag(5f) assertThat(resized).isTrue() // Drag back under the threshold underTest.onResizingDrag(-5f) assertThat(resized).isFalse() } @Test fun onResizingEnded_receivesResizeEndCallback() { var resizeEnded = false val onResizeEnd: (TileSpec) -> Unit = { resizeEnded = true } val underTest = MutableSelectionState({}, onResizeEnd = onResizeEnd) // Resizing starts with a selected tile underTest.select(TEST_SPEC, true) underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) underTest.onResizingDragEnd() assertThat(resizeEnded).isTrue() } @Test fun onResizingEnded_setsSelectionAutomatically() { val underTest = MutableSelectionState({}, {}) // Resizing starts with a selected tile underTest.select(TEST_SPEC, manual = true) underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) // Assert the selection was manual assertThat(underTest.selection?.manual).isTrue() underTest.onResizingDragEnd() // Assert the selection is no longer manual due to the resizing assertThat(underTest.selection?.manual).isFalse() } companion object { private val TEST_SPEC = TileSpec.create("testSpec") } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingStateTest.kt +21 −23 Original line number Diff line number Diff line Loading @@ -19,7 +19,9 @@ package com.android.systemui.qs.panels.ui.compose.selection import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.qs.pipeline.shared.TileSpec import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith Loading @@ -27,36 +29,32 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ResizingStateTest : SysuiTestCase() { @Test fun drag_updatesStateCorrectly() { var resized = false val underTest = ResizingState(TileWidths(base = 0, min = 0, max = 10)) { resized = !resized } assertThat(underTest.width).isEqualTo(0) private val underTest = ResizingState(TileSpec.create("a"), startsAsIcon = true).apply { updateAnchors(10f, 20f) } underTest.onDrag(2f) assertThat(underTest.width).isEqualTo(2) underTest.onDrag(1f) assertThat(underTest.width).isEqualTo(3) assertThat(resized).isTrue() @Test fun newResizingState_setInitialValueCorrectly() { assertThat(underTest.anchoredDraggableState.currentValue).isEqualTo(QSDragAnchor.Icon) } underTest.onDrag(-1f) assertThat(underTest.width).isEqualTo(2) assertThat(resized).isFalse() @Test fun updateAnchors_setBoundsCorrectly() { assertThat(underTest.bounds).isEqualTo(10f to 20f) } @Test fun dragOutOfBounds_isClampedCorrectly() { val underTest = ResizingState(TileWidths(base = 0, min = 0, max = 10)) {} fun dragOverThreshold_resizesToLarge() = runTest { underTest.anchoredDraggableState.anchoredDrag { dragTo(16f) } assertThat(underTest.width).isEqualTo(0) assertThat(underTest.temporaryResizeOperation.spec).isEqualTo(TileSpec.create("a")) assertThat(underTest.temporaryResizeOperation.toIcon).isFalse() } underTest.onDrag(100f) assertThat(underTest.width).isEqualTo(10) @Test fun dragUnderThreshold_staysIcon() = runTest { underTest.anchoredDraggableState.anchoredDrag { dragTo(12f) } underTest.onDrag(-200f) assertThat(underTest.width).isEqualTo(0) assertThat(underTest.temporaryResizeOperation.spec).isEqualTo(TileSpec.create("a")) assertThat(underTest.temporaryResizeOperation.toIcon).isTrue() } }
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt +8 −21 Original line number Diff line number Diff line Loading @@ -68,29 +68,16 @@ class EditTileListState( return _tiles.indexOfFirst { it is TileGridCell && it.tile.tileSpec == tileSpec } } /** * Whether the tile with this [TileSpec] is currently an icon in the [EditTileListState] * * @return true if the tile is an icon, false if it's large, null if the tile isn't in the list */ fun isIcon(tileSpec: TileSpec): Boolean? { val index = indexOf(tileSpec) return if (index != -1) { val cell = _tiles[index] cell as TileGridCell return cell.isIcon } else { null } } /** Toggle the size of the tile corresponding to the [TileSpec] */ fun toggleSize(tileSpec: TileSpec) { /** Resize the tile corresponding to the [TileSpec] to [toIcon] */ fun resizeTile(tileSpec: TileSpec, toIcon: Boolean) { val fromIndex = indexOf(tileSpec) if (fromIndex != -1) { val cell = _tiles.removeAt(fromIndex) cell as TileGridCell _tiles.add(fromIndex, cell.copy(width = if (cell.isIcon) largeTilesSpan else 1)) val cell = _tiles[fromIndex] as TileGridCell if (cell.isIcon == toIcon) return _tiles.removeAt(fromIndex) _tiles.add(fromIndex, cell.copy(width = if (toIcon) 1 else largeTilesSpan)) regenerateGrid(fromIndex) } } Loading
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +63 −52 Original line number Diff line number Diff line Loading @@ -20,7 +20,6 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn Loading Loading @@ -75,7 +74,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip Loading Loading @@ -124,8 +122,12 @@ import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults 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 import com.android.systemui.qs.panels.ui.compose.selection.TileWidths import com.android.systemui.qs.panels.ui.compose.selection.ResizingState import com.android.systemui.qs.panels.ui.compose.selection.ResizingState.ResizeOperation import com.android.systemui.qs.panels.ui.compose.selection.ResizingState.ResizeOperation.FinalResizeOperation import com.android.systemui.qs.panels.ui.compose.selection.ResizingState.ResizeOperation.TemporaryResizeOperation import com.android.systemui.qs.panels.ui.compose.selection.clearSelectionTile import com.android.systemui.qs.panels.ui.compose.selection.rememberResizingState import com.android.systemui.qs.panels.ui.compose.selection.rememberSelectionState import com.android.systemui.qs.panels.ui.compose.selection.selectableTile import com.android.systemui.qs.panels.ui.model.GridCell Loading @@ -136,10 +138,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.max import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest object TileType Loading Loading @@ -181,15 +183,7 @@ fun DefaultEditTileGrid( onStopEditing: () -> Unit, onReset: (() -> Unit)?, ) { val currentListState by rememberUpdatedState(listState) val selectionState = rememberSelectionState( onResize = { currentListState.toggleSize(it) }, onResizeEnd = { spec -> // Commit the size currently in the list currentListState.isIcon(spec)?.let { onResize(spec, it) } }, ) val selectionState = rememberSelectionState() val reset: (() -> Unit)? = if (onReset != null) { { Loading Loading @@ -349,10 +343,21 @@ private fun CurrentTilesGrid( } .testTag(CURRENT_TILES_GRID_TEST_TAG), ) { EditTiles(cells, columns, listState, selectionState, coroutineScope, largeTilesSpan) { spec -> // Toggle the current size of the tile currentListState.isIcon(spec)?.let { onResize(spec, !it) } EditTiles(cells, columns, listState, selectionState, coroutineScope, largeTilesSpan) { resizingOperation -> when (resizingOperation) { is TemporaryResizeOperation -> { currentListState.resizeTile(resizingOperation.spec, resizingOperation.toIcon) } is FinalResizeOperation -> { // Commit the new size of the tile onResize(resizingOperation.spec, resizingOperation.toIcon) // Mark the selection as automatic in case the tile ends up moving to a // different row with its new size. selectionState.select(resizingOperation.spec, manual = false) } } } } } Loading @@ -373,7 +378,7 @@ private fun AvailableTileGrid( // Available tiles Column( verticalArrangement = spacedBy(CommonTileDefaults.TileArrangementPadding), verticalArrangement = spacedBy(TileArrangementPadding), horizontalAlignment = Alignment.Start, modifier = Modifier.fillMaxWidth().wrapContentHeight().testTag(AVAILABLE_TILES_GRID_TEST_TAG), Loading @@ -387,7 +392,7 @@ private fun AvailableTileGrid( ) tiles.chunked(columns).forEach { row -> Row( horizontalArrangement = spacedBy(CommonTileDefaults.TileArrangementPadding), horizontalArrangement = spacedBy(TileArrangementPadding), modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Max), ) { row.forEachIndexed { index, tileGridCell -> Loading Loading @@ -436,7 +441,7 @@ fun LazyGridScope.EditTiles( selectionState: MutableSelectionState, coroutineScope: CoroutineScope, largeTilesSpan: Int, onToggleSize: (spec: TileSpec) -> Unit, onResize: (operation: ResizeOperation) -> Unit, ) { items( count = cells.size, Loading Loading @@ -464,7 +469,7 @@ fun LazyGridScope.EditTiles( index = index, dragAndDropState = dragAndDropState, selectionState = selectionState, onToggleSize = onToggleSize, onResize = onResize, coroutineScope = coroutineScope, bounceableInfo = cells.bounceableInfo(index, columns), largeTilesSpan = largeTilesSpan, Loading @@ -482,7 +487,7 @@ private fun TileGridCell( index: Int, dragAndDropState: DragAndDropState, selectionState: MutableSelectionState, onToggleSize: (spec: TileSpec) -> Unit, onResize: (operation: ResizeOperation) -> Unit, coroutineScope: CoroutineScope, largeTilesSpan: Int, bounceableInfo: BounceableInfo, Loading Loading @@ -511,28 +516,47 @@ private fun TileGridCell( selected = selectionState.selection?.tileSpec == cell.tile.tileSpec } // Current base, min and max width of this tile var tileWidths: TileWidths? by remember { mutableStateOf(null) } val padding = with(LocalDensity.current) { TileArrangementPadding.roundToPx() } val state = rememberResizingState(cell.tile.tileSpec, cell.isIcon) val progress: () -> Float = { if (selected) { // If selected, return the manual progress from the drag state.progress() } else { // Else, return the target progress for the tile format if (cell.isIcon) 0f else 1f } } if (!selected) { // Update the draggable anchor state when the tile's size is not manually toggled LaunchedEffect(cell.isIcon) { state.updateCurrentValue(cell.isIcon) } } else { // If the tile is selected, listen to new target values from the draggable anchor to toggle // the tile's size LaunchedEffect(state.temporaryResizeOperation) { onResize(state.temporaryResizeOperation) } LaunchedEffect(state.finalResizeOperation) { onResize(state.finalResizeOperation) } } val totalPadding = with(LocalDensity.current) { (largeTilesSpan - 1) * TileArrangementPadding.roundToPx() } ResizableTileContainer( selected = selected, selectionState = selectionState, state = state, selectionAlpha = { selectionAlpha }, selectionColor = selectionColor, tileWidths = { tileWidths }, modifier = modifier .height(TileHeight) .fillMaxWidth() .onSizeChanged { // Grab the size before the bounceable to get the idle width val totalPadding = (largeTilesSpan - 1) * padding val min = if (cell.isIcon) it.width else (it.width - totalPadding) / largeTilesSpan val max = if (cell.isIcon) (it.width * largeTilesSpan) + totalPadding else it.width tileWidths = TileWidths(it.width, min, max) state.updateAnchors(min.toFloat(), max.toFloat()) } .bounceable( bounceable = currentBounceableInfo.bounceable, Loading @@ -552,7 +576,7 @@ private fun TileGridCell( listOf( // TODO(b/367748260): Add final accessibility actions CustomAccessibilityAction("Toggle size") { onToggleSize(cell.tile.tileSpec) onResize(FinalResizeOperation(cell.tile.tileSpec, !cell.isIcon)) true } ) Loading @@ -567,24 +591,7 @@ private fun TileGridCell( ) .tileBackground(colors.background) ) { val targetValue = if (cell.isIcon) 0f else 1f val animatedProgress = remember { Animatable(targetValue) } val resizingState = selectionState.resizingState?.takeIf { selected } LaunchedEffect(targetValue, resizingState) { if (resizingState == null) { animatedProgress.animateTo(targetValue) } else { snapshotFlow { resizingState.progression } .collectLatest { animatedProgress.snapTo(it) } } } EditTile( tile = cell.tile, tileWidths = { tileWidths }, progress = { animatedProgress.value }, ) EditTile(tile = cell.tile, state = state, progress = progress) } } } Loading Loading @@ -651,7 +658,7 @@ private fun SpacerGridCell(modifier: Modifier = Modifier) { @Composable fun EditTile( tile: EditTileViewModel, tileWidths: () -> TileWidths?, state: ResizingState, progress: () -> Float, colors: TileColors = EditModeTileDefaults.editTileColors(), ) { Loading @@ -661,12 +668,16 @@ fun EditTile( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.layout { measurable, constraints -> val (min, max) = state.bounds val currentProgress = progress() // Always display the tile using the large size and trust the parent composable // to clip the content as needed. This stop the labels from being truncated. val width = tileWidths()?.max ?: constraints.maxWidth val width = max?.roundToInt()?.takeIf { it > constraints.maxWidth } ?: constraints.maxWidth val placeable = measurable.measure(constraints.copy(minWidth = width, maxWidth = width)) val currentProgress = progress() val startPadding = if (currentProgress == 0f) { // Find the center of the max width when the tile is icon only Loading @@ -675,7 +686,7 @@ fun EditTile( // Find the center of the minimum width to hold the same position as the // tile is resized. val basePadding = tileWidths()?.min?.let { iconHorizontalCenter(it) } ?: 0f min?.let { iconHorizontalCenter(it.roundToInt()) } ?: 0f // Large tiles, represented with a progress of 1f, have a 0.dp padding basePadding * (1f - currentProgress) } Loading
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt +3 −35 Original line number Diff line number Diff line Loading @@ -27,11 +27,8 @@ import com.android.systemui.qs.pipeline.shared.TileSpec /** Creates the state of the current selected tile that is remembered across compositions. */ @Composable fun rememberSelectionState( onResize: (TileSpec) -> Unit, onResizeEnd: (TileSpec) -> Unit, ): MutableSelectionState { return remember { MutableSelectionState(onResize, onResizeEnd) } fun rememberSelectionState(): MutableSelectionState { return remember { MutableSelectionState() } } /** Loading @@ -41,47 +38,18 @@ fun rememberSelectionState( data class Selection(val tileSpec: TileSpec, val manual: Boolean) /** Holds the state of the current selection. */ class MutableSelectionState( val onResize: (TileSpec) -> Unit, private val onResizeEnd: (TileSpec) -> Unit, ) { class MutableSelectionState { private var _selection = mutableStateOf<Selection?>(null) private var _resizingState = mutableStateOf<ResizingState?>(null) /** The [Selection] if a tile is selected, null if not. */ val selection by _selection /** The [ResizingState] of the selected tile is currently being resized, null if not. */ val resizingState by _resizingState fun select(tileSpec: TileSpec, manual: Boolean) { _selection.value = Selection(tileSpec, manual) } fun unSelect() { _selection.value = null onResizingDragEnd() } fun onResizingDrag(offset: Float) { _resizingState.value?.onDrag(offset) } fun onResizingDragStart(tileWidths: TileWidths) { _selection.value?.let { _resizingState.value = ResizingState(tileWidths) { onResize(it.tileSpec) } } } fun onResizingDragEnd() { _resizingState.value = null _selection.value?.let { onResizeEnd(it.tileSpec) // Mark the selection as automatic in case the tile ends up moving to a different // row with its new size. _selection.value = it.copy(manual = false) } } } Loading