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

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

Merge "Add grid scope to SpannegGrids" into main

parents 1ecaf7a6 db21570b
Loading
Loading
Loading
Loading
+128 −56
Original line number Diff line number Diff line
@@ -17,26 +17,40 @@
package com.android.systemui.grid.ui.compose

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.ParentDataModifierNode
import androidx.compose.ui.semantics.CollectionInfo
import androidx.compose.ui.semantics.CollectionItemInfo
import androidx.compose.ui.semantics.collectionInfo
import androidx.compose.ui.semantics.collectionItemInfo
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMapIndexed
import kotlin.math.max

/** Creates a [SpannedGridState] that is remembered across recompositions. */
@Composable
fun rememberSpannedGridState(): SpannedGridState {
    return remember { SpannedGridStateImpl() }
}

/**
 * Horizontal (non lazy) grid that supports [spans] for its elements.
 * Horizontal (non lazy) grid that supports spans for its elements.
 *
 * The elements will be laid down vertically first, and then by columns. So assuming LTR layout, it
 * will be (for a span list `[2, 1, 2, 1, 1, 1, 1, 1]` and 4 rows):
@@ -50,8 +64,11 @@ import kotlin.math.max
 * where repeated numbers show larger span. If an element doesn't fit in a column due to its span,
 * it will start a new column.
 *
 * Elements in [spans] must be in the interval `[1, rows]` ([rows] > 0), and the composables are
 * associated with the corresponding span based on their index.
 * Elements in [composables] can provide their span using [SpannedGridScope.span] and have a default
 * span of 1. Spans must be in the interval `[1, columns]` ([columns] > 0).
 *
 * Passing a [SpannedGridState] can be useful to get access to the [SpannedGridState.positions],
 * representing the row and column of each item.
 *
 * Due to the fact that elements are seen as a linear list that's laid out in a grid, the semantics
 * represent the collection as a list of elements.
@@ -61,23 +78,23 @@ fun HorizontalSpannedGrid(
    rows: Int,
    columnSpacing: Dp,
    rowSpacing: Dp,
    spans: List<Int>,
    modifier: Modifier = Modifier,
    composables: @Composable BoxScope.(spanIndex: Int) -> Unit,
    state: SpannedGridState = rememberSpannedGridState(),
    composables: @Composable SpannedGridScope.() -> Unit,
) {
    SpannedGrid(
        primarySpaces = rows,
        crossAxisSpacing = rowSpacing,
        mainAxisSpacing = columnSpacing,
        spans = spans,
        isVertical = false,
        state = state,
        modifier = modifier,
        composables = composables,
    )
}

/**
 * Horizontal (non lazy) grid that supports [spans] for its elements.
 * Horizontal (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):
@@ -90,8 +107,11 @@ fun HorizontalSpannedGrid(
 * where repeated numbers show larger span. If an element doesn't fit in a row due to its span, it
 * will start a new row.
 *
 * Elements in [spans] must be in the interval `[1, columns]` ([columns] > 0), and the composables
 * are associated with the corresponding span based on their index.
 * Elements in [composables] can provide their span using [SpannedGridScope.span] and have a default
 * span of 1. Spans must be in the interval `[1, columns]` ([columns] > 0).
 *
 * Passing a [SpannedGridState] can be useful to get access to the [SpannedGridState.positions],
 * representing the row and column of each item.
 *
 * Due to the fact that elements are seen as a linear list that's laid out in a grid, the semantics
 * represent the collection as a list of elements.
@@ -101,16 +121,16 @@ fun VerticalSpannedGrid(
    columns: Int,
    columnSpacing: Dp,
    rowSpacing: Dp,
    spans: List<Int>,
    modifier: Modifier = Modifier,
    composables: @Composable BoxScope.(spanIndex: Int) -> Unit,
    state: SpannedGridState = rememberSpannedGridState(),
    composables: @Composable SpannedGridScope.() -> Unit,
) {
    SpannedGrid(
        primarySpaces = columns,
        crossAxisSpacing = columnSpacing,
        mainAxisSpacing = rowSpacing,
        spans = spans,
        isVertical = true,
        state = state,
        modifier = modifier,
        composables = composables,
    )
@@ -121,18 +141,15 @@ private fun SpannedGrid(
    primarySpaces: Int,
    crossAxisSpacing: Dp,
    mainAxisSpacing: Dp,
    spans: List<Int>,
    isVertical: Boolean,
    state: SpannedGridState,
    modifier: Modifier = Modifier,
    composables: @Composable BoxScope.(spanIndex: Int) -> Unit,
    composables: @Composable SpannedGridScope.() -> Unit,
) {
    state as SpannedGridStateImpl
    SideEffect { state.setOrientation(isVertical) }

    val crossAxisArrangement = Arrangement.spacedBy(crossAxisSpacing)
    spans.forEachIndexed { index, span ->
        check(span in 1..primarySpaces) {
            "Span out of bounds. Span at index $index has value of $span which is outside of the " +
                "expected rance of [1, $primarySpaces]"
        }
    }

    if (isVertical) {
        check(crossAxisSpacing >= 0.dp) { "Negative columnSpacing $crossAxisSpacing" }
@@ -142,21 +159,6 @@ private fun SpannedGrid(
        check(crossAxisSpacing >= 0.dp) { "Negative rowSpacing $crossAxisSpacing" }
    }

    val totalMainAxisGroups: Int =
        remember(primarySpaces, spans) {
            var currentAccumulated = 0
            var groups = 1
            spans.forEach { span ->
                if (currentAccumulated + span <= primarySpaces) {
                    currentAccumulated += span
                } else {
                    groups += 1
                    currentAccumulated = span
                }
            }
            groups
        }

    val slotPositionsAndSizesCache = remember {
        object {
            var sizes = IntArray(0)
@@ -165,25 +167,28 @@ private fun SpannedGrid(
    }

    Layout(
        {
            (0 until spans.size).map { spanIndex ->
                Box(
                    Modifier.semantics {
                        collectionItemInfo =
                            if (isVertical) {
                                CollectionItemInfo(spanIndex, 1, 0, 1)
                            } else {
                                CollectionItemInfo(0, 1, spanIndex, 1)
        { SpannedGridScope.composables() },
        modifier.semantics { collectionInfo = CollectionInfo(state.positions.size, 1) },
    ) { measurables, constraints ->
        val spans =
            measurables.fastMapIndexed { index, measurable ->
                measurable.spannedGridParentData.span.also { span ->
                    check(span in 1..primarySpaces) {
                        "Span out of bounds. Span at index $index has value of $span which is " +
                            "outside of the expected rance of [1, $primarySpaces]"
                    }
                }
                ) {
                    composables(spanIndex)
            }
        var totalMainAxisGroups = 1
        var currentAccumulated = 0
        spans.forEach { span ->
            if (currentAccumulated + span <= primarySpaces) {
                currentAccumulated += span
            } else {
                totalMainAxisGroups += 1
                currentAccumulated = span
            }
        }
        },
        modifier.semantics { collectionInfo = CollectionInfo(spans.size, 1) },
    ) { measurables, constraints ->
        check(measurables.size == spans.size)
        val crossAxisSize = if (isVertical) constraints.maxWidth else constraints.maxHeight
        check(crossAxisSize != Constraints.Infinity) { "Width must be constrained" }
        if (slotPositionsAndSizesCache.sizes.size != primarySpaces) {
@@ -275,11 +280,54 @@ private fun SpannedGrid(
                    }
                placeable.placeRelative(x, y)
            }
            state.onPlaceResults(placeables)
        }
    }
}

/** Receiver scope which is used by [VerticalSpannedGrid] and [HorizontalSpannedGrid] */
@Stable
object SpannedGridScope {
    fun Modifier.span(span: Int) = this then SpanElement(span)
}

/** A state object that can be hoisted to observe items positioning */
@Stable
sealed interface SpannedGridState {
    data class Position(val row: Int, val column: Int)

    val positions: List<Position>
}

fun makeConstraint(isVertical: Boolean, mainAxisSize: Int, crossAxisSize: Int): Constraints {
private class SpannedGridStateImpl : SpannedGridState {
    private val _positions = mutableStateListOf<SpannedGridState.Position>()
    override val positions
        get() = _positions

    private var isVertical by mutableStateOf(false)

    fun onPlaceResults(placeResults: List<PlaceResult>) {
        _positions.clear()
        _positions.addAll(
            placeResults.fastMap { placeResult ->
                SpannedGridState.Position(
                    row = if (isVertical) placeResult.mainAxisGroup else placeResult.slotIndex,
                    column = if (isVertical) placeResult.slotIndex else placeResult.mainAxisGroup,
                )
            }
        )
    }

    fun setOrientation(isVertical: Boolean) {
        this.isVertical = isVertical
    }
}

private fun makeConstraint(
    isVertical: Boolean,
    mainAxisSize: Int,
    crossAxisSize: Int,
): Constraints {
    return if (isVertical) {
        Constraints(maxHeight = mainAxisSize, minWidth = crossAxisSize, maxWidth = crossAxisSize)
    } else {
@@ -319,3 +367,27 @@ private data class PlaceResult(
    val slotIndex: Int,
    val mainAxisGroup: Int,
)

private val IntrinsicMeasurable.spannedGridParentData: SpannedGridParentData?
    get() = parentData as? SpannedGridParentData

private val SpannedGridParentData?.span: Int
    get() = this?.span ?: 1

private data class SpannedGridParentData(val span: Int = 1)

private data class SpanElement(val span: Int) : ModifierNodeElement<SpanNode>() {
    override fun create(): SpanNode {
        return SpanNode(span)
    }

    override fun update(node: SpanNode) {
        node.span = span
    }
}

private class SpanNode(var span: Int) : ParentDataModifierNode, Modifier.Node() {
    override fun Density.modifyParentData(parentData: Any?): Any? {
        return ((parentData as? SpannedGridParentData) ?: SpannedGridParentData()).copy(span = span)
    }
}
+28 −12
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.qs.panels.ui.compose

import com.android.compose.animation.Bounceable
import com.android.systemui.grid.ui.compose.SpannedGridState
import com.android.systemui.qs.panels.shared.model.SizedTile
import com.android.systemui.qs.panels.ui.model.GridCell
import com.android.systemui.qs.panels.ui.model.TileGridCell
@@ -37,22 +38,37 @@ fun List<Pair<GridCell, BounceableTileViewModel>>.bounceableInfo(
    val cell = this[index].first as TileGridCell
    // Only look for neighbor bounceables if they are on the same row
    val onLastColumn = cell.onLastColumn(cell.column, columns)
    val previousTile = getOrNull(index - 1)?.takeIf { cell.column != 0 }
    val nextTile = getOrNull(index + 1)?.takeIf { !onLastColumn }
    val previousTile = getOrNull(index - 1)?.takeIf { it.first.row == cell.row }
    val nextTile = getOrNull(index + 1)?.takeIf { it.first.row == cell.row }
    return BounceableInfo(this[index].second, previousTile?.second, nextTile?.second, !onLastColumn)
}

fun List<BounceableTileViewModel>.bounceableInfo(
    sizedTile: SizedTile<TileViewModel>,
    index: Int,
    column: Int,
inline fun List<SizedTile<TileViewModel>>.forEachWithBounceables(
    positions: List<SpannedGridState.Position>,
    bounceables: List<BounceableTileViewModel>,
    columns: Int,
): BounceableInfo {
    // Only look for neighbor bounceables if they are on the same row
    val onLastColumn = sizedTile.onLastColumn(column, columns)
    val previousTile = getOrNull(index - 1)?.takeIf { column != 0 }
    val nextTile = getOrNull(index + 1)?.takeIf { !onLastColumn }
    return BounceableInfo(this[index], previousTile, nextTile, !onLastColumn)
    action: (index: Int, tile: SizedTile<TileViewModel>, bounceableInfo: BounceableInfo) -> Unit,
) {
    this.forEachIndexed { index, tile ->
        val position = positions.getOrNull(index)
        val onLastColumn = position?.column == columns - tile.width
        val previousBounceable =
            bounceables.getOrNull(index - 1)?.takeIf {
                position != null && positions.getOrNull(index - 1)?.row == position.row
            }
        val nextBounceable =
            bounceables.getOrNull(index + 1)?.takeIf {
                position != null && positions.getOrNull(index + 1)?.row == position.row
            }
        val bounceableInfo =
            BounceableInfo(
                bounceable = bounceables[index],
                previousTile = previousBounceable,
                nextTile = nextBounceable,
                bounceEnd = !onLastColumn,
            )
        action(index, tile, bounceableInfo)
    }
}

private fun <T> SizedTile<T>.onLastColumn(column: Int, columns: Int): Boolean {
+24 −17
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.scene.SceneScope
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.grid.ui.compose.VerticalSpannedGrid
import com.android.systemui.grid.ui.compose.rememberSpannedGridState
import com.android.systemui.qs.composefragment.ui.GridAnchor
import com.android.systemui.qs.panels.ui.compose.infinitegrid.Tile
import com.android.systemui.qs.panels.ui.viewmodel.BounceableTileViewModel
@@ -47,6 +48,8 @@ fun SceneScope.QuickQuickSettings(
    val bounceables = remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } }
    val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle()
    val scope = rememberCoroutineScope()
    val gridState = rememberSpannedGridState()
    val positions = gridState.positions

    DisposableEffect(tiles) {
        val token = Any()
@@ -54,30 +57,34 @@ fun SceneScope.QuickQuickSettings(
        onDispose { tiles.forEach { it.stopListening(token) } }
    }
    val columns = viewModel.columns
    var cellIndex = 0
    Box(modifier = modifier) {
        GridAnchor()
        VerticalSpannedGrid(
            columns = columns,
            columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal),
            rowSpacing = dimensionResource(R.dimen.qs_tile_margin_vertical),
            spans = sizedTiles.fastMap { it.width },
            state = gridState,
            modifier = Modifier.sysuiResTag("qqs_tile_layout"),
        ) { spanIndex ->
            val it = sizedTiles[spanIndex]
            val column = cellIndex % columns
            cellIndex += it.width
        ) {
            sizedTiles.forEachWithBounceables(positions, bounceables, columns) {
                index,
                sizedTile,
                bounceableInfo ->
                Tile(
                tile = it.tile,
                iconOnly = it.isIcon,
                modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)),
                    tile = sizedTile.tile,
                    iconOnly = sizedTile.isIcon,
                    modifier =
                        Modifier.element(sizedTile.tile.spec.toElementKey(index))
                            .span(sizedTile.width),
                    squishiness = { squishiness },
                    coroutineScope = scope,
                bounceableInfo = bounceables.bounceableInfo(it, spanIndex, column, columns),
                tileHapticsViewModelFactoryProvider = viewModel.tileHapticsViewModelFactoryProvider,
                    bounceableInfo = bounceableInfo,
                    tileHapticsViewModelFactoryProvider =
                        viewModel.tileHapticsViewModelFactoryProvider,
                    // There should be no QuickQuickSettings when the details view is enabled.
                    detailsViewModel = null,
                )
            }
        }
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -395,7 +395,7 @@ private fun CurrentTilesGrid(

    val cells =
        remember(listState.tiles) {
            listState.tiles.fastMap { Pair(it, BounceableTileViewModel()) }
            listState.tiles.fastMap { tile -> Pair(tile, BounceableTileViewModel()) }
        }

    TileLazyGrid(
+23 −17
Original line number Diff line number Diff line
@@ -23,17 +23,18 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.util.fastMap
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.scene.SceneScope
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.grid.ui.compose.VerticalSpannedGrid
import com.android.systemui.grid.ui.compose.rememberSpannedGridState
import com.android.systemui.haptics.msdl.qs.TileHapticsViewModelFactoryProvider
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QS
import com.android.systemui.qs.panels.shared.model.SizedTileImpl
import com.android.systemui.qs.panels.ui.compose.PaginatableGridLayout
import com.android.systemui.qs.panels.ui.compose.bounceableInfo
import com.android.systemui.qs.panels.ui.compose.forEachWithBounceables
import com.android.systemui.qs.panels.ui.compose.rememberEditListState
import com.android.systemui.qs.panels.ui.viewmodel.BounceableTileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.DetailsViewModel
@@ -83,29 +84,34 @@ constructor(
            remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } }
        val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle()
        val scope = rememberCoroutineScope()
        var cellIndex = 0
        val gridState = rememberSpannedGridState()
        val positions = gridState.positions

        VerticalSpannedGrid(
            columns = columns,
            state = gridState,
            columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal),
            rowSpacing = dimensionResource(R.dimen.qs_tile_margin_vertical),
            spans = sizedTiles.fastMap { it.width },
        ) { spanIndex ->
            val it = sizedTiles[spanIndex]
            val column = cellIndex % columns
            cellIndex += it.width
        ) {
            sizedTiles.forEachWithBounceables(positions, bounceables, columns) {
                index,
                sizedTile,
                bounceableInfo ->
                Tile(
                tile = it.tile,
                iconOnly = iconTilesViewModel.isIconTile(it.tile.spec),
                modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)),
                    tile = sizedTile.tile,
                    iconOnly = iconTilesViewModel.isIconTile(sizedTile.tile.spec),
                    modifier =
                        Modifier.element(sizedTile.tile.spec.toElementKey(index))
                            .span(sizedTile.width),
                    squishiness = { squishiness },
                    tileHapticsViewModelFactoryProvider = tileHapticsViewModelFactoryProvider,
                    coroutineScope = scope,
                bounceableInfo = bounceables.bounceableInfo(it, spanIndex, column, columns),
                    bounceableInfo = bounceableInfo,
                    detailsViewModel = detailsViewModel,
                )
            }
        }
    }

    @Composable
    override fun EditTileGrid(