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