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