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