Files

394 lines
18 KiB
Markdown

When creating a Glimmer List component, refer to the following source code in
`List.kt`:
```kotlin
/*
* Copyright 2025 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.xr.glimmer.list
import androidx.compose.foundation.OverscrollEffect
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.layout.LazyLayout
import androidx.compose.foundation.rememberOverscrollEffect
import androidx.compose.foundation.scrollableArea
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
import androidx.xr.glimmer.GlimmerTheme
import androidx.xr.glimmer.edgeScrim
import kotlin.math.max
/**
* This is a scrolling list component that only composes and lays out the currently visible items.
* It is based on [androidx.compose.foundation.lazy.LazyColumn], but with extra functionality and
* customized behavior required for Jetpack Compose Glimmer. For Jetpack Compose Glimmer
* applications, it is recommended to use [VerticalList] instead of
* [androidx.compose.foundation.lazy.LazyColumn], as it is specifically designed to provide seamless
* focus-based navigation, visual scrim edge effects and support for focus-aware snap behavior.
*
* The [content] block defines a DSL which allows you to emit items of different types. For example,
* you can use [ListScope.item] to add a single item and [ListScope.items] to add a list of items.
*
* See the other [VerticalList] overload for a variant with a title slot.
*
* @sample androidx.xr.glimmer.samples.VerticalListSample
* @param modifier the modifier to apply to this layout.
* @param state the state object to be used to control or observe the list's state.
* @param contentPadding a padding around the whole content. This will add padding for the content
* after it has been clipped, which is not possible via [modifier] param. You can use it to add a
* padding before the first item or after the last one.
* @param userScrollEnabled If user gestures are enabled.
* @param overscrollEffect the [OverscrollEffect] that will be used to render overscroll for this
* layout. Note that the [OverscrollEffect.node] will be applied internally as well - you do not
* need to use Modifier.overscroll separately.
* @param flingBehavior logic describing fling and snapping behavior when drag has finished.
* @param reverseLayout reverses the direction of scrolling and layout.
* @param horizontalAlignment aligns items horizontally.
* @param verticalArrangement is arrangement for items. This only applies if the content is smaller
* than the viewport.
* @param content a block which describes the content. Inside this block you can use methods like
* [ListScope.item] to add a single item or [ListScope.items] to add a list of items.
*/
@Composable
public fun VerticalList(
modifier: Modifier = Modifier,
state: ListState = rememberListState(),
contentPadding: PaddingValues = VerticalListDefaults.contentPadding,
userScrollEnabled: Boolean = true,
overscrollEffect: OverscrollEffect? = rememberOverscrollEffect(),
flingBehavior: FlingBehavior = VerticalListDefaults.flingBehavior(state),
reverseLayout: Boolean = false,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
verticalArrangement: Arrangement.Vertical = VerticalListDefaults.verticalArrangement,
content: ListScope.() -> Unit,
): Unit =
List(
orientation = Orientation.Vertical,
modifier = modifier,
state = state,
reverseLayout = reverseLayout,
contentPadding = contentPadding,
userScrollEnabled = userScrollEnabled,
overscrollEffect = overscrollEffect,
flingBehavior = flingBehavior,
horizontalAlignment = horizontalAlignment,
verticalArrangement = verticalArrangement,
verticalAlignment = null,
horizontalArrangement = null,
content = content,
)
/**
* This is a scrolling list component that only composes and lays out the currently visible items.
* It is based on [androidx.compose.foundation.lazy.LazyColumn], but with extra functionality and
* customized behavior required for Jetpack Compose Glimmer. Jetpack Compose Glimmer applications
* should always use VerticalList instead of LazyColumn to ensure correct behavior.
*
* The [content] block defines a DSL which allows you to emit items of different types. For example,
* you can use [ListScope.item] to add a single item and [ListScope.items] to add a list of items.
*
* This overload of `VerticalList` contains a `title` slot. The title is expected to be a
* [androidx.xr.glimmer.TitleChip]. It is positioned at the top center and visually overlaps the
* list content. The list is vertically offset to start from the title's vertical center. When the
* list is scrolled, the title remains static.
*
* See the other [VerticalList] overload for a variant with no title slot.
*
* @sample androidx.xr.glimmer.samples.VerticalListWithTitleChipSample
* @param title a composable slot for the list title, expected to be a
* [androidx.xr.glimmer.TitleChip]. It overlaps the list, positioned at the top-center, and
* remains stuck to the top when the list is scrolled.
* @param modifier applies to the layout that contains both list and title.
* @param state the state object to be used to control or observe the list's state.
* @param contentPadding a padding around the whole content. This will add padding for the content
* after it has been clipped, which is not possible via [modifier] param. You can use it to add a
* padding before the first item or after the last one. The list is vertically offset to start
* from the title's vertical center, so custom content paddings must provide sufficient space to
* avoid content being obscured.
* @param userScrollEnabled If user gestures are enabled.
* @param overscrollEffect the [OverscrollEffect] that will be used to render overscroll for this
* layout. Note that the [OverscrollEffect.node] will be applied internally as well - you do not
* need to use Modifier.overscroll separately.
* @param flingBehavior logic describing fling and snapping behavior when drag has finished.
* @param reverseLayout reverses the direction of scrolling and layout.
* @param horizontalAlignment aligns items horizontally.
* @param verticalArrangement is arrangement for items. This only applies if the content is smaller
* than the viewport.
* @param content a block which describes the content. Inside this block you can use methods like
* [ListScope.item] to add a single item or [ListScope.items] to add a list of items.
*/
@Suppress(
// The main trailing lambda is [content], but it's DSL.
"ComposableLambdaParameterNaming",
"ComposableLambdaParameterPosition",
)
@Composable
public fun VerticalList(
title: @Composable () -> Unit,
modifier: Modifier = Modifier,
state: ListState = rememberListState(),
contentPadding: PaddingValues = VerticalListDefaults.contentPaddingWithTitle,
userScrollEnabled: Boolean = true,
overscrollEffect: OverscrollEffect? = rememberOverscrollEffect(),
flingBehavior: FlingBehavior = VerticalListDefaults.flingBehavior(state),
reverseLayout: Boolean = false,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
verticalArrangement: Arrangement.Vertical = VerticalListDefaults.verticalArrangement,
content: ListScope.() -> Unit,
) {
VerticalListWithTitleLayout(
modifier = modifier,
title = title,
list = {
VerticalList(
state = state,
contentPadding = contentPadding,
userScrollEnabled = userScrollEnabled,
overscrollEffect = overscrollEffect,
flingBehavior = flingBehavior,
reverseLayout = reverseLayout,
horizontalAlignment = horizontalAlignment,
verticalArrangement = verticalArrangement,
content = content,
)
},
)
}
/** Contains the default values used by [VerticalList]. */
public object VerticalListDefaults {
/**
* Recommended value for the distance between items.
*
* @see [verticalArrangement] for the default arrangement that uses this spacing.
*/
public val itemSpacing: Dp
@Composable get() = GlimmerTheme.componentSpacingValues.extraLarge
/** The maximum height of the fade effects on the sides of the list. */
public val ScrimMaxHeight: Dp = 46.dp
/** Recommended content padding values for lists without a title. */
public val contentPadding: PaddingValues
@Composable get() = PaddingValues(vertical = itemSpacing, horizontal = 0.dp)
/** Recommended content padding values for lists with a title. */
public val contentPaddingWithTitle: PaddingValues
@Composable get() = PaddingValues(top = ScrimMaxHeight, bottom = itemSpacing)
/** Recommended values for the vertical arrangement. */
public val verticalArrangement: Arrangement.Vertical
@Composable get() = Arrangement.spacedBy(itemSpacing)
/**
* Creates and remembers the default fling behavior for a [VerticalList] that aligns the focus
* position with list scroll.
*
* @param state The [ListState] to observe for layout and focus information.
* @return A [FlingBehavior] instance that provides focus-aware snapping.
*/
@Composable
public fun flingBehavior(state: ListState): FlingBehavior {
val snapLayoutInfoProvider = remember(state) { SnapLayoutInfoProvider(state) }
return rememberSnapFlingBehavior(snapLayoutInfoProvider)
}
}
@Composable
private fun VerticalListWithTitleLayout(
modifier: Modifier = Modifier,
title: @Composable () -> Unit,
list: @Composable () -> Unit,
) {
Layout(modifier = modifier, contents = listOf(list, title)) { measurables, constraints ->
// The title parameter is provided by users, requiring iteration through all measurables.
// The list parameter is provided by us, allowing to guarantee that it contains only
// a single measurable.
val listMeasurable = measurables[0][0]
val titleMeasurables = measurables[1]
// Measure title(s) first.
var titleMaxHeight = 0
var titleMaxWidth = 0
val titleConstraints = constraints.copyMaxDimensions()
val titlePlaceables =
titleMeasurables.fastMap { measurable ->
val placeable = measurable.measure(titleConstraints)
titleMaxHeight = max(titleMaxHeight, placeable.height)
titleMaxWidth = max(titleMaxWidth, placeable.width)
placeable
}
// List shouldn't use the space above the vertical center of the title.
val titleYOffset = titleMaxHeight / 2
val maxListHeight = constraints.maxHeight - titleYOffset
val minListHeight = minOf(constraints.minHeight, maxListHeight)
val listConstraints = constraints.copy(minHeight = minListHeight, maxHeight = maxListHeight)
val listPlaceable = listMeasurable.measure(listConstraints)
val layoutWidth = maxOf(listPlaceable.width, titleMaxWidth)
val layoutHeight = listPlaceable.height + titleYOffset
layout(width = layoutWidth, height = layoutHeight) {
// Place the list first.
listPlaceable.placeRelative(
x = (layoutWidth - listPlaceable.width) / 2,
y = titleYOffset,
)
// Then place the rest of the titles on top of the list.
titlePlaceables.fastForEach { titlePlaceable ->
// Each title's center aligned with the top of the list.
titlePlaceable.placeRelative(
x = (layoutWidth - titlePlaceable.width) / 2,
y = (titleMaxHeight - titlePlaceable.height) / 2,
)
}
}
}
}
/**
* The scrolling list that only composes and lays out the currently visible items. The [content]
* block defines a DSL which allows you to emit items of different types. For example, you can use
* [ListScope.item] to add a single item and [ListScope.items] to add a list of items.
*
* @param orientation The orientation in which to layout items in this list.
* @param modifier the modifier to apply to this layout.
* @param state the state object to be used to control or observe the list's state.
* @param contentPadding a padding around the whole content. This will add padding for the content
* after it has been clipped, which is not possible via [modifier] param. You can use it to add a
* padding before the first item or after the last one.
* @param userScrollEnabled If user gestures are enabled.
* @param overscrollEffect the [OverscrollEffect] that will be used to render overscroll for this
* layout. Note that the [OverscrollEffect.node] will be applied internally as well - you do not
* need to use Modifier.overscroll separately.
* @param flingBehavior logic describing fling and snapping behavior when drag has finished.
* @param reverseLayout reverses the direction of scrolling and layout.
* @param horizontalAlignment aligns items horizontally. It's required and used only if
* [orientation] is [Orientation.Vertical].
* @param verticalArrangement is arrangement for items. This only applies if the content is smaller
* than the viewport. It's required and used only if [orientation] is [Orientation.Vertical].
* @param verticalAlignment aligns items vertically. It's required and used only if [orientation] is
* [Orientation.Horizontal].
* @param horizontalArrangement is arrangement for items. This only applies if the content is
* smaller than the viewport. It's required and used only if [orientation] is
* [Orientation.Vertical].
* @param content a block which describes the content. Inside this block you can use methods like
* [ListScope.item] to add a single item or [ListScope.items] to add a list of items.
*/
@Composable
internal fun List(
orientation: Orientation,
modifier: Modifier,
state: ListState,
contentPadding: PaddingValues,
userScrollEnabled: Boolean,
overscrollEffect: OverscrollEffect?,
flingBehavior: FlingBehavior,
reverseLayout: Boolean,
horizontalAlignment: Alignment.Horizontal?,
verticalArrangement: Arrangement.Vertical?,
verticalAlignment: Alignment.Vertical?,
horizontalArrangement: Arrangement.Horizontal?,
content: ListScope.() -> Unit,
) {
val itemProvider = rememberGlimmerListItemProviderLambda(state, content)
val semanticState = rememberGlimmerListSemanticState(state, orientation)
val scrollEnabled = isScrollEnabled(userScrollEnabled, state)
val measurePolicy =
rememberGlimmerListMeasurePolicy(
itemProviderLambda = itemProvider,
state = state,
contentPadding = contentPadding,
reverseLayout = reverseLayout,
orientation = orientation,
horizontalAlignment = horizontalAlignment,
verticalArrangement = verticalArrangement,
verticalAlignment = verticalAlignment,
horizontalArrangement = horizontalArrangement,
)
val beyondBoundsModifier =
if (scrollEnabled) {
Modifier.lazyLayoutBeyondBoundsModifier(
state = rememberGlimmerListBeyondBoundsState(state),
beyondBoundsInfo = state.beyondBoundsInfo,
reverseLayout = reverseLayout,
orientation = orientation,
)
} else {
Modifier
}
LazyLayout(
modifier =
modifier
.then(state.remeasurementModifier)
.then(state.awaitLayoutModifier)
.autoFocus(state.autoFocusState)
.lazyLayoutSemantics(
itemProviderLambda = itemProvider,
state = semanticState,
orientation = orientation,
userScrollEnabled = scrollEnabled,
reverseScrolling = reverseLayout,
)
.then(beyondBoundsModifier)
.edgeScrim(
state = state.scrollIndicatorState,
orientation = orientation,
maxScrimSize = VerticalListDefaults.ScrimMaxHeight,
)
.scrollableArea(
state = state,
orientation = orientation,
enabled = scrollEnabled,
interactionSource = state.internalInteractionSource,
overscrollEffect = overscrollEffect,
flingBehavior = flingBehavior,
),
itemProvider = itemProvider,
measurePolicy = measurePolicy,
)
}
@Composable
private fun isScrollEnabled(userScrollEnabled: Boolean, state: ListState): Boolean {
if (userScrollEnabled) {
val derivedState =
remember(state) { derivedStateOf { state.canScrollForward || state.canScrollBackward } }
return derivedState.value
} else {
return false
}
}
```
<br />