xref: /aosp_15_r20/frameworks/base/packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt (revision d57664e9bc4670b3ecf6748a746a57c557b6bc9e)
1 /*
<lambda>null2  * Copyright (C) 2023 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 
17 package com.android.compose.grid
18 
19 import androidx.compose.runtime.Composable
20 import androidx.compose.runtime.remember
21 import androidx.compose.ui.Modifier
22 import androidx.compose.ui.layout.Layout
23 import androidx.compose.ui.unit.Constraints
24 import androidx.compose.ui.unit.Dp
25 import androidx.compose.ui.unit.dp
26 import kotlin.math.ceil
27 import kotlin.math.max
28 import kotlin.math.roundToInt
29 
30 /**
31  * Renders a grid with [columns] columns.
32  *
33  * Child composables will be arranged row by row.
34  *
35  * Each column is spaced from the columns to its left and right by [horizontalSpacing]. Each cell
36  * inside a column is spaced from the cells above and below it with [verticalSpacing].
37  */
38 @Composable
39 fun VerticalGrid(
40     columns: Int,
41     modifier: Modifier = Modifier,
42     verticalSpacing: Dp = 0.dp,
43     horizontalSpacing: Dp = 0.dp,
44     content: @Composable () -> Unit,
45 ) {
46     Grid(
47         primarySpaces = columns,
48         isVertical = true,
49         modifier = modifier,
50         verticalSpacing = verticalSpacing,
51         horizontalSpacing = horizontalSpacing,
52         content = content,
53     )
54 }
55 
56 /**
57  * Renders a grid with [rows] rows.
58  *
59  * Child composables will be arranged column by column.
60  *
61  * Each column is spaced from the columns to its left and right by [horizontalSpacing]. Each cell
62  * inside a column is spaced from the cells above and below it with [verticalSpacing].
63  */
64 @Composable
HorizontalGridnull65 fun HorizontalGrid(
66     rows: Int,
67     modifier: Modifier = Modifier,
68     verticalSpacing: Dp = 0.dp,
69     horizontalSpacing: Dp = 0.dp,
70     content: @Composable () -> Unit,
71 ) {
72     Grid(
73         primarySpaces = rows,
74         isVertical = false,
75         modifier = modifier,
76         verticalSpacing = verticalSpacing,
77         horizontalSpacing = horizontalSpacing,
78         content = content,
79     )
80 }
81 
82 @Composable
Gridnull83 private fun Grid(
84     primarySpaces: Int,
85     isVertical: Boolean,
86     modifier: Modifier = Modifier,
87     verticalSpacing: Dp,
88     horizontalSpacing: Dp,
89     content: @Composable () -> Unit,
90 ) {
91     check(primarySpaces > 0) {
92         "Must provide a positive number of ${if (isVertical) "columns" else "rows"}"
93     }
94 
95     val sizeCache = remember {
96         object {
97             var rowHeights = intArrayOf()
98             var columnWidths = intArrayOf()
99         }
100     }
101 
102     Layout(modifier = modifier, content = content) { measurables, constraints ->
103         val cells = measurables.size
104         val columns: Int
105         val rows: Int
106         if (isVertical) {
107             columns = primarySpaces
108             rows = ceil(cells.toFloat() / primarySpaces).toInt()
109         } else {
110             columns = ceil(cells.toFloat() / primarySpaces).toInt()
111             rows = primarySpaces
112         }
113 
114         if (sizeCache.rowHeights.size != rows) {
115             sizeCache.rowHeights = IntArray(rows) { 0 }
116         } else {
117             repeat(rows) { i -> sizeCache.rowHeights[i] = 0 }
118         }
119 
120         if (sizeCache.columnWidths.size != columns) {
121             sizeCache.columnWidths = IntArray(columns) { 0 }
122         } else {
123             repeat(columns) { i -> sizeCache.columnWidths[i] = 0 }
124         }
125 
126         val totalHorizontalSpacingBetweenChildren =
127             ((columns - 1) * horizontalSpacing.toPx()).roundToInt()
128         val totalVerticalSpacingBetweenChildren = ((rows - 1) * verticalSpacing.toPx()).roundToInt()
129         val childConstraints =
130             Constraints(
131                 maxWidth =
132                     if (constraints.maxWidth != Constraints.Infinity) {
133                         (constraints.maxWidth - totalHorizontalSpacingBetweenChildren) / columns
134                     } else {
135                         Constraints.Infinity
136                     },
137                 maxHeight =
138                     if (constraints.maxHeight != Constraints.Infinity) {
139                         (constraints.maxHeight - totalVerticalSpacingBetweenChildren) / rows
140                     } else {
141                         Constraints.Infinity
142                     },
143             )
144 
145         val placeables = buildList {
146             for (cellIndex in measurables.indices) {
147                 val column: Int
148                 val row: Int
149                 if (isVertical) {
150                     column = cellIndex % columns
151                     row = cellIndex / columns
152                 } else {
153                     column = cellIndex / rows
154                     row = cellIndex % rows
155                 }
156 
157                 val placeable = measurables[cellIndex].measure(childConstraints)
158                 sizeCache.rowHeights[row] = max(sizeCache.rowHeights[row], placeable.height)
159                 sizeCache.columnWidths[column] =
160                     max(sizeCache.columnWidths[column], placeable.width)
161                 add(placeable)
162             }
163         }
164 
165         var totalWidth = totalHorizontalSpacingBetweenChildren
166         for (column in sizeCache.columnWidths.indices) {
167             totalWidth += sizeCache.columnWidths[column]
168         }
169 
170         var totalHeight = totalVerticalSpacingBetweenChildren
171         for (row in sizeCache.rowHeights.indices) {
172             totalHeight += sizeCache.rowHeights[row]
173         }
174 
175         layout(totalWidth, totalHeight) {
176             var y = 0
177             repeat(rows) { row ->
178                 var x = 0
179                 var maxChildHeight = 0
180                 repeat(columns) { column ->
181                     val cellIndex = row * columns + column
182                     if (cellIndex < cells) {
183                         val placeable = placeables[cellIndex]
184                         placeable.placeRelative(x, y)
185                         x += placeable.width + horizontalSpacing.roundToPx()
186                         maxChildHeight = max(maxChildHeight, placeable.height)
187                     }
188                 }
189                 y += maxChildHeight + verticalSpacing.roundToPx()
190             }
191         }
192     }
193 }
194