1 /*
<lambda>null2  * Copyright 2021 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.compose.runtime.Composable
21 import androidx.compose.runtime.key
22 import androidx.glance.Emittable
23 import androidx.glance.EmittableLazyItemWithChildren
24 import androidx.glance.EmittableWithChildren
25 import androidx.glance.ExperimentalGlanceApi
26 import androidx.glance.GlanceModifier
27 import androidx.glance.GlanceNode
28 import androidx.glance.layout.Alignment
29 import androidx.glance.layout.fillMaxWidth
30 import androidx.glance.layout.wrapContentHeight
31 
32 /**
33  * A vertical scrolling list that only lays out the currently visible items. The [content] block
34  * defines a DSL which allows you to emit different list items.
35  *
36  * @param modifier the modifier to apply to this layout
37  * @param horizontalAlignment the horizontal alignment applied to the items.
38  * @param content a block which describes the content. Inside this block you can use methods like
39  *   [LazyListScope.item] to add a single item or [LazyListScope.items] to add a list of items. If
40  *   the item has more than one top-level child, they will be automatically wrapped in a Box.
41  */
42 // TODO(b/198618359): interaction handling
43 @Composable
44 fun LazyColumn(
45     modifier: GlanceModifier = GlanceModifier,
46     horizontalAlignment: Alignment.Horizontal = Alignment.Start,
47     content: LazyListScope.() -> Unit
48 ) {
49     GlanceNode(
50         factory = ::EmittableLazyColumn,
51         update = {
52             this.set(modifier) { this.modifier = it }
53             this.set(horizontalAlignment) { this.horizontalAlignment = it }
54         },
55         content =
56             applyListScope(
57                 Alignment(horizontalAlignment, Alignment.Vertical.CenterVertically),
58                 content
59             )
60     )
61 }
62 
63 /**
64  * A vertical scrolling list that only lays out the currently visible items. The [content] block
65  * defines a DSL which allows you to emit different list items.
66  *
67  * @param activityOptions Additional options built from an [android.app.ActivityOptions] to apply to
68  *   an activity start.
69  * @param modifier the modifier to apply to this layout
70  * @param horizontalAlignment the horizontal alignment applied to the items.
71  * @param content a block which describes the content. Inside this block you can use methods like
72  *   [LazyListScope.item] to add a single item or [LazyListScope.items] to add a list of items. If
73  *   the item has more than one top-level child, they will be automatically wrapped in a Box.
74  */
75 @ExperimentalGlanceApi
76 @Composable
LazyColumnnull77 fun LazyColumn(
78     activityOptions: Bundle,
79     modifier: GlanceModifier = GlanceModifier,
80     horizontalAlignment: Alignment.Horizontal = Alignment.Start,
81     content: LazyListScope.() -> Unit
82 ) {
83     GlanceNode(
84         factory = ::EmittableLazyColumn,
85         update = {
86             this.set(modifier) { this.modifier = it }
87             this.set(horizontalAlignment) { this.horizontalAlignment = it }
88             this.set(activityOptions) { this.activityOptions = it }
89         },
90         content =
91             applyListScope(
92                 Alignment(horizontalAlignment, Alignment.Vertical.CenterVertically),
93                 content
94             )
95     )
96 }
97 
applyListScopenull98 private fun applyListScope(
99     alignment: Alignment,
100     content: LazyListScope.() -> Unit
101 ): @Composable () -> Unit {
102     val itemList = mutableListOf<Pair<Long?, @Composable LazyItemScope.() -> Unit>>()
103     val listScopeImpl =
104         object : LazyListScope {
105             override fun item(itemId: Long, content: @Composable LazyItemScope.() -> Unit) {
106                 require(
107                     itemId == LazyListScope.UnspecifiedItemId || itemId > ReservedItemIdRangeEnd
108                 ) {
109                     """
110                     You may not specify item ids less than $ReservedItemIdRangeEnd in a Glance
111                     widget. These are reserved.
112                 """
113                         .trimIndent()
114                 }
115                 itemList.add(itemId to content)
116             }
117 
118             override fun items(
119                 count: Int,
120                 itemId: ((index: Int) -> Long),
121                 itemContent: @Composable LazyItemScope.(index: Int) -> Unit
122             ) {
123                 repeat(count) { index -> item(itemId(index)) { itemContent(index) } }
124             }
125         }
126     listScopeImpl.apply(content)
127     return {
128         itemList.forEachIndexed { index, (itemId, composable) ->
129             val id =
130                 itemId.takeIf { it != LazyListScope.UnspecifiedItemId }
131                     ?: (ReservedItemIdRangeEnd - index)
132             check(id != LazyListScope.UnspecifiedItemId) { "Implicit list item ids exhausted." }
133             LazyListItem(id, alignment) { object : LazyItemScope {}.composable() }
134         }
135     }
136 }
137 
138 @Composable
LazyListItemnull139 private fun LazyListItem(itemId: Long, alignment: Alignment, content: @Composable () -> Unit) {
140     // We wrap LazyListItem in the key composable to ensure that lambda actions declared within each
141     // item's scope will get a unique ID based on the currentCompositeKeyHash.
142     key(itemId) {
143         GlanceNode(
144             factory = ::EmittableLazyListItem,
145             update = {
146                 this.set(itemId) { this.itemId = it }
147                 this.set(alignment) { this.alignment = it }
148             },
149             content = content
150         )
151     }
152 }
153 
154 /**
155  * Values between -2^63 and -2^62 are reserved for list items whose id has not been explicitly
156  * defined.
157  */
158 internal const val ReservedItemIdRangeEnd = -0x4_000_000_000_000_000L
159 
160 @DslMarker annotation class LazyScopeMarker
161 
162 /** Receiver scope being used by the item content parameter of [LazyColumn]. */
163 @LazyScopeMarker interface LazyItemScope
164 
165 @JvmDefaultWithCompatibility
166 /** Receiver scope which is used by [LazyColumn]. */
167 @LazyScopeMarker
168 interface LazyListScope {
169     /**
170      * Adds a single item.
171      *
172      * @param itemId a stable and unique id representing the item. The value may not be less than or
173      *   equal to -2^62, as these values are reserved by the Glance API. Specifying the list item
174      *   ids will maintain the scroll position through app widget updates in Android S and higher
175      *   devices.
176      * @param content the content of the item
177      */
itemnull178     fun item(itemId: Long = UnspecifiedItemId, content: @Composable LazyItemScope.() -> Unit)
179 
180     /**
181      * Adds a [count] of items.
182      *
183      * @param count the count of items
184      * @param itemId a factory of stable and unique ids representing the item. The value may not be
185      *   less than or equal to -2^62, as these values are reserved by the Glance API. Specifying the
186      *   list item ids will maintain the scroll position through app widget updates in Android S and
187      *   higher devices.
188      * @param itemContent the content displayed by a single item
189      */
190     fun items(
191         count: Int,
192         itemId: ((index: Int) -> Long) = { UnspecifiedItemId },
193         itemContent: @Composable LazyItemScope.(index: Int) -> Unit
194     )
195 
196     companion object {
197         const val UnspecifiedItemId = Long.MIN_VALUE
198     }
199 }
200 
201 /**
202  * Adds a list of items.
203  *
204  * @param items the data list
205  * @param itemId a factory of stable and unique ids representing the item. The value may not be less
206  *   than or equal to -2^62, as these values are reserved by the Glance API. Specifying the list
207  *   item ids will maintain the scroll position through app widget updates in Android S and higher
208  *   devices.
209  * @param itemContent the content displayed by a single item
210  */
itemsnull211 inline fun <T> LazyListScope.items(
212     items: List<T>,
213     crossinline itemId: ((item: T) -> Long) = { LazyListScope.UnspecifiedItemId },
214     crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit
<lambda>null215 ) = items(items.size, { index: Int -> itemId(items[index]) }) { itemContent(items[it]) }
216 
217 /**
218  * Adds a list of items where the content of an item is aware of its index.
219  *
220  * @param items the data list
221  * @param itemId a factory of stable and unique ids representing the item. The value may not be less
222  *   than or equal to -2^62, as these values are reserved by the Glance API. Specifying the list
223  *   item ids will maintain the scroll position through app widget updates in Android S and higher
224  *   devices.
225  * @param itemContent the content displayed by a single item
226  */
itemsIndexednull227 inline fun <T> LazyListScope.itemsIndexed(
228     items: List<T>,
229     crossinline itemId: ((index: Int, item: T) -> Long) = { _, _ ->
230         LazyListScope.UnspecifiedItemId
231     },
232     crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
<lambda>null233 ) = items(items.size, { index: Int -> itemId(index, items[index]) }) { itemContent(it, items[it]) }
234 
235 /**
236  * Adds an array of items.
237  *
238  * @param items the data array
239  * @param itemId a factory of stable and unique list item ids. Using the same itemId for multiple
240  *   items in the array is not allowed. When you specify the itemId, the scroll position will be
241  *   maintained based on the itemId, which means if you add/remove items before the current visible
242  *   item the item with the given itemId will be kept as the first visible one.
243  * @param itemContent the content displayed by a single item
244  */
itemsnull245 inline fun <T> LazyListScope.items(
246     items: Array<T>,
247     noinline itemId: ((item: T) -> Long) = { LazyListScope.UnspecifiedItemId },
248     crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit
<lambda>null249 ) = items(items.size, { index: Int -> itemId(items[index]) }) { itemContent(items[it]) }
250 
251 /**
252  * Adds a array of items where the content of an item is aware of its index.
253  *
254  * @param items the data array
255  * @param itemId a factory of stable and unique list item ids. Using the same itemId for multiple
256  *   items in the array is not allowed. When you specify the itemId the scroll position will be
257  *   maintained based on the itemId, which means if you add/remove items before the current visible
258  *   item the item with the given itemId will be kept as the first visible one.
259  * @param itemContent the content displayed by a single item
260  */
itemsIndexednull261 inline fun <T> LazyListScope.itemsIndexed(
262     items: Array<T>,
263     noinline itemId: ((index: Int, item: T) -> Long) = { _, _ -> LazyListScope.UnspecifiedItemId },
264     crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
<lambda>null265 ) = items(items.size, { index: Int -> itemId(index, items[index]) }) { itemContent(it, items[it]) }
266 
267 internal abstract class EmittableLazyList : EmittableWithChildren(resetsDepthForChildren = true) {
268     override var modifier: GlanceModifier = GlanceModifier
269     var horizontalAlignment: Alignment.Horizontal = Alignment.Start
270     var activityOptions: Bundle? = null
271 
toStringnull272     override fun toString() =
273         "EmittableLazyList(modifier=$modifier, horizontalAlignment=$horizontalAlignment, " +
274             "activityOptions=$activityOptions, children=[\n${childrenToString()}\n])"
275 }
276 
277 internal class EmittableLazyListItem : EmittableLazyItemWithChildren() {
278     // Fill max width of the lazy column so that item contents can be aligned per the horizontal
279     // alignment.
280     override var modifier: GlanceModifier = GlanceModifier.wrapContentHeight().fillMaxWidth()
281     var itemId: Long = 0
282 
283     override fun copy(): Emittable =
284         EmittableLazyListItem().also {
285             it.itemId = itemId
286             it.alignment = alignment
287             it.children.addAll(children.map { it.copy() })
288         }
289 
290     override fun toString() =
291         "EmittableLazyListItem(modifier=$modifier, alignment=$alignment, " +
292             "children=[\n${childrenToString()}\n])"
293 }
294 
295 internal class EmittableLazyColumn : EmittableLazyList() {
copynull296     override fun copy(): Emittable =
297         EmittableLazyColumn().also {
298             it.modifier = modifier
299             it.horizontalAlignment = horizontalAlignment
300             it.activityOptions = activityOptions
301             it.children.addAll(children.map { it.copy() })
302         }
303 }
304