When creating a Glimmer List component, refer to the following source code in
List.kt:
/* * 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 } }