| /* |
| * Copyright 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.glance.appwidget.lazy |
| |
| import android.os.Bundle |
| import androidx.compose.runtime.Composable |
| import androidx.compose.runtime.key |
| import androidx.glance.Emittable |
| import androidx.glance.EmittableLazyItemWithChildren |
| import androidx.glance.EmittableWithChildren |
| import androidx.glance.ExperimentalGlanceApi |
| import androidx.glance.GlanceModifier |
| import androidx.glance.GlanceNode |
| import androidx.glance.layout.Alignment |
| import androidx.glance.layout.fillMaxWidth |
| import androidx.glance.layout.wrapContentHeight |
| |
| /** |
| * A vertical scrolling list that only lays out the currently visible items. The [content] block |
| * defines a DSL which allows you to emit different list items. |
| * |
| * @param modifier the modifier to apply to this layout |
| * @param horizontalAlignment the horizontal alignment applied to the items. |
| * @param content a block which describes the content. Inside this block you can use methods like |
| * [LazyListScope.item] to add a single item or [LazyListScope.items] to add a list of items. If the |
| * item has more than one top-level child, they will be automatically wrapped in a Box. |
| */ |
| // TODO(b/198618359): interaction handling |
| @Composable |
| fun LazyColumn( |
| modifier: GlanceModifier = GlanceModifier, |
| horizontalAlignment: Alignment.Horizontal = Alignment.Start, |
| content: LazyListScope.() -> Unit |
| ) { |
| GlanceNode( |
| factory = ::EmittableLazyColumn, |
| update = { |
| this.set(modifier) { this.modifier = it } |
| this.set(horizontalAlignment) { this.horizontalAlignment = it } |
| }, |
| content = applyListScope( |
| Alignment(horizontalAlignment, Alignment.Vertical.CenterVertically), |
| content |
| ) |
| ) |
| } |
| |
| /** |
| * A vertical scrolling list that only lays out the currently visible items. The [content] block |
| * defines a DSL which allows you to emit different list items. |
| * |
| * @param activityOptions Additional options built from an [android.app.ActivityOptions] to apply to |
| * an activity start. |
| * @param modifier the modifier to apply to this layout |
| * @param horizontalAlignment the horizontal alignment applied to the items. |
| * @param content a block which describes the content. Inside this block you can use methods like |
| * [LazyListScope.item] to add a single item or [LazyListScope.items] to add a list of items. If the |
| * item has more than one top-level child, they will be automatically wrapped in a Box. |
| */ |
| @ExperimentalGlanceApi |
| @Composable |
| fun LazyColumn( |
| activityOptions: Bundle, |
| modifier: GlanceModifier = GlanceModifier, |
| horizontalAlignment: Alignment.Horizontal = Alignment.Start, |
| content: LazyListScope.() -> Unit |
| ) { |
| GlanceNode( |
| factory = ::EmittableLazyColumn, |
| update = { |
| this.set(modifier) { this.modifier = it } |
| this.set(horizontalAlignment) { this.horizontalAlignment = it } |
| this.set(activityOptions) { this.activityOptions = it } |
| }, |
| content = applyListScope( |
| Alignment(horizontalAlignment, Alignment.Vertical.CenterVertically), |
| content |
| ) |
| ) |
| } |
| |
| private fun applyListScope( |
| alignment: Alignment, |
| content: LazyListScope.() -> Unit |
| ): @Composable () -> Unit { |
| val itemList = mutableListOf<Pair<Long?, @Composable LazyItemScope.() -> Unit>>() |
| val listScopeImpl = object : LazyListScope { |
| override fun item(itemId: Long, content: @Composable LazyItemScope.() -> Unit) { |
| require(itemId == LazyListScope.UnspecifiedItemId || itemId > ReservedItemIdRangeEnd) { |
| """ |
| You may not specify item ids less than $ReservedItemIdRangeEnd in a Glance |
| widget. These are reserved. |
| """.trimIndent() |
| } |
| itemList.add(itemId to content) |
| } |
| |
| override fun items( |
| count: Int, |
| itemId: ((index: Int) -> Long), |
| itemContent: @Composable LazyItemScope.(index: Int) -> Unit |
| ) { |
| repeat(count) { index -> |
| item(itemId(index)) { itemContent(index) } |
| } |
| } |
| } |
| listScopeImpl.apply(content) |
| return { |
| itemList.forEachIndexed { index, (itemId, composable) -> |
| val id = itemId.takeIf { it != LazyListScope.UnspecifiedItemId } |
| ?: (ReservedItemIdRangeEnd - index) |
| check(id != LazyListScope.UnspecifiedItemId) { "Implicit list item ids exhausted." } |
| LazyListItem(id, alignment) { |
| object : LazyItemScope { }.composable() |
| } |
| } |
| } |
| } |
| |
| @Composable |
| private fun LazyListItem( |
| itemId: Long, |
| alignment: Alignment, |
| content: @Composable () -> Unit |
| ) { |
| // We wrap LazyListItem in the key composable to ensure that lambda actions declared within each |
| // item's scope will get a unique ID based on the currentCompositeKeyHash. |
| key(itemId) { |
| GlanceNode( |
| factory = ::EmittableLazyListItem, |
| update = { |
| this.set(itemId) { this.itemId = it } |
| this.set(alignment) { this.alignment = it } |
| }, |
| content = content |
| ) |
| } |
| } |
| |
| /** |
| * Values between -2^63 and -2^62 are reserved for list items whose id has not been explicitly |
| * defined. |
| */ |
| internal const val ReservedItemIdRangeEnd = -0x4_000_000_000_000_000L |
| |
| @DslMarker |
| annotation class LazyScopeMarker |
| |
| /** |
| * Receiver scope being used by the item content parameter of [LazyColumn]. |
| */ |
| @LazyScopeMarker |
| interface LazyItemScope |
| |
| @JvmDefaultWithCompatibility |
| /** |
| * Receiver scope which is used by [LazyColumn]. |
| */ |
| @LazyScopeMarker |
| interface LazyListScope { |
| /** |
| * Adds a single item. |
| * |
| * @param itemId a stable and unique id representing the item. The value may not be less than |
| * or equal to -2^62, as these values are reserved by the Glance API. Specifying the list |
| * item ids will maintain the scroll position through app widget updates in Android S and |
| * higher devices. |
| * @param content the content of the item |
| */ |
| fun item(itemId: Long = UnspecifiedItemId, content: @Composable LazyItemScope.() -> Unit) |
| |
| /** |
| * Adds a [count] of items. |
| * |
| * @param count the count of items |
| * @param itemId a factory of stable and unique ids representing the item. The value may not be |
| * less than or equal to -2^62, as these values are reserved by the Glance API. Specifying |
| * the list item ids will maintain the scroll position through app widget updates in Android |
| * S and higher devices. |
| * @param itemContent the content displayed by a single item |
| */ |
| fun items( |
| count: Int, |
| itemId: ((index: Int) -> Long) = { UnspecifiedItemId }, |
| itemContent: @Composable LazyItemScope.(index: Int) -> Unit |
| ) |
| |
| companion object { |
| const val UnspecifiedItemId = Long.MIN_VALUE |
| } |
| } |
| |
| /** |
| * Adds a list of items. |
| * |
| * @param items the data list |
| * @param itemId a factory of stable and unique ids representing the item. The value may not be |
| * less than or equal to -2^62, as these values are reserved by the Glance API. Specifying |
| * the list item ids will maintain the scroll position through app widget updates in Android |
| * S and higher devices. |
| * @param itemContent the content displayed by a single item |
| */ |
| inline fun <T> LazyListScope.items( |
| items: List<T>, |
| crossinline itemId: ((item: T) -> Long) = { LazyListScope.UnspecifiedItemId }, |
| crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit |
| ) = items(items.size, { index: Int -> itemId(items[index]) }) { |
| itemContent(items[it]) |
| } |
| |
| /** |
| * Adds a list of items where the content of an item is aware of its index. |
| * |
| * @param items the data list |
| * @param itemId a factory of stable and unique ids representing the item. The value may not be |
| * less than or equal to -2^62, as these values are reserved by the Glance API. Specifying |
| * the list item ids will maintain the scroll position through app widget updates in Android |
| * S and higher devices. |
| * @param itemContent the content displayed by a single item |
| */ |
| inline fun <T> LazyListScope.itemsIndexed( |
| items: List<T>, |
| crossinline itemId: ((index: Int, item: T) -> Long) = |
| { _, _ -> LazyListScope.UnspecifiedItemId }, |
| crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit |
| ) = items(items.size, { index: Int -> itemId(index, items[index]) }) { |
| itemContent(it, items[it]) |
| } |
| |
| /** |
| * Adds an array of items. |
| * |
| * @param items the data array |
| * @param itemId a factory of stable and unique list item ids. Using the same itemId for multiple |
| * items in the array is not allowed. When you specify the itemId, the scroll position will be |
| * maintained based on the itemId, which means if you add/remove items before the current visible |
| * item the item with the given itemId will be kept as the first visible one. |
| * @param itemContent the content displayed by a single item |
| */ |
| inline fun <T> LazyListScope.items( |
| items: Array<T>, |
| noinline itemId: ((item: T) -> Long) = { LazyListScope.UnspecifiedItemId }, |
| crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit |
| ) = items(items.size, { index: Int -> itemId(items[index]) }) { |
| itemContent(items[it]) |
| } |
| |
| /** |
| * Adds a array of items where the content of an item is aware of its index. |
| * |
| * @param items the data array |
| * @param itemId a factory of stable and unique list item ids. Using the same itemId for multiple |
| * items in the array is not allowed. When you specify the itemId the scroll position will be |
| * maintained based on the itemId, which means if you add/remove items before the current visible |
| * item the item with the given itemId will be kept as the first visible one. |
| * @param itemContent the content displayed by a single item |
| */ |
| inline fun <T> LazyListScope.itemsIndexed( |
| items: Array<T>, |
| noinline itemId: ((index: Int, item: T) -> Long) = { _, _ -> LazyListScope.UnspecifiedItemId }, |
| crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit |
| ) = items(items.size, { index: Int -> itemId(index, items[index]) }) { |
| itemContent(it, items[it]) |
| } |
| |
| internal abstract class EmittableLazyList : EmittableWithChildren(resetsDepthForChildren = true) { |
| override var modifier: GlanceModifier = GlanceModifier |
| var horizontalAlignment: Alignment.Horizontal = Alignment.Start |
| var activityOptions: Bundle? = null |
| |
| override fun toString() = |
| "EmittableLazyList(modifier=$modifier, horizontalAlignment=$horizontalAlignment, " + |
| "activityOptions=$activityOptions, children=[\n${childrenToString()}\n])" |
| } |
| |
| internal class EmittableLazyListItem : EmittableLazyItemWithChildren() { |
| // Fill max width of the lazy column so that item contents can be aligned per the horizontal |
| // alignment. |
| override var modifier: GlanceModifier = GlanceModifier.wrapContentHeight().fillMaxWidth() |
| var itemId: Long = 0 |
| |
| override fun copy(): Emittable = EmittableLazyListItem().also { |
| it.itemId = itemId |
| it.alignment = alignment |
| it.children.addAll(children.map { it.copy() }) |
| } |
| |
| override fun toString() = |
| "EmittableLazyListItem(modifier=$modifier, alignment=$alignment, " + |
| "children=[\n${childrenToString()}\n])" |
| } |
| |
| internal class EmittableLazyColumn : EmittableLazyList() { |
| override fun copy(): Emittable = EmittableLazyColumn().also { |
| it.modifier = modifier |
| it.horizontalAlignment = horizontalAlignment |
| it.activityOptions = activityOptions |
| it.children.addAll(children.map { it.copy() }) |
| } |
| } |