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

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

Avoid recreating the SnapshotStateList for edit mode at every changes

This helps reduce jank.
As a result, two bugs were uncovered:
- The resizing anchors would be updated with the wrong size values. Using the LazyGridState to calculate the bounds fixes this issue.
- The DragAndDrop API would _sometimes_ stop the tile content from initially composing when a tile is resized rapidly. Applying the DragAndDrop modifier after the first frame fixes this

Test: manually
Test: EditTileListStateTest
Test: ResizingStateTest
Test: ResizingTest
Test: DragAndDropTest
Test: EditModeTest
Flag: com.android.systemui.qs_ui_refactor_compose_fragment
Bug: 405079536
Change-Id: I5931d3a087d965dac1abd18af089b581de5e34b0
parent 043d2389
Loading
Loading
Loading
Loading
+75 −44
Original line number Diff line number Diff line
@@ -21,7 +21,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.Icon
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.selection.PlacementEvent
import com.android.systemui.qs.panels.ui.model.GridCell
@@ -36,23 +35,26 @@ import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
class EditTileListStateTest : SysuiTestCase() {
    private val underTest = EditTileListState(TestEditTiles, columns = 4, largeTilesSpan = 2)
    private val underTest =
        EditTileListState(TestEditTiles, TestLargeTiles, columns = 4, largeTilesSpan = 2)

    @Test
    fun startDrag_listHasSpacers() {
        underTest.onStarted(TestEditTiles[0], DragType.Add)
        val cell = underTest.tiles[0] as TileGridCell
        underTest.onStarted(cell, DragType.Add)

        // [ a ] [ b ] [ c ] [ X ]
        // [ Large D ] [ e ] [ X ]
        assertThat(underTest.tiles.toStrings())
            .isEqualTo(listOf("a", "b", "c", "spacer", "d", "e", "spacer"))
        assertThat(underTest.isMoving(TestEditTiles[0].tile.tileSpec)).isTrue()
        assertThat(underTest.isMoving(cell.tile.tileSpec)).isTrue()
        assertThat(underTest.dragInProgress).isTrue()
    }

    @Test
    fun moveDrag_listChanges() {
        underTest.onStarted(TestEditTiles[4], DragType.Add)
        val cell = underTest.tiles[5] as TileGridCell
        underTest.onStarted(cell, DragType.Add)
        underTest.onTargeting(3, false)

        // Tile E goes to index 3
@@ -64,7 +66,7 @@ class EditTileListStateTest : SysuiTestCase() {

    @Test
    fun moveDragOnSidesOfLargeTile_listChanges() {
        val draggedCell = TestEditTiles[4]
        val draggedCell = underTest.tiles[5] as TileGridCell

        underTest.onStarted(draggedCell, DragType.Add)
        underTest.onTargeting(4, true)
@@ -86,7 +88,7 @@ class EditTileListStateTest : SysuiTestCase() {

    @Test
    fun moveNewTile_tileIsAdded() {
        val newTile = createEditTile("newTile", 2)
        val newTile = SizedTileImpl(createEditTile("newTile"), 2)

        underTest.onStarted(newTile, DragType.Add)
        underTest.onTargeting(5, false)
@@ -103,18 +105,19 @@ class EditTileListStateTest : SysuiTestCase() {

    @Test
    fun movedTileOutOfBounds_tileDisappears() {
        underTest.onStarted(TestEditTiles[0], DragType.Add)
        val cell = underTest.tiles[0] as TileGridCell
        underTest.onStarted(cell, DragType.Add)
        underTest.movedOutOfBounds()

        assertThat(underTest.tiles.toStrings()).doesNotContain(TestEditTiles[0].tile.tileSpec.spec)
        assertThat(underTest.tiles.toStrings()).doesNotContain(TestEditTiles[0].tileSpec.spec)
    }

    @Test
    fun targetIndexForPlacementToTileSpec_returnsCorrectIndex() {
        val placementEvent =
            PlacementEvent.PlaceToTileSpec(
                movingSpec = TestEditTiles[0].tile.tileSpec,
                targetSpec = TestEditTiles[3].tile.tileSpec,
                movingSpec = TestEditTiles[0].tileSpec,
                targetSpec = TestEditTiles[3].tileSpec,
            )
        val index = underTest.targetIndexForPlacement(placementEvent)

@@ -124,19 +127,13 @@ class EditTileListStateTest : SysuiTestCase() {
    @Test
    fun targetIndexForPlacementToIndex_indexOutOfBounds_returnsCorrectIndex() {
        val placementEventTooLow =
            PlacementEvent.PlaceToIndex(
                movingSpec = TestEditTiles[0].tile.tileSpec,
                targetIndex = -1,
            )
            PlacementEvent.PlaceToIndex(movingSpec = TestEditTiles[0].tileSpec, targetIndex = -1)
        val index1 = underTest.targetIndexForPlacement(placementEventTooLow)

        assertThat(index1).isEqualTo(0)

        val placementEventTooHigh =
            PlacementEvent.PlaceToIndex(
                movingSpec = TestEditTiles[0].tile.tileSpec,
                targetIndex = 10,
            )
            PlacementEvent.PlaceToIndex(movingSpec = TestEditTiles[0].tileSpec, targetIndex = 10)
        val index2 = underTest.targetIndexForPlacement(placementEventTooHigh)
        assertThat(index2).isEqualTo(TestEditTiles.size)
    }
@@ -151,10 +148,7 @@ class EditTileListStateTest : SysuiTestCase() {
         * 'e' is now at index 3
         */
        val placementEvent =
            PlacementEvent.PlaceToIndex(
                movingSpec = TestEditTiles[4].tile.tileSpec,
                targetIndex = 3,
            )
            PlacementEvent.PlaceToIndex(movingSpec = TestEditTiles[4].tileSpec, targetIndex = 3)
        val index = underTest.targetIndexForPlacement(placementEvent)

        assertThat(index).isEqualTo(3)
@@ -170,15 +164,54 @@ class EditTileListStateTest : SysuiTestCase() {
         * 'a' is now at index 2
         */
        val placementEvent =
            PlacementEvent.PlaceToIndex(
                movingSpec = TestEditTiles[0].tile.tileSpec,
                targetIndex = 3,
            )
            PlacementEvent.PlaceToIndex(movingSpec = TestEditTiles[0].tileSpec, targetIndex = 3)
        val index = underTest.targetIndexForPlacement(placementEvent)

        assertThat(index).isEqualTo(2)
    }

    @Test
    fun updateTiles_shouldRearrangeGrid() {
        val newTiles =
            listOf(
                createEditTile("1"),
                createEditTile("2"),
                createEditTile("3"),
                createEditTile("4"),
                createEditTile("5"),
                createEditTile("6"),
                createEditTile("7"),
                createEditTile("8"),
            )
        underTest.updateTiles(
            newTiles,
            largeTiles =
                setOf(
                    newTiles[0].tileSpec,
                    newTiles[1].tileSpec,
                    newTiles[6].tileSpec,
                    newTiles[7].tileSpec,
                ),
        )

        // Update should result in
        // [ Large 1 ] [ Large 2 ]
        // [ 3 ] [ 4 ] [ 5 ] [ 6 ]
        // [ Large 7 ] [ Large 8 ]

        assertThat(underTest.tiles)
            .containsExactly(
                TileGridCell(newTiles[0], row = 0, column = 0, width = 2),
                TileGridCell(newTiles[1], row = 0, column = 2, width = 2),
                TileGridCell(newTiles[2], row = 1, column = 0, width = 1),
                TileGridCell(newTiles[3], row = 1, column = 1, width = 1),
                TileGridCell(newTiles[4], row = 1, column = 2, width = 1),
                TileGridCell(newTiles[5], row = 1, column = 3, width = 1),
                TileGridCell(newTiles[6], row = 2, column = 0, width = 2),
                TileGridCell(newTiles[7], row = 2, column = 2, width = 2),
            )
    }

    private fun List<GridCell>.toStrings(): List<String> {
        return map {
            if (it is TileGridCell) {
@@ -190,9 +223,8 @@ class EditTileListStateTest : SysuiTestCase() {
    }

    companion object {
        private fun createEditTile(tileSpec: String, width: Int): SizedTile<EditTileViewModel> {
            return SizedTileImpl(
                EditTileViewModel(
        private fun createEditTile(tileSpec: String): EditTileViewModel {
            return EditTileViewModel(
                tileSpec = TileSpec.create(tileSpec),
                icon = Icon.Resource(0, null),
                label = AnnotatedString("unused"),
@@ -200,8 +232,6 @@ class EditTileListStateTest : SysuiTestCase() {
                isCurrent = true,
                availableEditActions = emptySet(),
                category = TileCategory.UNKNOWN,
                ),
                width,
            )
        }

@@ -209,11 +239,12 @@ class EditTileListStateTest : SysuiTestCase() {
        // [ Large D ] [ e ] [ f ]
        private val TestEditTiles =
            listOf(
                createEditTile("a", 1),
                createEditTile("b", 1),
                createEditTile("c", 1),
                createEditTile("d", 2),
                createEditTile("e", 1),
                createEditTile("a"),
                createEditTile("b"),
                createEditTile("c"),
                createEditTile("d"),
                createEditTile("e"),
            )
        private val TestLargeTiles = setOf(TileSpec.create("d"))
    }
}
+40 −3
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.qs.panels.ui.compose.selection

import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -30,7 +31,14 @@ import org.junit.runner.RunWith
class ResizingStateTest : SysuiTestCase() {

    private val underTest =
        ResizingState(TileSpec.create("a"), startsAsIcon = true).apply { updateAnchors(10f, 20f) }
        ResizingState(TileSpec.create("a"), startsAsIcon = true).apply {
            anchoredDraggableState.updateAnchors(
                DraggableAnchors {
                    QSDragAnchor.Icon at 10f
                    QSDragAnchor.Large at 20f
                }
            )
        }

    @Test
    fun newResizingState_setInitialValueCorrectly() {
@@ -38,8 +46,37 @@ class ResizingStateTest : SysuiTestCase() {
    }

    @Test
    fun updateAnchors_setBoundsCorrectly() {
        assertThat(underTest.bounds).isEqualTo(10f to 20f)
    fun updateAnchors_onIconTile_setBoundsCorrectly() {
        // With an icon tile 10px wide, and a max span of 2 with a tile padding of 5
        underTest.updateAnchors(isIcon = true, 10, maxSpan = 2, padding = 5)

        // New bounds should be: 10 to 25
        // max = 10 * 2 + 5 * (2 - 1)
        assertThat(underTest.bounds).isEqualTo(10f to 25f)

        // With an icon tile 5px wide, and a max span of 10 with a tile padding of 15
        underTest.updateAnchors(isIcon = true, 5, maxSpan = 10, padding = 15)

        // New bounds should be: 5 to 185
        // max = 5 * 10 + 15 * (10 - 1)
        assertThat(underTest.bounds).isEqualTo(5f to 185f)
    }

    @Test
    fun updateAnchors_onLargeTile_setBoundsCorrectly() {
        // With a large tile 60px wide, and a max span of 2 with a tile padding of 20
        underTest.updateAnchors(isIcon = false, 60, maxSpan = 2, padding = 20)

        // New bounds should be: 20 to 60
        // min = (60 - (20 * (2 - 1))) / 2
        assertThat(underTest.bounds).isEqualTo(20f to 60f)

        // With a large tile 35px wide, and a max span of 5 with a tile padding of 2
        underTest.updateAnchors(isIcon = false, 36, maxSpan = 6, padding = 2)

        // New bounds should be: 4 to 36
        // min = (46 - (2 * (6 - 1))) / 6
        assertThat(underTest.bounds).isEqualTo(4f to 36f)
    }

    @Test
+38 −23
Original line number Diff line number Diff line
@@ -16,15 +16,14 @@

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.shared.model.SizedTileImpl
import com.android.systemui.qs.panels.ui.compose.selection.PlacementEvent
import com.android.systemui.qs.panels.ui.model.GridCell
import com.android.systemui.qs.panels.ui.model.TileGridCell
@@ -33,24 +32,14 @@ import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec

/**
 * Creates the edit tile list state that is remembered across compositions.
 *
 * Changes to the tiles or columns will recreate the state.
 * Holds the state for the tiles to display and builds a grid using their sizes and the available
 * columns.
 */
@Composable
fun rememberEditListState(
    tiles: List<SizedTile<EditTileViewModel>>,
    columns: Int,
    largeTilesSpan: Int,
): EditTileListState {
    return remember(tiles, columns) { EditTileListState(tiles, columns, largeTilesSpan) }
}

/** Holds the temporary state of the tile list during a drag movement where we move tiles around. */
class EditTileListState(
    tiles: List<SizedTile<EditTileViewModel>>,
    private val columns: Int,
    private val largeTilesSpan: Int,
    initialTiles: List<EditTileViewModel>,
    initialLargeTiles: Set<TileSpec>,
    val columns: Int,
    val largeTilesSpan: Int,
) : DragAndDropState {
    override var draggedCell by mutableStateOf<SizedTile<EditTileViewModel>?>(null)
        private set
@@ -70,9 +59,18 @@ class EditTileListState(
        get() = draggedCell != null

    private val _tiles: SnapshotStateList<GridCell> =
        tiles.toGridCells(columns).toMutableStateList()
    val tiles: List<GridCell>
        get() = _tiles.toList()
        initialTiles.toGridCells(initialLargeTiles).toMutableStateList()
    val tiles: List<GridCell> = _tiles

    /** Update the grid with this new list of tiles and new set of large tileSpecs. */
    fun updateTiles(tiles: List<EditTileViewModel>, largeTiles: Set<TileSpec>) {
        tiles.toGridCells(largeTiles).let {
            _tiles.apply {
                clear()
                addAll(it)
            }
        }
    }

    fun tileSpecs(): List<TileSpec> {
        return _tiles.filterIsInstance<TileGridCell>().map { it.tile.tileSpec }
@@ -96,8 +94,7 @@ class EditTileListState(

            if (cell.isIcon == toIcon) return

            _tiles.removeAt(fromIndex)
            _tiles.add(fromIndex, cell.copy(width = if (toIcon) 1 else largeTilesSpan))
            _tiles[fromIndex] = cell.copy(width = if (toIcon) 1 else largeTilesSpan)
            regenerateGrid(fromIndex)
        }
    }
@@ -195,6 +192,24 @@ class EditTileListState(
        }
    }

    private fun List<TileGridCell>.updateLargeWidth(
        previousWidth: Int,
        newWidth: Int,
    ): List<TileGridCell> {
        return if (previousWidth != newWidth) {
            map { if (!it.isIcon) it.copy(width = newWidth) else it }
        } else {
            this
        }
    }

    private fun List<EditTileViewModel>.toGridCells(largeTiles: Set<TileSpec>): List<GridCell> {
        return map {
                SizedTileImpl(it, if (largeTiles.contains(it.tileSpec)) largeTilesSpan else 1)
            }
            .toGridCells(columns)
    }

    /** Regenerate the list of [GridCell] with their new potential rows */
    private fun regenerateGrid() {
        _tiles.filterIsInstance<TileGridCell>().toGridCells(columns).let {
+63 −58
Original line number Diff line number Diff line
@@ -59,6 +59,8 @@ import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
@@ -93,6 +95,7 @@ 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
@@ -106,7 +109,6 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
@@ -127,9 +129,9 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastMap
import com.android.compose.gesture.effect.rememberOffsetOverscrollEffectFactory
import com.android.compose.modifiers.height
import com.android.compose.modifiers.thenIf
import com.android.compose.theme.LocalAndroidColorScheme
import com.android.systemui.common.ui.compose.load
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.DragAndDropState
import com.android.systemui.qs.panels.ui.compose.DragType
@@ -170,6 +172,9 @@ import com.android.systemui.res.R
import kotlin.math.abs
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch

object TileType
@@ -228,9 +233,7 @@ private fun EditModeTopBar(onStopEditing: () -> Unit, onReset: (() -> Unit)?) {
@Composable
fun DefaultEditTileGrid(
    listState: EditTileListState,
    otherTiles: List<SizedTile<EditTileViewModel>>,
    columns: Int,
    largeTilesSpan: Int,
    otherTiles: List<EditTileViewModel>,
    modifier: Modifier,
    onAddTile: (TileSpec, Int) -> Unit,
    onRemoveTile: (TileSpec) -> Unit,
@@ -306,15 +309,7 @@ fun DefaultEditTileGrid(
                    modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp),
                )

                CurrentTilesGrid(
                    listState,
                    selectionState,
                    columns,
                    largeTilesSpan,
                    onResize,
                    onRemoveTile,
                    onSetTiles,
                )
                CurrentTilesGrid(listState, selectionState, onResize, onRemoveTile, onSetTiles)

                // Sets a minimum height to be used when available tiles are hidden
                Box(
@@ -349,7 +344,7 @@ fun DefaultEditTileGrid(
                            AvailableTileGrid(
                                availableTiles,
                                selectionState,
                                columns,
                                listState.columns,
                                { onAddTile(it, listState.tileSpecs().size) }, // Add to the end
                                listState,
                            )
@@ -513,8 +508,6 @@ private fun RemoveTileTarget(onClick: () -> Unit) {
private fun CurrentTilesGrid(
    listState: EditTileListState,
    selectionState: MutableSelectionState,
    columns: Int,
    largeTilesSpan: Int,
    onResize: (TileSpec, toIcon: Boolean) -> Unit,
    onRemoveTile: (TileSpec) -> Unit,
    onSetTiles: (List<TileSpec>) -> Unit,
@@ -530,11 +523,10 @@ private fun CurrentTilesGrid(
    var gridContentOffset by remember { mutableStateOf(Offset(0f, 0f)) }
    val coroutineScope = rememberCoroutineScope()

    val cells = listState.tiles
    val primaryColor = MaterialTheme.colorScheme.primary
    TileLazyGrid(
        state = gridState,
        columns = GridCells.Fixed(columns),
        columns = GridCells.Fixed(listState.columns),
        contentPadding = PaddingValues(CurrentTilesGridPadding),
        modifier =
            Modifier.fillMaxWidth()
@@ -561,11 +553,10 @@ private fun CurrentTilesGrid(
                .testTag(CURRENT_TILES_GRID_TEST_TAG),
    ) {
        EditTiles(
            cells = cells,
            dragAndDropState = listState,
            listState = listState,
            selectionState = selectionState,
            gridState = gridState,
            coroutineScope = coroutineScope,
            largeTilesSpan = largeTilesSpan,
            onRemoveTile = onRemoveTile,
        ) { resizingOperation ->
            when (resizingOperation) {
@@ -581,7 +572,6 @@ private fun CurrentTilesGrid(
    }
}

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun AvailableTileGrid(
    tiles: List<AvailableTileGridCell>,
@@ -660,32 +650,30 @@ private fun GridCell.key(index: Int): Any {
/**
 * Adds a list of [GridCell] to the lazy grid
 *
 * @param cells the list of [GridCell]
 * @param dragAndDropState the [DragAndDropState] for this grid
 * @param listState the [EditTileListState] for this grid
 * @param selectionState the [MutableSelectionState] for this grid
 * @param gridState the [LazyGridState] for this grid
 * @param coroutineScope the [CoroutineScope] to be used for the tiles
 * @param largeTilesSpan the width used for large tiles
 * @param onRemoveTile the callback when a tile is removed from this grid
 * @param onResize the callback when a tile has a new [ResizeOperation]
 */
fun LazyGridScope.EditTiles(
    cells: List<GridCell>,
    dragAndDropState: DragAndDropState,
    listState: EditTileListState,
    selectionState: MutableSelectionState,
    gridState: LazyGridState,
    coroutineScope: CoroutineScope,
    largeTilesSpan: Int,
    onRemoveTile: (TileSpec) -> Unit,
    onResize: (operation: ResizeOperation) -> Unit,
) {
    items(
        count = cells.size,
        key = { cells[it].key(it) },
        span = { cells[it].span },
        contentType = { TileType },
    ) { index ->
        when (val cell = cells[index]) {
    itemsIndexed(
        items = listState.tiles,
        key = { index, item -> item.key(index) },
        span = { _, item -> item.span },
        contentType = { _, _ -> TileType },
    ) { index, cell ->
        when (cell) {
            is TileGridCell ->
                if (dragAndDropState.isMoving(cell.tile.tileSpec)) {
                if (listState.isMoving(cell.tile.tileSpec)) {
                    // If the tile is being moved, replace it with a visible spacer
                    SpacerGridCell(
                        Modifier.background(
@@ -700,12 +688,13 @@ fun LazyGridScope.EditTiles(
                    TileGridCell(
                        cell = cell,
                        index = index,
                        dragAndDropState = dragAndDropState,
                        dragAndDropState = listState,
                        selectionState = selectionState,
                        gridState = gridState,
                        onResize = onResize,
                        onRemoveTile = onRemoveTile,
                        coroutineScope = coroutineScope,
                        largeTilesSpan = largeTilesSpan,
                        largeTilesSpan = listState.largeTilesSpan,
                        modifier =
                            Modifier.animateItem(
                                placementSpec =
@@ -749,6 +738,7 @@ private fun TileGridCell(
    index: Int,
    dragAndDropState: DragAndDropState,
    selectionState: MutableSelectionState,
    gridState: LazyGridState,
    onResize: (operation: ResizeOperation) -> Unit,
    onRemoveTile: (TileSpec) -> Unit,
    coroutineScope: CoroutineScope,
@@ -780,8 +770,21 @@ private fun TileGridCell(
        }
    }

    val totalPadding =
        with(LocalDensity.current) { (largeTilesSpan - 1) * TileArrangementPadding.roundToPx() }
    val tilePadding = with(LocalDensity.current) { TileArrangementPadding.roundToPx() }
    LaunchedEffect(gridState) {
        snapshotFlow { gridState.layoutInfo }
            .map { layoutInfo ->
                layoutInfo.visibleItemsInfo
                    .find { it.key == cell.key }
                    ?.let { (it.span == 1) to it.size.width }
            }
            .filterNotNull()
            .distinctUntilChanged()
            .collect { (isIcon, width) ->
                resizingState.updateAnchors(isIcon, width, largeTilesSpan, tilePadding)
            }
    }

    val colors = EditModeTileDefaults.editTileColors()
    val toggleSizeLabel = stringResource(R.string.accessibility_qs_edit_toggle_tile_size_action)
    val togglePlacementModeLabel =
@@ -798,13 +801,7 @@ private fun TileGridCell(
    InteractiveTileContainer(
        tileState = tileState,
        resizingState = resizingState,
        modifier =
            modifier.height(TileHeight).fillMaxWidth().onSizeChanged {
                // Calculate the min/max width from the idle size
                val min = if (cell.isIcon) it.width else (it.width - totalPadding) / largeTilesSpan
                val max = if (cell.isIcon) (it.width * largeTilesSpan) + totalPadding else it.width
                resizingState.updateAnchors(min.toFloat(), max.toFloat())
            },
        modifier = modifier.height(TileHeight).fillMaxWidth(),
        onClick = {
            if (tileState == TileState.Removable) {
                onRemoveTile(cell.tile.tileSpec)
@@ -819,9 +816,22 @@ private fun TileGridCell(
            animateColorAsState(
                if (tileState == TileState.Placeable) placeableColor else colors.background
            )

        // Rapidly composing elements with the draggable modifier can cause visual jank. This
        // usually happens when resizing a tile multiple times. We can fix this by applying the
        // draggable modifier after the first frame
        var isReadyToDrag by remember { mutableStateOf(false) }
        LaunchedEffect(Unit) { isReadyToDrag = true }
        val draggableModifier =
            Modifier.dragAndDropTileSource(
                SizedTileImpl(cell.tile, cell.width),
                dragAndDropState,
                DragType.Move,
                selectionState::unSelect,
            )

        Box(
            modifier
                .fillMaxSize()
            Modifier.fillMaxSize()
                .semantics(mergeDescendants = true) {
                    this.stateDescription = stateDescription
                    contentDescription = cell.tile.label.text
@@ -839,12 +849,7 @@ private fun TileGridCell(
                        )
                }
                .selectableTile(cell.tile.tileSpec, selectionState)
                .dragAndDropTileSource(
                    SizedTileImpl(cell.tile, cell.width),
                    dragAndDropState,
                    DragType.Move,
                    selectionState::unSelect,
                )
                .thenIf(isReadyToDrag) { draggableModifier }
                .tileBackground { backgroundColor }
        ) {
            EditTile(
@@ -1025,11 +1030,11 @@ fun EditTile(

private fun toAvailableTiles(
    currentTiles: List<GridCell>,
    otherTiles: List<SizedTile<EditTileViewModel>>,
    otherTiles: List<EditTileViewModel>,
): List<AvailableTileGridCell> {
    return currentTiles.filterIsInstance<TileGridCell>().fastMap {
        AvailableTileGridCell(it.tile, isAvailable = false)
    } + otherTiles.fastMap { AvailableTileGridCell(it.tile) }
    } + otherTiles.fastMap { AvailableTileGridCell(it) }
}

private fun MeasureScope.iconHorizontalCenter(containerSize: Int): Float {
+13 −16

File changed.

Preview size limit exceeded, changes collapsed.

Loading