Loading packages/SystemUI/src/com/android/systemui/grid/ui/compose/SpannedGrids.kt +128 −56 Original line number Diff line number Diff line Loading @@ -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): Loading @@ -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. Loading @@ -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): Loading @@ -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. Loading @@ -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, ) Loading @@ -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" } Loading @@ -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) Loading @@ -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) { Loading Loading @@ -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 { Loading Loading @@ -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) } } packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/BounceableInfo.kt +28 −12 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 { Loading packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt +24 −17 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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() Loading @@ -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, ) } } } } packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +1 −1 Original line number Diff line number Diff line Loading @@ -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( Loading packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +23 −17 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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( Loading Loading
packages/SystemUI/src/com/android/systemui/grid/ui/compose/SpannedGrids.kt +128 −56 Original line number Diff line number Diff line Loading @@ -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): Loading @@ -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. Loading @@ -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): Loading @@ -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. Loading @@ -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, ) Loading @@ -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" } Loading @@ -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) Loading @@ -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) { Loading Loading @@ -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 { Loading Loading @@ -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) } }
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/BounceableInfo.kt +28 −12 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 { Loading
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt +24 −17 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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() Loading @@ -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, ) } } } }
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +1 −1 Original line number Diff line number Diff line Loading @@ -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( Loading
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +23 −17 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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( Loading