LazyLayout
Deprecated Please use overload with LazyLayoutMeasurePolicy
@ExperimentalFoundationApi
@Composable
fun LazyLayout(
itemProvider: () -> LazyLayoutItemProvider,
modifier: Modifier = Modifier,
prefetchState: LazyLayoutPrefetchState? = null,
measurePolicy: LazyLayoutMeasureScope.(Constraints) -> MeasureResult,
) = LazyLayout(itemProvider, modifier, prefetchState, LazyLayoutMeasurePolicy(measurePolicy))
A layout that only composes and lays out currently needed items. Can be used to build efficient
complex layouts. Currently needed items depend on the LazyLayout implementation, that is, on how
the LazyLayoutMeasurePolicy
is implemented. Composing items during the measure pass is the
signal to indicate which items are "currently needed". In general, only visible items are
considered needed, but additional items may be requested by calling
LazyLayoutMeasureScope.compose
.
This is a low level API for building efficient complex layouts, for a ready-to-use linearly
scrollable lazy layout implementation see androidx.compose.foundation.lazy.LazyRow
and
androidx.compose.foundation.lazy.LazyRow
. For a grid-like scrollable lazy layout, see
androidx.compose.foundation.lazy.grid.LazyVerticalGrid
and
androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
. For a pager-like lazy layout, see
androidx.compose.foundation.pager.VerticalPager
and
androidx.compose.foundation.pager.HorizontalPager
For a basic lazy layout sample, see:
For a scrollable lazy layout, see:
Parameters
itemProvider | lambda producing an item provider containing all the needed info about the items which could be used to compose and measure items as part of measurePolicy . |
modifier | to apply on the layout |
prefetchState | allows to schedule items for prefetching |
measurePolicy | Measure policy which allows to only compose and measure needed items. |
@Composable
fun LazyLayout(
itemProvider: () -> LazyLayoutItemProvider,
modifier: Modifier = Modifier,
prefetchState: LazyLayoutPrefetchState? = null,
measurePolicy: LazyLayoutMeasurePolicy,
)
A layout that only composes and lays out currently needed items. Can be used to build efficient
complex layouts. Currently needed items depend on the LazyLayout implementation, that is, on how
the LazyLayoutMeasurePolicy
is implemented. Composing items during the measure pass is the
signal to indicate which items are "currently needed". In general, only visible items are
considered needed, but additional items may be requested by calling
LazyLayoutMeasureScope.compose
.
This is a low level API for building efficient complex layouts, for a ready-to-use linearly
scrollable lazy layout implementation see androidx.compose.foundation.lazy.LazyRow
and
androidx.compose.foundation.lazy.LazyRow
. For a grid-like scrollable lazy layout, see
androidx.compose.foundation.lazy.grid.LazyVerticalGrid
and
androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
. For a pager-like lazy layout, see
androidx.compose.foundation.pager.VerticalPager
and
androidx.compose.foundation.pager.HorizontalPager
For a basic lazy layout sample, see:
For a scrollable lazy layout, see:
Parameters
itemProvider | lambda producing an item provider containing all the needed info about the items which could be used to compose and measure items as part of measurePolicy . This is the bridge between your item data source and the LazyLayout and is implemented as a lambda to promote a performant implementation. State backed implementations of LazyLayoutItemProvider are supported, though it is encouraged to implement this as an immutable entity that will return a new instance in case the dataset updates. |
modifier | to apply on the layout |
prefetchState | allows to schedule items for prefetching. See LazyLayoutPrefetchState on how to control prefetching. Passing null will disable prefetching. |
measurePolicy | Measure policy which allows to only compose and measure needed items. |
Code Examples
LazyLayoutSample
/** A Lazy Layout that will place items right to left with scrolling support. */
@Preview
@Composable
fun LazyLayoutSample() {
val items = remember { (0..100).toList().map { it.toString() } }
// Create an item provider
val itemProvider = remember {
{
object : LazyLayoutItemProvider {
override val itemCount: Int
get() = 100
@Composable
override fun Item(index: Int, key: Any) {
Box(
modifier =
Modifier.width(100.dp)
.height(100.dp)
.background(color = if (index % 2 == 0) Color.Red else Color.Green)
) {
Text(text = items[index])
}
}
}
}
}
LazyLayout(modifier = Modifier.size(500.dp), itemProvider = itemProvider) { constraints ->
// plug the measure policy, this is how we create and layout items.
val placeablesCache = mutableListOf<Pair<Placeable, Int>>()
fun Placeable.mainAxisSize() = width
fun Placeable.crossAxisSize() = height
val childConstraints =
Constraints(maxWidth = Constraints.Infinity, maxHeight = constraints.maxHeight)
var currentItemIndex = 0
var crossAxisSize = 0
var mainAxisSize = 0
// measure items until we either fill in the space or run out of items.
while (mainAxisSize < constraints.maxWidth && currentItemIndex < items.size) {
val itemPlaceables = compose(currentItemIndex).map { it.measure(childConstraints) }
for (item in itemPlaceables) {
// save placeable to be placed later.
placeablesCache.add(item to mainAxisSize)
mainAxisSize += item.mainAxisSize() // item size contributes to main axis size
// cross axis size will the size of tallest/widest item
crossAxisSize = maxOf(crossAxisSize, item.crossAxisSize())
}
currentItemIndex++
}
val layoutWidth = minOf(mainAxisSize, constraints.maxHeight)
val layoutHeight = crossAxisSize
layout(layoutWidth, layoutHeight) {
// since this is a linear list all items are placed on the same cross-axis position
for ((placeable, position) in placeablesCache) {
placeable.place(position, 0)
}
}
}
}
LazyLayoutScrollableSample
/** A simple Layout that will place items right to left with scrolling support. */
@Preview
@Composable
fun LazyLayoutScrollableSample() {
val items = remember { (0..100).toList().map { it.toString() } }
/** Saves the deltas from the scroll gesture. */
var scrollAmount by remember { mutableFloatStateOf(0f) }
/**
* Lazy Layout measurement starts at a known position identified by the first item's position.
*/
var firstVisibleItemIndex by remember { mutableIntStateOf(0) }
var firstVisibleItemOffset by remember { mutableIntStateOf(0) }
// A scrollable state is needed for scroll support
val scrollableState = rememberScrollableState { delta ->
scrollAmount += delta
delta // assume we consumed everything.
}
// Create an item provider
val itemProvider = {
object : LazyLayoutItemProvider {
override val itemCount: Int
get() = items.size
@Composable
override fun Item(index: Int, key: Any) {
Box(
modifier =
Modifier.width(100.dp)
.height(100.dp)
.background(color = if (index % 2 == 0) Color.Red else Color.Green)
) {
Text(text = items[index])
}
}
}
}
fun LazyLayoutMeasureScope.createItem(
index: Int,
constraints: Constraints,
placeablesCache: IntObjectMap<Placeable>,
): Placeable {
val cachedPlaceable = placeablesCache[index]
return if (cachedPlaceable == null) {
val measurables = compose(index)
require(measurables.size == 1) { "Only one composable item emission is supported." }
measurables[0].measure(constraints)
} else {
cachedPlaceable
}
}
fun LazyLayoutMeasureScope.measureLayout(
scrollAmount: Float,
firstVisibleItemIndex: Int,
firstVisibleItemOffset: Int,
itemCount: Int,
placeablesCache: MutableIntObjectMap<Placeable>,
containerConstraints: Constraints,
updatePositions: (Int, Int) -> Unit,
): MeasureResult {
/** 1) Resolve layout information and constraints. */
val viewportSize = containerConstraints.maxWidth
// children are only restricted on the cross axis size.
val childConstraints =
Constraints(maxWidth = Constraints.Infinity, maxHeight = viewportSize)
/** 2) Initialize data holders for the pass. */
// All items that will be placed in this layout in the layout pass.
val placeables = mutableListOf<Pair<Placeable, Int>>()
// The anchor points, we start from a known position, the position of the first item.
var currentFirstVisibleItemIndex = firstVisibleItemIndex
var currentFirstVisibleItemOffset = firstVisibleItemOffset
// represents the real amount of scroll we applied as a result of this measure pass.
val scrollDelta = scrollAmount.fastRoundToInt()
// The amount of space available to items.
val maxOffset = containerConstraints.maxWidth
// tallest item from the ones we've created in this layout.This will determined the cross
// axis
// size of this layout
var maxCrossAxis = 0
/** 3) Apply Scroll */
// applying the whole requested scroll offset.
currentFirstVisibleItemOffset -= scrollDelta
// if the current scroll offset is less than minimally possible we reset. Imagine we've
// reached
// the bounds at the start of the layout and we tried to scroll back.
if (currentFirstVisibleItemIndex == 0 && currentFirstVisibleItemOffset < 0) {
currentFirstVisibleItemOffset = 0
}
/** 4) Consider we scrolled back */
while (currentFirstVisibleItemOffset < 0 && currentFirstVisibleItemIndex > 0) {
val previous = currentFirstVisibleItemIndex - 1
val measuredItem = createItem(previous, childConstraints, placeablesCache)
placeables.add(0, measuredItem to currentFirstVisibleItemOffset)
maxCrossAxis = maxOf(maxCrossAxis, measuredItem.height)
currentFirstVisibleItemOffset += measuredItem.width
currentFirstVisibleItemIndex = previous
}
// if we were scrolled backward, but there were not enough items before. this means
// not the whole scroll was consumed
if (currentFirstVisibleItemOffset < 0) {
val notConsumedScrollDelta = -currentFirstVisibleItemOffset
currentFirstVisibleItemOffset = 0
}
/** 5) Compose forward. */
var index = currentFirstVisibleItemIndex
var currentMainAxisOffset = -currentFirstVisibleItemOffset
// first we need to skip items we already composed while composing backward
var indexInVisibleItems = 0
while (indexInVisibleItems < placeables.size) {
if (currentMainAxisOffset >= maxOffset) {
// this item is out of the bounds and will not be visible.
placeables.removeAt(indexInVisibleItems)
} else {
index++
currentMainAxisOffset += placeables[indexInVisibleItems].first.width
indexInVisibleItems++
}
}
// then composing visible items forward until we fill the whole viewport.
while (
index < itemCount &&
(currentMainAxisOffset < maxOffset ||
currentMainAxisOffset <= 0 ||
placeables.isEmpty())
) {
val measuredItem = createItem(index, childConstraints, placeablesCache)
val measuredItemPosition = currentMainAxisOffset
currentMainAxisOffset += measuredItem.width
if (currentMainAxisOffset <= 0 && index != itemCount - 1) {
// this item is offscreen and will not be visible. advance firstVisibleItemIndex
currentFirstVisibleItemIndex = index + 1
currentFirstVisibleItemOffset -= measuredItem.width
} else {
maxCrossAxis = maxOf(maxCrossAxis, measuredItem.height)
placeables.add(measuredItem to measuredItemPosition)
}
index++
}
val layoutWidth = containerConstraints.constrainWidth(currentMainAxisOffset)
val layoutHeight = containerConstraints.constrainHeight(maxCrossAxis)
/** 7) Update state information. */
updatePositions(currentFirstVisibleItemIndex, currentFirstVisibleItemOffset)
/** 8) Perform layout. */
return layout(layoutWidth, layoutHeight) {
// since this is a linear list all items are placed on the same cross-axis position
for ((placeable, position) in placeables) {
placeable.place(position, 0)
}
}
}
LazyLayout(
modifier = Modifier.size(500.dp).scrollable(scrollableState, Orientation.Horizontal),
itemProvider = itemProvider,
) { constraints ->
// plug the measure policy, this is how we create and layout items.
val placeablesCache = mutableIntObjectMapOf<Placeable>()
measureLayout(
scrollAmount, // will trigger a re-measure when it changes.
firstVisibleItemIndex,
firstVisibleItemOffset,
items.size,
placeablesCache,
constraints,
) { newFirstVisibleItemIndex, newFirstVisibleItemOffset ->
// update the information about the anchor item.
firstVisibleItemIndex = newFirstVisibleItemIndex
firstVisibleItemOffset = newFirstVisibleItemOffset
// resets the scrolling state
scrollAmount = 0f
}
}
}