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

Commit 8100c292 authored by Olivier St-Onge's avatar Olivier St-Onge Committed by Android (Google) Code Review
Browse files

Merge "Migrate to AnchoredDraggable" into main

parents fb41b374 af20223c
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