1 /*
<lambda>null2 * Copyright 2022 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 androidx.glance.appwidget.lazy
18
19 import android.os.Bundle
20 import androidx.annotation.RequiresApi
21 import androidx.compose.runtime.Composable
22 import androidx.compose.runtime.key
23 import androidx.compose.ui.unit.Dp
24 import androidx.glance.Emittable
25 import androidx.glance.EmittableLazyItemWithChildren
26 import androidx.glance.EmittableWithChildren
27 import androidx.glance.ExperimentalGlanceApi
28 import androidx.glance.GlanceModifier
29 import androidx.glance.GlanceNode
30 import androidx.glance.layout.Alignment
31 import androidx.glance.layout.fillMaxWidth
32 import androidx.glance.layout.wrapContentHeight
33
34 /**
35 * The DSL implementation of a lazy grid layout. It composes only visible rows of the grid.
36 *
37 * @param gridCells the number of columns in the grid.
38 * @param modifier the modifier to apply to this layout
39 * @param horizontalAlignment the horizontal alignment applied to the items.
40 * @param content a block which describes the content. Inside this block you can use methods like
41 * [LazyVerticalGridScope.item] to add a single item or [LazyVerticalGridScope.items] to add a
42 * list of items. If the item has more than one top-level child, they will be automatically
43 * wrapped in a Box.
44 */
45 @Composable
46 fun LazyVerticalGrid(
47 gridCells: GridCells,
48 modifier: GlanceModifier = GlanceModifier,
49 horizontalAlignment: Alignment.Horizontal = Alignment.Start,
50 content: LazyVerticalGridScope.() -> Unit
51 ) {
52 GlanceNode(
53 factory = ::EmittableLazyVerticalGrid,
54 update = {
55 this.set(gridCells) { this.gridCells = it }
56 this.set(modifier) { this.modifier = it }
57 this.set(horizontalAlignment) { this.horizontalAlignment = it }
58 },
59 content =
60 applyVerticalGridScope(
61 Alignment(horizontalAlignment, Alignment.Vertical.CenterVertically),
62 content
63 )
64 )
65 }
66
67 /**
68 * The DSL implementation of a lazy grid layout. It composes only visible rows of the grid.
69 *
70 * @param gridCells the number of columns in the grid.
71 * @param activityOptions Additional options built from an [android.app.ActivityOptions] to apply to
72 * an activity start.
73 * @param modifier the modifier to apply to this layout
74 * @param horizontalAlignment the horizontal alignment applied to the items.
75 * @param content a block which describes the content. Inside this block you can use methods like
76 * [LazyVerticalGridScope.item] to add a single item or [LazyVerticalGridScope.items] to add a
77 * list of items. If the item has more than one top-level child, they will be automatically
78 * wrapped in a Box.
79 */
80 @ExperimentalGlanceApi
81 @Composable
LazyVerticalGridnull82 fun LazyVerticalGrid(
83 gridCells: GridCells,
84 activityOptions: Bundle,
85 modifier: GlanceModifier = GlanceModifier,
86 horizontalAlignment: Alignment.Horizontal = Alignment.Start,
87 content: LazyVerticalGridScope.() -> Unit
88 ) {
89 GlanceNode(
90 factory = ::EmittableLazyVerticalGrid,
91 update = {
92 this.set(gridCells) { this.gridCells = it }
93 this.set(modifier) { this.modifier = it }
94 this.set(horizontalAlignment) { this.horizontalAlignment = it }
95 this.set(activityOptions) { this.activityOptions = it }
96 },
97 content =
98 applyVerticalGridScope(
99 Alignment(horizontalAlignment, Alignment.Vertical.CenterVertically),
100 content
101 )
102 )
103 }
104
applyVerticalGridScopenull105 internal fun applyVerticalGridScope(
106 alignment: Alignment,
107 content: LazyVerticalGridScope.() -> Unit
108 ): @Composable () -> Unit {
109 val itemList = mutableListOf<Pair<Long?, @Composable LazyItemScope.() -> Unit>>()
110 val listScopeImpl =
111 object : LazyVerticalGridScope {
112 override fun item(itemId: Long, content: @Composable LazyItemScope.() -> Unit) {
113 require(
114 itemId == LazyVerticalGridScope.UnspecifiedItemId ||
115 itemId > ReservedItemIdRangeEnd
116 ) {
117 """
118 You may not specify item ids less than $ReservedItemIdRangeEnd in a Glance
119 widget. These are reserved.
120 """
121 .trimIndent()
122 }
123 itemList.add(itemId to content)
124 }
125
126 override fun items(
127 count: Int,
128 itemId: ((index: Int) -> Long),
129 itemContent: @Composable LazyItemScope.(index: Int) -> Unit
130 ) {
131 repeat(count) { index -> item(itemId(index)) { itemContent(index) } }
132 }
133 }
134 listScopeImpl.apply(content)
135 return {
136 itemList.forEachIndexed { index, (itemId, composable) ->
137 val id =
138 itemId.takeIf { it != LazyVerticalGridScope.UnspecifiedItemId }
139 ?: (ReservedItemIdRangeEnd - index)
140 check(id != LazyVerticalGridScope.UnspecifiedItemId) {
141 "Implicit list item ids exhausted."
142 }
143 LazyVerticalGridItem(id, alignment) { object : LazyItemScope {}.composable() }
144 }
145 }
146 }
147
148 @Composable
LazyVerticalGridItemnull149 private fun LazyVerticalGridItem(
150 itemId: Long,
151 alignment: Alignment,
152 content: @Composable () -> Unit
153 ) {
154 // We wrap LazyVerticalGridItem in the key composable to ensure that lambda actions declared
155 // within each item's scope will get a unique ID based on the currentCompositeKeyHash.
156 key(itemId) {
157 GlanceNode(
158 factory = ::EmittableLazyVerticalGridListItem,
159 update = {
160 this.set(itemId) { this.itemId = it }
161 this.set(alignment) { this.alignment = it }
162 },
163 content = content
164 )
165 }
166 }
167
168 @JvmDefaultWithCompatibility
169 /** Receiver scope which is used by [LazyColumn]. */
170 @LazyScopeMarker
171 interface LazyVerticalGridScope {
172 /**
173 * Adds a single item.
174 *
175 * @param itemId a stable and unique id representing the item. The value may not be less than or
176 * equal to -2^62, as these values are reserved by the Glance API. Specifying the list item
177 * ids will maintain the scroll position through app widget updates in Android S and higher
178 * devices.
179 * @param content the content of the item
180 */
itemnull181 fun item(itemId: Long = UnspecifiedItemId, content: @Composable LazyItemScope.() -> Unit)
182
183 /**
184 * Adds a [count] of items.
185 *
186 * @param count the count of items
187 * @param itemId a factory of stable and unique ids representing the item. The value may not be
188 * less than or equal to -2^62, as these values are reserved by the Glance API. Specifying the
189 * list item ids will maintain the scroll position through app widget updates in Android S and
190 * higher devices.
191 * @param itemContent the content displayed by a single item
192 */
193 fun items(
194 count: Int,
195 itemId: ((index: Int) -> Long) = { UnspecifiedItemId },
196 itemContent: @Composable LazyItemScope.(index: Int) -> Unit
197 )
198
199 companion object {
200 const val UnspecifiedItemId = Long.MIN_VALUE
201 }
202 }
203
204 /**
205 * Adds a list of items.
206 *
207 * @param items the data list
208 * @param itemId a factory of stable and unique ids representing the item. The value may not be less
209 * than or equal to -2^62, as these values are reserved by the Glance API. Specifying the list
210 * item ids will maintain the scroll position through app widget updates in Android S and higher
211 * devices.
212 * @param itemContent the content displayed by a single item
213 */
itemsnull214 inline fun <T> LazyVerticalGridScope.items(
215 items: List<T>,
216 crossinline itemId: ((item: T) -> Long) = { LazyVerticalGridScope.UnspecifiedItemId },
217 crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit
<lambda>null218 ) = items(items.size, { index: Int -> itemId(items[index]) }) { itemContent(items[it]) }
219
220 /**
221 * Adds a list of items where the content of an item is aware of its index.
222 *
223 * @param items the data list
224 * @param itemId a factory of stable and unique ids representing the item. The value may not be less
225 * than or equal to -2^62, as these values are reserved by the Glance API. Specifying the list
226 * item ids will maintain the scroll position through app widget updates in Android S and higher
227 * devices.
228 * @param itemContent the content displayed by a single item
229 */
itemsIndexednull230 inline fun <T> LazyVerticalGridScope.itemsIndexed(
231 items: List<T>,
232 crossinline itemId: ((index: Int, item: T) -> Long) = { _, _ ->
233 LazyVerticalGridScope.UnspecifiedItemId
234 },
235 crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
<lambda>null236 ) = items(items.size, { index: Int -> itemId(index, items[index]) }) { itemContent(it, items[it]) }
237
238 /**
239 * Adds an array of items.
240 *
241 * @param items the data array
242 * @param itemId a factory of stable and unique list item ids. Using the same itemId for multiple
243 * items in the array is not allowed. When you specify the itemId, the scroll position will be
244 * maintained based on the itemId, which means if you add/remove items before the current visible
245 * item the item with the given itemId will be kept as the first visible one.
246 * @param itemContent the content displayed by a single item
247 */
itemsnull248 inline fun <T> LazyVerticalGridScope.items(
249 items: Array<T>,
250 noinline itemId: ((item: T) -> Long) = { LazyVerticalGridScope.UnspecifiedItemId },
251 crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit
<lambda>null252 ) = items(items.size, { index: Int -> itemId(items[index]) }) { itemContent(items[it]) }
253
254 /**
255 * Adds a array of items where the content of an item is aware of its index.
256 *
257 * @param items the data array
258 * @param itemId a factory of stable and unique list item ids. Using the same itemId for multiple
259 * items in the array is not allowed. When you specify the itemId the scroll position will be
260 * maintained based on the itemId, which means if you add/remove items before the current visible
261 * item the item with the given itemId will be kept as the first visible one.
262 * @param itemContent the content displayed by a single item
263 */
itemsIndexednull264 inline fun <T> LazyVerticalGridScope.itemsIndexed(
265 items: Array<T>,
266 noinline itemId: ((index: Int, item: T) -> Long) = { _, _ ->
267 LazyVerticalGridScope.UnspecifiedItemId
268 },
269 crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
<lambda>null270 ) = items(items.size, { index: Int -> itemId(index, items[index]) }) { itemContent(it, items[it]) }
271
272 internal abstract class EmittableLazyVerticalGridList :
273 EmittableWithChildren(resetsDepthForChildren = true) {
274 override var modifier: GlanceModifier = GlanceModifier
275 var horizontalAlignment: Alignment.Horizontal = Alignment.Start
276 var gridCells: GridCells = GridCells.Fixed(1)
277 var activityOptions: Bundle? = null
278
toStringnull279 override fun toString(): String =
280 "EmittableLazyVerticalGridList(modifier=$modifier, " +
281 "horizontalAlignment=$horizontalAlignment, " +
282 "numColumn=$gridCells, " +
283 "activityOptions=$activityOptions, " +
284 "children=[\n${childrenToString()}\n])"
285 }
286
287 internal class EmittableLazyVerticalGridListItem : EmittableLazyItemWithChildren() {
288 // Fill max width of the grid cell so that item contents can be aligned per the horizontal
289 // alignment.
290 override var modifier: GlanceModifier = GlanceModifier.wrapContentHeight().fillMaxWidth()
291 var itemId: Long = 0
292
293 override fun copy(): Emittable =
294 EmittableLazyVerticalGridListItem().also {
295 it.itemId = itemId
296 it.alignment = alignment
297 it.children.addAll(children.map { it.copy() })
298 }
299
300 override fun toString(): String =
301 "EmittableLazyVerticalGridListItem(" +
302 "modifier=$modifier, " +
303 "alignment=$alignment, " +
304 "children=[\n${childrenToString()}\n])"
305 }
306
307 internal class EmittableLazyVerticalGrid : EmittableLazyVerticalGridList() {
copynull308 override fun copy(): Emittable =
309 EmittableLazyVerticalGrid().also {
310 it.modifier = modifier
311 it.horizontalAlignment = horizontalAlignment
312 it.gridCells = gridCells
313 it.activityOptions = activityOptions
314 it.children.addAll(children.map { it.copy() })
315 }
316 }
317
318 /** Defines the number of columns of the GridView. */
319 sealed class GridCells {
320 /**
321 * Defines a fixed number of columns, limited to 1 through 5.
322 *
323 * For example, [LazyVerticalGrid] Fixed(3) would mean that there are 3 columns 1/3 of the
324 * parent wide.
325 *
326 * @param count number of columns in LazyVerticalGrid
327 */
328 class Fixed(val count: Int) : GridCells() {
equalsnull329 override fun equals(other: Any?): Boolean {
330 if (this === other) return true
331 if (javaClass != other?.javaClass) return false
332
333 other as Fixed
334
335 if (count != other.count) return false
336
337 return true
338 }
339
hashCodenull340 override fun hashCode(): Int {
341 return count
342 }
343 }
344
345 /**
346 * Defines a grid with as many columns as possible on the condition that every cell has at least
347 * [minSize] space and all extra space distributed evenly.
348 *
349 * For example, for the vertical [LazyVerticalGrid] Adaptive(20.dp) would mean that there will
350 * be as many columns as possible and every column will be at least 20.dp and all the columns
351 * will have equal width. If the screen is 88.dp wide then there will be 4 columns 22.dp each.
352 *
353 * @param minSize fixed width of each column in LazyVerticalGrid
354 */
355 @RequiresApi(31)
356 class Adaptive(val minSize: Dp) : GridCells() {
equalsnull357 override fun equals(other: Any?): Boolean {
358 if (this === other) return true
359 if (javaClass != other?.javaClass) return false
360
361 other as Adaptive
362
363 if (minSize != other.minSize) return false
364
365 return true
366 }
367
hashCodenull368 override fun hashCode(): Int {
369 return minSize.hashCode()
370 }
371 }
372 }
373