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(
103 modifier = modifier,
104 content = content,
105 ) { measurables, constraints ->
106 val cells = measurables.size
107 val columns: Int
108 val rows: Int
109 if (isVertical) {
110 columns = primarySpaces
111 rows = ceil(cells.toFloat() / primarySpaces).toInt()
112 } else {
113 columns = ceil(cells.toFloat() / primarySpaces).toInt()
114 rows = primarySpaces
115 }
116
117 if (sizeCache.rowHeights.size != rows) {
118 sizeCache.rowHeights = IntArray(rows) { 0 }
119 } else {
120 repeat(rows) { i -> sizeCache.rowHeights[i] = 0 }
121 }
122
123 if (sizeCache.columnWidths.size != columns) {
124 sizeCache.columnWidths = IntArray(columns) { 0 }
125 } else {
126 repeat(columns) { i -> sizeCache.columnWidths[i] = 0 }
127 }
128
129 val totalHorizontalSpacingBetweenChildren =
130 ((columns - 1) * horizontalSpacing.toPx()).roundToInt()
131 val totalVerticalSpacingBetweenChildren = ((rows - 1) * verticalSpacing.toPx()).roundToInt()
132 val childConstraints =
133 Constraints(
134 maxWidth =
135 if (constraints.maxWidth != Constraints.Infinity) {
136 (constraints.maxWidth - totalHorizontalSpacingBetweenChildren) / columns
137 } else {
138 Constraints.Infinity
139 },
140 maxHeight =
141 if (constraints.maxHeight != Constraints.Infinity) {
142 (constraints.maxHeight - totalVerticalSpacingBetweenChildren) / rows
143 } else {
144 Constraints.Infinity
145 }
146 )
147
148 val placeables = buildList {
149 for (cellIndex in measurables.indices) {
150 val column: Int
151 val row: Int
152 if (isVertical) {
153 column = cellIndex % columns
154 row = cellIndex / columns
155 } else {
156 column = cellIndex / rows
157 row = cellIndex % rows
158 }
159
160 val placeable = measurables[cellIndex].measure(childConstraints)
161 sizeCache.rowHeights[row] = max(sizeCache.rowHeights[row], placeable.height)
162 sizeCache.columnWidths[column] =
163 max(sizeCache.columnWidths[column], placeable.width)
164 add(placeable)
165 }
166 }
167
168 var totalWidth = totalHorizontalSpacingBetweenChildren
169 for (column in sizeCache.columnWidths.indices) {
170 totalWidth += sizeCache.columnWidths[column]
171 }
172
173 var totalHeight = totalVerticalSpacingBetweenChildren
174 for (row in sizeCache.rowHeights.indices) {
175 totalHeight += sizeCache.rowHeights[row]
176 }
177
178 layout(totalWidth, totalHeight) {
179 var y = 0
180 repeat(rows) { row ->
181 var x = 0
182 var maxChildHeight = 0
183 repeat(columns) { column ->
184 val cellIndex = row * columns + column
185 if (cellIndex < cells) {
186 val placeable = placeables[cellIndex]
187 placeable.placeRelative(x, y)
188 x += placeable.width + horizontalSpacing.roundToPx()
189 maxChildHeight = max(maxChildHeight, placeable.height)
190 }
191 }
192 y += maxChildHeight + verticalSpacing.roundToPx()
193 }
194 }
195 }
196 }
197