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

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

Merge "Add ability for SpannedGrids to provide row and column for each item" into main

parents 8f66c43a b49294ea
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,
                )
            }