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