Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit af20223c authored by Olivier St-Onge's avatar Olivier St-Onge
Browse files

Migrate to AnchoredDraggable

This handles the drag gesture and "snap" animations as well as fix ome minor issues with the current resizing logic, such as stuck animations when resetting tiles or tile's squishing when dragging fast.

Test: manually resizing tiles and resetting edit mode
Test: ResizingStateTest
Flag: com.android.systemui.qs_ui_refactor_compose_fragment
Bug: 350984160
Change-Id: Iaf4170740dd30da8f0ee7069e17e833394f0e939
parent 9e1bc9ea
Loading
Loading
Loading
Loading
+1 −115
Original line number Diff line number Diff line
@@ -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() {
@@ -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")
    }
+21 −23
Original line number Diff line number Diff line
@@ -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

@@ -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()
    }
}
+8 −21
Original line number Diff line number Diff line
@@ -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)
        }
    }
+63 −52
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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
@@ -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

@@ -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) {
            {
@@ -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)
                }
            }
        }
    }
}
@@ -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),
@@ -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 ->
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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
                            }
                        )
@@ -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)
        }
    }
}
@@ -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(),
) {
@@ -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
@@ -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)
                        }
+3 −35
Original line number Diff line number Diff line
@@ -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() }
}

/**
@@ -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