• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.systemui.grid.ui.compose
17 
18 import androidx.collection.IntIntPair
19 import androidx.compose.foundation.layout.Arrangement
20 import androidx.compose.foundation.layout.Box
21 import androidx.compose.foundation.layout.BoxScope
22 import androidx.compose.runtime.Composable
23 import androidx.compose.runtime.key
24 import androidx.compose.runtime.remember
25 import androidx.compose.ui.Modifier
26 import androidx.compose.ui.layout.Layout
27 import androidx.compose.ui.semantics.CollectionInfo
28 import androidx.compose.ui.semantics.CollectionItemInfo
29 import androidx.compose.ui.semantics.collectionInfo
30 import androidx.compose.ui.semantics.collectionItemInfo
31 import androidx.compose.ui.semantics.semantics
32 import androidx.compose.ui.unit.Constraints
33 import androidx.compose.ui.unit.Dp
34 import androidx.compose.ui.unit.LayoutDirection
35 import androidx.compose.ui.unit.dp
36 import androidx.compose.ui.util.fastForEachIndexed
37 import androidx.compose.ui.util.fastMapIndexed
38 import kotlin.math.max
39 
40 /**
41  * Horizontal (non lazy) grid that supports [spans] for its elements.
42  *
43  * The elements will be laid down vertically first, and then by columns. So assuming LTR layout, it
44  * will be (for a span list `[2, 1, 2, 1, 1, 1, 1, 1]` and 4 rows):
45  * ```
46  * 0  2  5
47  * 0  2  6
48  * 1  3  7
49  *    4
50  * ```
51  *
52  * where repeated numbers show larger span. If an element doesn't fit in a column due to its span,
53  * it will start a new column.
54  *
55  * Elements in [spans] must be in the interval `[1, rows]` ([rows] > 0), and the composables are
56  * associated with the corresponding span based on their index.
57  *
58  * Due to the fact that elements are seen as a linear list that's laid out in a grid, the semantics
59  * represent the collection as a list of elements.
60  */
61 @Composable
62 fun HorizontalSpannedGrid(
63     rows: Int,
64     columnSpacing: Dp,
65     rowSpacing: Dp,
66     spans: List<Int>,
67     modifier: Modifier = Modifier,
68     keys: (spanIndex: Int) -> Any = { it },
69     composables:
70         @Composable
71         BoxScope.(
72             spanIndex: Int, row: Int, isFirstInColumn: Boolean, isLastInColumn: Boolean,
73         ) -> Unit,
74 ) {
75     SpannedGrid(
76         primarySpaces = rows,
77         crossAxisSpacing = rowSpacing,
78         mainAxisSpacing = columnSpacing,
79         spans = spans,
80         isVertical = false,
81         modifier = modifier,
82         keys = keys,
83         composables = composables,
84     )
85 }
86 
87 /**
88  * Vertical (non lazy) grid that supports [spans] for its elements.
89  *
90  * The elements will be laid down horizontally first, and then by rows. So assuming LTR layout, it
91  * will be (for a span list `[2, 1, 2, 1, 1, 1, 1, 1]` and 4 columns):
92  * ```
93  * 0  0  1
94  * 2  2  3  4
95  * 5  6  7
96  * ```
97  *
98  * where repeated numbers show larger span. If an element doesn't fit in a row due to its span, it
99  * will start a new row.
100  *
101  * Elements in [spans] must be in the interval `[1, columns]` ([columns] > 0), and the composables
102  * are associated with the corresponding span based on their index.
103  *
104  * Due to the fact that elements are seen as a linear list that's laid out in a grid, the semantics
105  * represent the collection as a list of elements.
106  */
107 @Composable
VerticalSpannedGridnull108 fun VerticalSpannedGrid(
109     columns: Int,
110     columnSpacing: Dp,
111     rowSpacing: Dp,
112     spans: List<Int>,
113     modifier: Modifier = Modifier,
114     keys: (spanIndex: Int) -> Any = { it },
115     composables:
116         @Composable
117         BoxScope.(spanIndex: Int, column: Int, isFirstInRow: Boolean, isLastInRow: Boolean) -> Unit,
118 ) {
119     SpannedGrid(
120         primarySpaces = columns,
121         crossAxisSpacing = columnSpacing,
122         mainAxisSpacing = rowSpacing,
123         spans = spans,
124         isVertical = true,
125         modifier = modifier,
126         keys = keys,
127         composables = composables,
128     )
129 }
130 
131 @Composable
SpannedGridnull132 private fun SpannedGrid(
133     primarySpaces: Int,
134     crossAxisSpacing: Dp,
135     mainAxisSpacing: Dp,
136     spans: List<Int>,
137     isVertical: Boolean,
138     modifier: Modifier = Modifier,
139     keys: (spanIndex: Int) -> Any = { it },
140     composables:
141         @Composable
142         BoxScope.(spanIndex: Int, secondaryAxis: Int, isFirst: Boolean, isLast: Boolean) -> Unit,
143 ) {
144     val crossAxisArrangement = Arrangement.spacedBy(crossAxisSpacing)
indexnull145     spans.forEachIndexed { index, span ->
146         check(span in 1..primarySpaces) {
147             "Span out of bounds. Span at index $index has value of $span which is outside of the " +
148                 "expected rance of [1, $primarySpaces]"
149         }
150     }
151     if (isVertical) {
<lambda>null152         check(crossAxisSpacing >= 0.dp) { "Negative columnSpacing $crossAxisSpacing" }
<lambda>null153         check(mainAxisSpacing >= 0.dp) { "Negative rowSpacing $mainAxisSpacing" }
154     } else {
<lambda>null155         check(mainAxisSpacing >= 0.dp) { "Negative columnSpacing $mainAxisSpacing" }
<lambda>null156         check(crossAxisSpacing >= 0.dp) { "Negative rowSpacing $crossAxisSpacing" }
157     }
158     // List of primary axis index to secondary axis index
159     // This is keyed to the size of the spans list for performance reasons as we don't expect the
160     // spans value to change outside of edit mode.
<lambda>null161     val positions = remember(spans.size) { Array(spans.size) { IntIntPair(0, 0) } }
162     val totalMainAxisGroups =
<lambda>null163         remember(primarySpaces, spans) {
164             var mainAxisGroup = 0
165             var currentSlot = 0
166             spans.fastForEachIndexed { index, span ->
167                 if (currentSlot + span > primarySpaces) {
168                     currentSlot = 0
169                     mainAxisGroup += 1
170                 }
171                 positions[index] = IntIntPair(mainAxisGroup, currentSlot)
172                 currentSlot += span
173             }
174             mainAxisGroup + 1
175         }
<lambda>null176     val slotPositionsAndSizesCache = remember {
177         object {
178             var sizes = IntArray(0)
179             var positions = IntArray(0)
180         }
181     }
182     Layout(
<lambda>null183         {
184             (0 until spans.size).map { spanIndex ->
185                 key(keys(spanIndex)) {
186                     Box(
187                         Modifier.semantics {
188                             collectionItemInfo =
189                                 if (isVertical) {
190                                     CollectionItemInfo(spanIndex, 1, 0, 1)
191                                 } else {
192                                     CollectionItemInfo(0, 1, spanIndex, 1)
193                                 }
194                         }
195                     ) {
196                         val position = positions[spanIndex]
197                         composables(
198                             spanIndex,
199                             position.second,
200                             position.second == 0,
201                             positions.getOrNull(spanIndex + 1)?.first != position.first,
202                         )
203                     }
204                 }
205             }
206         },
<lambda>null207         modifier.semantics { collectionInfo = CollectionInfo(spans.size, 1) },
measurablesnull208     ) { measurables, constraints ->
209         check(measurables.size == spans.size)
210         val crossAxisSize = if (isVertical) constraints.maxWidth else constraints.maxHeight
211         check(crossAxisSize != Constraints.Infinity) { "Width must be constrained" }
212         if (slotPositionsAndSizesCache.sizes.size != primarySpaces) {
213             slotPositionsAndSizesCache.sizes = IntArray(primarySpaces)
214             slotPositionsAndSizesCache.positions = IntArray(primarySpaces)
215         }
216         calculateCellsCrossAxisSize(
217             crossAxisSize,
218             primarySpaces,
219             crossAxisSpacing.roundToPx(),
220             slotPositionsAndSizesCache.sizes,
221         )
222         val cellSizesInCrossAxis = slotPositionsAndSizesCache.sizes
223         // with is needed because of the double receiver (Density, Arrangement).
224         with(crossAxisArrangement) {
225             arrange(
226                 crossAxisSize,
227                 slotPositionsAndSizesCache.sizes,
228                 LayoutDirection.Ltr,
229                 slotPositionsAndSizesCache.positions,
230             )
231         }
232         val startPositions = slotPositionsAndSizesCache.positions
233         val mainAxisSpacingPx = mainAxisSpacing.roundToPx()
234         val mainAxisTotalGaps = (totalMainAxisGroups - 1) * mainAxisSpacingPx
235         val mainAxisMaxSize = if (isVertical) constraints.maxHeight else constraints.maxWidth
236         val mainAxisElementConstraint =
237             if (mainAxisMaxSize == Constraints.Infinity) {
238                 Constraints.Infinity
239             } else {
240                 max(0, (mainAxisMaxSize - mainAxisTotalGaps) / totalMainAxisGroups)
241             }
242 
243         var mainAxisTotalSize = mainAxisTotalGaps
244         var currentMainAxis = 0
245         var currentMainAxisMax = 0
246         val placeables =
247             measurables.fastMapIndexed { index, measurable ->
248                 val span = spans[index]
249                 val position = positions[index]
250                 val crossAxisConstraint =
251                     calculateWidth(cellSizesInCrossAxis, startPositions, position.second, span)
252 
253                 measurable
254                     .measure(
255                         makeConstraint(isVertical, mainAxisElementConstraint, crossAxisConstraint)
256                     )
257                     .also {
258                         val placeableSize = if (isVertical) it.height else it.width
259                         if (position.first != currentMainAxis) {
260                             // New row -- Add the max size to the total and reset the max
261                             mainAxisTotalSize += currentMainAxisMax
262                             currentMainAxisMax = placeableSize
263                             currentMainAxis = position.first
264                         } else {
265                             currentMainAxisMax = max(currentMainAxisMax, placeableSize)
266                         }
267                     }
268             }
269         mainAxisTotalSize += currentMainAxisMax
270 
271         val height = if (isVertical) mainAxisTotalSize else crossAxisSize
272         val width = if (isVertical) crossAxisSize else mainAxisTotalSize
273 
274         layout(width, height) {
275             var previousMainAxis = 0
276             var currentMainAxisPosition = 0
277             var currentMainAxisMax = 0
278             placeables.forEachIndexed { index, placeable ->
279                 val slot = positions[index].second
280                 val mainAxisSize = if (isVertical) placeable.height else placeable.width
281 
282                 if (positions[index].first != previousMainAxis) {
283                     // Move up a row + padding
284                     currentMainAxisPosition += currentMainAxisMax + mainAxisSpacingPx
285                     currentMainAxisMax = mainAxisSize
286                     previousMainAxis = positions[index].first
287                 } else {
288                     currentMainAxisMax = max(currentMainAxisMax, mainAxisSize)
289                 }
290 
291                 val x =
292                     if (isVertical) {
293                         startPositions[slot]
294                     } else {
295                         currentMainAxisPosition
296                     }
297                 val y =
298                     if (isVertical) {
299                         currentMainAxisPosition
300                     } else {
301                         startPositions[slot]
302                     }
303                 placeable.placeRelative(x, y)
304             }
305         }
306     }
307 }
308 
makeConstraintnull309 fun makeConstraint(isVertical: Boolean, mainAxisSize: Int, crossAxisSize: Int): Constraints {
310     return if (isVertical) {
311         Constraints(maxHeight = mainAxisSize, minWidth = crossAxisSize, maxWidth = crossAxisSize)
312     } else {
313         Constraints(maxWidth = mainAxisSize, minHeight = crossAxisSize, maxHeight = crossAxisSize)
314     }
315 }
316 
calculateWidthnull317 private fun calculateWidth(sizes: IntArray, positions: IntArray, startSlot: Int, span: Int): Int {
318     val crossAxisSize =
319         if (span == 1) {
320                 sizes[startSlot]
321             } else {
322                 val endSlot = startSlot + span - 1
323                 positions[endSlot] + sizes[endSlot] - positions[startSlot]
324             }
325             .coerceAtLeast(0)
326     return crossAxisSize
327 }
328 
calculateCellsCrossAxisSizenull329 private fun calculateCellsCrossAxisSize(
330     gridSize: Int,
331     slotCount: Int,
332     spacingPx: Int,
333     outArray: IntArray,
334 ) {
335     check(outArray.size == slotCount)
336     val gridSizeWithoutSpacing = gridSize - spacingPx * (slotCount - 1)
337     val slotSize = gridSizeWithoutSpacing / slotCount
338     val remainingPixels = gridSizeWithoutSpacing % slotCount
339     outArray.indices.forEach { index ->
340         outArray[index] = slotSize + if (index < remainingPixels) 1 else 0
341     }
342 }
343