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

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

Add ability for SpannedGrids to provide row and column for each item

This fixes a bug with QS where rows wouldn't be calculated properly for the bounce effect

Flag: com.android.systemui.qs_ui_refactor_compose_fragment
Test: manually
Test: SpannedGridScreenshotTest
Fixes: 379309901
Change-Id: I3f8956467fe09696c5308b684632a8a57733670b
parent 6f022908
Loading
Loading
Loading
Loading
+77 −64
Original line number Diff line number Diff line
@@ -13,9 +13,9 @@
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.grid.ui.compose

import androidx.collection.IntIntPair
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
@@ -24,7 +24,6 @@ import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.semantics.CollectionInfo
import androidx.compose.ui.semantics.CollectionItemInfo
import androidx.compose.ui.semantics.collectionInfo
@@ -34,6 +33,8 @@ import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import androidx.compose.ui.util.fastMapIndexed
import kotlin.math.max

/**
@@ -65,7 +66,11 @@ fun HorizontalSpannedGrid(
    spans: List<Int>,
    modifier: Modifier = Modifier,
    keys: (spanIndex: Int) -> Any = { it },
    composables: @Composable BoxScope.(spanIndex: Int) -> Unit,
    composables:
        @Composable
        BoxScope.(
            spanIndex: Int, row: Int, isFirstInColumn: Boolean, isLastInColumn: Boolean,
        ) -> Unit,
) {
    SpannedGrid(
        primarySpaces = rows,
@@ -80,7 +85,7 @@ fun HorizontalSpannedGrid(
}

/**
 * Horizontal (non lazy) grid that supports [spans] for its elements.
 * Vertical (non lazy) grid that supports [spans] for its elements.
 *
 * The elements will be laid down horizontally first, and then by rows. So assuming LTR layout, it
 * will be (for a span list `[2, 1, 2, 1, 1, 1, 1, 1]` and 4 columns):
@@ -107,7 +112,9 @@ fun VerticalSpannedGrid(
    spans: List<Int>,
    modifier: Modifier = Modifier,
    keys: (spanIndex: Int) -> Any = { it },
    composables: @Composable BoxScope.(spanIndex: Int) -> Unit,
    composables:
        @Composable
        BoxScope.(spanIndex: Int, column: Int, isFirstInRow: Boolean, isLastInRow: Boolean) -> Unit,
) {
    SpannedGrid(
        primarySpaces = columns,
@@ -130,7 +137,9 @@ private fun SpannedGrid(
    isVertical: Boolean,
    modifier: Modifier = Modifier,
    keys: (spanIndex: Int) -> Any = { it },
    composables: @Composable BoxScope.(spanIndex: Int) -> Unit,
    composables:
        @Composable
        BoxScope.(spanIndex: Int, secondaryAxis: Int, isFirst: Boolean, isLast: Boolean) -> Unit,
) {
    val crossAxisArrangement = Arrangement.spacedBy(crossAxisSpacing)
    spans.forEachIndexed { index, span ->
@@ -139,7 +148,6 @@ private fun SpannedGrid(
                "expected rance of [1, $primarySpaces]"
        }
    }

    if (isVertical) {
        check(crossAxisSpacing >= 0.dp) { "Negative columnSpacing $crossAxisSpacing" }
        check(mainAxisSpacing >= 0.dp) { "Negative rowSpacing $mainAxisSpacing" }
@@ -147,29 +155,30 @@ private fun SpannedGrid(
        check(mainAxisSpacing >= 0.dp) { "Negative columnSpacing $mainAxisSpacing" }
        check(crossAxisSpacing >= 0.dp) { "Negative rowSpacing $crossAxisSpacing" }
    }

    val totalMainAxisGroups: Int =
    // List of primary axis index to secondary axis index
    // This is keyed to the size of the spans list for performance reasons as we don't expect the
    // spans value to change outside of edit mode.
    val positions = remember(spans.size) { Array(spans.size) { IntIntPair(0, 0) } }
    val totalMainAxisGroups =
        remember(primarySpaces, spans) {
            var currentAccumulated = 0
            var groups = 1
            spans.forEach { span ->
                if (currentAccumulated + span <= primarySpaces) {
                    currentAccumulated += span
                } else {
                    groups += 1
                    currentAccumulated = span
            var mainAxisGroup = 0
            var currentSlot = 0
            spans.fastForEachIndexed { index, span ->
                if (currentSlot + span > primarySpaces) {
                    currentSlot = 0
                    mainAxisGroup += 1
                }
                positions[index] = IntIntPair(mainAxisGroup, currentSlot)
                currentSlot += span
            }
            groups
            mainAxisGroup + 1
        }

    val slotPositionsAndSizesCache = remember {
        object {
            var sizes = IntArray(0)
            var positions = IntArray(0)
        }
    }

    Layout(
        {
            (0 until spans.size).map { spanIndex ->
@@ -184,7 +193,13 @@ private fun SpannedGrid(
                                }
                        }
                    ) {
                        composables(spanIndex)
                        val position = positions[spanIndex]
                        composables(
                            spanIndex,
                            position.second,
                            position.second == 0,
                            positions.getOrNull(spanIndex + 1)?.first != position.first,
                        )
                    }
                }
            }
@@ -205,7 +220,6 @@ private fun SpannedGrid(
            slotPositionsAndSizesCache.sizes,
        )
        val cellSizesInCrossAxis = slotPositionsAndSizesCache.sizes

        // with is needed because of the double receiver (Density, Arrangement).
        with(crossAxisArrangement) {
            arrange(
@@ -216,68 +230,73 @@ private fun SpannedGrid(
            )
        }
        val startPositions = slotPositionsAndSizesCache.positions

        val mainAxisSpacingPx = mainAxisSpacing.roundToPx()
        val mainAxisTotalGaps = (totalMainAxisGroups - 1) * mainAxisSpacingPx
        val mainAxisSize = if (isVertical) constraints.maxHeight else constraints.maxWidth
        val mainAxisMaxSize = if (isVertical) constraints.maxHeight else constraints.maxWidth
        val mainAxisElementConstraint =
            if (mainAxisSize == Constraints.Infinity) {
            if (mainAxisMaxSize == Constraints.Infinity) {
                Constraints.Infinity
            } else {
                max(0, (mainAxisSize - mainAxisTotalGaps) / totalMainAxisGroups)
                max(0, (mainAxisMaxSize - mainAxisTotalGaps) / totalMainAxisGroups)
            }

        val mainAxisSizes = IntArray(totalMainAxisGroups) { 0 }

        var currentSlot = 0
        var mainAxisGroup = 0
        var mainAxisTotalSize = mainAxisTotalGaps
        var currentMainAxis = 0
        var currentMainAxisMax = 0
        val placeables =
            measurables.mapIndexed { index, measurable ->
            measurables.fastMapIndexed { index, measurable ->
                val span = spans[index]
                if (currentSlot + span > primarySpaces) {
                    currentSlot = 0
                    mainAxisGroup += 1
                }
                val position = positions[index]
                val crossAxisConstraint =
                    calculateWidth(cellSizesInCrossAxis, startPositions, currentSlot, span)
                PlaceResult(
                        measurable.measure(
                            makeConstraint(
                                isVertical,
                                mainAxisElementConstraint,
                                crossAxisConstraint,
                            )
                        ),
                        currentSlot,
                        mainAxisGroup,
                    calculateWidth(cellSizesInCrossAxis, startPositions, position.second, span)

                measurable
                    .measure(
                        makeConstraint(isVertical, mainAxisElementConstraint, crossAxisConstraint)
                    )
                    .also {
                        currentSlot += span
                        mainAxisSizes[mainAxisGroup] =
                            max(
                                mainAxisSizes[mainAxisGroup],
                                if (isVertical) it.placeable.height else it.placeable.width,
                            )
                        val placeableSize = if (isVertical) it.height else it.width
                        if (position.first != currentMainAxis) {
                            // New row -- Add the max size to the total and reset the max
                            mainAxisTotalSize += currentMainAxisMax
                            currentMainAxisMax = placeableSize
                            currentMainAxis = position.first
                        } else {
                            currentMainAxisMax = max(currentMainAxisMax, placeableSize)
                        }
                    }
            }
        mainAxisTotalSize += currentMainAxisMax

        val mainAxisTotalSize = mainAxisTotalGaps + mainAxisSizes.sum()
        val mainAxisStartingPoints =
            mainAxisSizes.runningFold(0) { acc, value -> acc + value + mainAxisSpacingPx }
        val height = if (isVertical) mainAxisTotalSize else crossAxisSize
        val width = if (isVertical) crossAxisSize else mainAxisTotalSize

        layout(width, height) {
            placeables.forEach { (placeable, slot, mainAxisGroup) ->
            var previousMainAxis = 0
            var currentMainAxisPosition = 0
            var currentMainAxisMax = 0
            placeables.forEachIndexed { index, placeable ->
                val slot = positions[index].second
                val mainAxisSize = if (isVertical) placeable.height else placeable.width

                if (positions[index].first != previousMainAxis) {
                    // Move up a row + padding
                    currentMainAxisPosition += currentMainAxisMax + mainAxisSpacingPx
                    currentMainAxisMax = mainAxisSize
                    previousMainAxis = positions[index].first
                } else {
                    currentMainAxisMax = max(currentMainAxisMax, mainAxisSize)
                }

                val x =
                    if (isVertical) {
                        startPositions[slot]
                    } else {
                        mainAxisStartingPoints[mainAxisGroup]
                        currentMainAxisPosition
                    }
                val y =
                    if (isVertical) {
                        mainAxisStartingPoints[mainAxisGroup]
                        currentMainAxisPosition
                    } else {
                        startPositions[slot]
                    }
@@ -321,9 +340,3 @@ private fun calculateCellsCrossAxisSize(
        outArray[index] = slotSize + if (index < remainingPixels) 1 else 0
    }
}

private data class PlaceResult(
    val placeable: Placeable,
    val slotIndex: Int,
    val mainAxisGroup: Int,
)
+8 −3
Original line number Diff line number Diff line
@@ -35,11 +35,16 @@ fun List<BounceableTileViewModel>.bounceableInfo(
    index: Int,
    column: Int,
    columns: Int,
    isFirstInRow: Boolean,
    isLastInRow: Boolean,
): BounceableInfo {
    // Only look for neighbor bounceables if they are on the same row
    // A tile may be the last in the row without being on the last column
    val onLastColumn = sizedTile.onLastColumn(column, columns)
    val previousTile = getOrNull(index - 1)?.takeIf { column != 0 }
    val nextTile = getOrNull(index + 1)?.takeIf { !onLastColumn }

    // Only look for neighbor bounceables if they are on the same row
    val previousTile = getOrNull(index - 1)?.takeIf { !isFirstInRow }
    val nextTile = getOrNull(index + 1)?.takeIf { !isLastInRow }

    return BounceableInfo(this[index], previousTile, nextTile, !onLastColumn)
}

+10 −5
Original line number Diff line number Diff line
@@ -57,7 +57,6 @@ fun ContentScope.QuickQuickSettings(
        onDispose { tiles.forEach { it.stopListening(token) } }
    }
    val columns = viewModel.columns
    var cellIndex = 0
    Box(modifier = modifier) {
        GridAnchor()
        VerticalSpannedGrid(
@@ -67,17 +66,23 @@ fun ContentScope.QuickQuickSettings(
            spans = spans,
            modifier = Modifier.sysuiResTag("qqs_tile_layout"),
            keys = { sizedTiles[it].tile.spec },
        ) { spanIndex ->
        ) { spanIndex, column, isFirstInColumn, isLastInColumn ->
            val it = sizedTiles[spanIndex]
            val column = cellIndex % columns
            cellIndex += it.width
            Element(it.tile.spec.toElementKey(spanIndex), Modifier) {
                Tile(
                    tile = it.tile,
                    iconOnly = it.isIcon,
                    squishiness = { squishiness },
                    coroutineScope = scope,
                    bounceableInfo = bounceables.bounceableInfo(it, spanIndex, column, columns),
                    bounceableInfo =
                        bounceables.bounceableInfo(
                            it,
                            index = spanIndex,
                            column = column,
                            columns = columns,
                            isFirstInRow = isFirstInColumn,
                            isLastInRow = isLastInColumn,
                        ),
                    tileHapticsViewModelFactoryProvider =
                        viewModel.tileHapticsViewModelFactoryProvider,
                    // There should be no QuickQuickSettings when the details view is enabled.
+11 −6
Original line number Diff line number Diff line
@@ -85,8 +85,6 @@ constructor(
            remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } }
        val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle()
        val scope = rememberCoroutineScope()
        var cellIndex = 0

        val spans by remember(sizedTiles) { derivedStateOf { sizedTiles.fastMap { it.width } } }

        VerticalSpannedGrid(
@@ -95,10 +93,9 @@ constructor(
            rowSpacing = dimensionResource(R.dimen.qs_tile_margin_vertical),
            spans = spans,
            keys = { sizedTiles[it].tile.spec },
        ) { spanIndex ->
        ) { spanIndex, column, isFirstInColumn, isLastInColumn ->
            val it = sizedTiles[spanIndex]
            val column = cellIndex % columns
            cellIndex += it.width

            Element(it.tile.spec.toElementKey(spanIndex), Modifier) {
                Tile(
                    tile = it.tile,
@@ -106,7 +103,15 @@ constructor(
                    squishiness = { squishiness },
                    tileHapticsViewModelFactoryProvider = tileHapticsViewModelFactoryProvider,
                    coroutineScope = scope,
                    bounceableInfo = bounceables.bounceableInfo(it, spanIndex, column, columns),
                    bounceableInfo =
                        bounceables.bounceableInfo(
                            it,
                            index = spanIndex,
                            column = column,
                            columns = columns,
                            isFirstInRow = isFirstInColumn,
                            isLastInRow = isLastInColumn,
                        ),
                    detailsViewModel = detailsViewModel,
                )
            }