commonMain.androidx.compose.foundation.lazy.grid.LazyGridDsl.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of foundation Show documentation
Show all versions of foundation Show documentation
Higher level abstractions of the Compose UI primitives. This library is design system agnostic, providing the high-level building blocks for both application and design-system developers
The newest version!
/*
* Copyright 2022 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.compose.foundation.lazy.grid
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.internal.requirePrecondition
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
/**
* A lazy vertical grid layout. It composes only visible rows of the grid.
*
* Sample:
*
* @sample androidx.compose.foundation.samples.LazyVerticalGridSample
*
* Sample with custom item spans:
*
* @sample androidx.compose.foundation.samples.LazyVerticalGridSpanSample
* @param columns describes the count and the size of the grid's columns, see [GridCells] doc for
* more information
* @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 specify a padding around the whole content
* @param reverseLayout reverse the direction of scrolling and layout. When `true`, items will be
* laid out in the reverse order and [LazyGridState.firstVisibleItemIndex] == 0 means that grid is
* scrolled to the bottom. Note that [reverseLayout] does not change the behavior of
* [verticalArrangement], e.g. with [Arrangement.Top] (top) 123### (bottom) becomes (top) 321###
* (bottom).
* @param verticalArrangement The vertical arrangement of the layout's children
* @param horizontalArrangement The horizontal arrangement of the layout's children
* @param flingBehavior logic describing fling behavior
* @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions is
* allowed. You can still scroll programmatically using the state even when it is disabled.
* @param content the [LazyGridScope] which describes the content
*/
@Composable
fun LazyVerticalGrid(
columns: GridCells,
modifier: Modifier = Modifier,
state: LazyGridState = rememberLazyGridState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical =
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
content: LazyGridScope.() -> Unit
) {
LazyGrid(
slots = rememberColumnWidthSums(columns, horizontalArrangement, contentPadding),
modifier = modifier,
state = state,
contentPadding = contentPadding,
reverseLayout = reverseLayout,
isVertical = true,
horizontalArrangement = horizontalArrangement,
verticalArrangement = verticalArrangement,
flingBehavior = flingBehavior,
userScrollEnabled = userScrollEnabled,
content = content
)
}
/**
* A lazy horizontal grid layout. It composes only visible columns of the grid.
*
* Sample:
*
* @sample androidx.compose.foundation.samples.LazyHorizontalGridSample
*
* Sample with custom item spans:
*
* @sample androidx.compose.foundation.samples.LazyHorizontalGridSpanSample
* @param rows a class describing how cells form rows, see [GridCells] doc for more information
* @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 specify a padding around the whole content
* @param reverseLayout reverse the direction of scrolling and layout. When `true`, items are laid
* out in the reverse order and [LazyGridState.firstVisibleItemIndex] == 0 means that grid is
* scrolled to the end. Note that [reverseLayout] does not change the behavior of
* [horizontalArrangement], e.g. with [Arrangement.Start] [123###] becomes [321###].
* @param verticalArrangement The vertical arrangement of the layout's children
* @param horizontalArrangement The horizontal arrangement of the layout's children
* @param flingBehavior logic describing fling behavior
* @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions is
* allowed. You can still scroll programmatically using the state even when it is disabled.
* @param content the [LazyGridScope] which describes the content
*/
@Composable
fun LazyHorizontalGrid(
rows: GridCells,
modifier: Modifier = Modifier,
state: LazyGridState = rememberLazyGridState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
horizontalArrangement: Arrangement.Horizontal =
if (!reverseLayout) Arrangement.Start else Arrangement.End,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
content: LazyGridScope.() -> Unit
) {
LazyGrid(
slots = rememberRowHeightSums(rows, verticalArrangement, contentPadding),
modifier = modifier,
state = state,
contentPadding = contentPadding,
reverseLayout = reverseLayout,
isVertical = false,
horizontalArrangement = horizontalArrangement,
verticalArrangement = verticalArrangement,
flingBehavior = flingBehavior,
userScrollEnabled = userScrollEnabled,
content = content
)
}
/** Returns prefix sums of column widths. */
@Composable
private fun rememberColumnWidthSums(
columns: GridCells,
horizontalArrangement: Arrangement.Horizontal,
contentPadding: PaddingValues
) =
remember(
columns,
horizontalArrangement,
contentPadding,
) {
GridSlotCache { constraints ->
requirePrecondition(constraints.maxWidth != Constraints.Infinity) {
"LazyVerticalGrid's width should be bound by parent."
}
val horizontalPadding =
contentPadding.calculateStartPadding(LayoutDirection.Ltr) +
contentPadding.calculateEndPadding(LayoutDirection.Ltr)
val gridWidth = constraints.maxWidth - horizontalPadding.roundToPx()
with(columns) {
calculateCrossAxisCellSizes(gridWidth, horizontalArrangement.spacing.roundToPx())
.toIntArray()
.let { sizes ->
val positions = IntArray(sizes.size)
with(horizontalArrangement) {
arrange(gridWidth, sizes, LayoutDirection.Ltr, positions)
}
LazyGridSlots(sizes, positions)
}
}
}
}
/** Returns prefix sums of row heights. */
@Composable
private fun rememberRowHeightSums(
rows: GridCells,
verticalArrangement: Arrangement.Vertical,
contentPadding: PaddingValues
) =
remember(
rows,
verticalArrangement,
contentPadding,
) {
GridSlotCache { constraints ->
requirePrecondition(constraints.maxHeight != Constraints.Infinity) {
"LazyHorizontalGrid's height should be bound by parent."
}
val verticalPadding =
contentPadding.calculateTopPadding() + contentPadding.calculateBottomPadding()
val gridHeight = constraints.maxHeight - verticalPadding.roundToPx()
with(rows) {
calculateCrossAxisCellSizes(gridHeight, verticalArrangement.spacing.roundToPx())
.toIntArray()
.let { sizes ->
val positions = IntArray(sizes.size)
with(verticalArrangement) { arrange(gridHeight, sizes, positions) }
LazyGridSlots(sizes, positions)
}
}
}
}
// Note: Implementing function interface is prohibited in K/JS (class A: () -> Unit)
// therefore we workaround this limitation by inheriting a fun interface instead
internal fun interface LazyGridSlotsProvider {
fun invoke(density: Density, constraints: Constraints): LazyGridSlots
}
/** measurement cache to avoid recalculating row/column sizes on each scroll. */
private class GridSlotCache(private val calculation: Density.(Constraints) -> LazyGridSlots) :
LazyGridSlotsProvider {
private var cachedConstraints = Constraints()
private var cachedDensity: Float = 0f
private var cachedSizes: LazyGridSlots? = null
override fun invoke(density: Density, constraints: Constraints): LazyGridSlots {
with(density) {
if (
cachedSizes != null &&
cachedConstraints == constraints &&
cachedDensity == this.density
) {
return cachedSizes!!
}
cachedConstraints = constraints
cachedDensity = this.density
return calculation(constraints).also { cachedSizes = it }
}
}
}
/**
* This class describes the count and the sizes of columns in vertical grids, or rows in horizontal
* grids.
*/
@Stable
interface GridCells {
/**
* Calculates the number of cells and their cross axis size based on [availableSize] and
* [spacing].
*
* For example, in vertical grids, [spacing] is passed from the grid's [Arrangement.Horizontal].
* The [Arrangement.Horizontal] will also be used to arrange items in a row if the grid is wider
* than the calculated sum of columns.
*
* Note that the calculated cross axis sizes will be considered in an RTL-aware manner -- if the
* grid is vertical and the layout direction is RTL, the first width in the returned list will
* correspond to the rightmost column.
*
* @param availableSize available size on cross axis, e.g. width of [LazyVerticalGrid].
* @param spacing cross axis spacing, e.g. horizontal spacing for [LazyVerticalGrid]. The
* spacing is passed from the corresponding [Arrangement] param of the lazy grid.
*/
fun Density.calculateCrossAxisCellSizes(availableSize: Int, spacing: Int): List
/**
* Defines a grid with fixed number of rows or columns.
*
* For example, for the vertical [LazyVerticalGrid] Fixed(3) would mean that there are 3 columns
* 1/3 of the parent width.
*/
class Fixed(private val count: Int) : GridCells {
init {
requirePrecondition(count > 0) { "Provided count should be larger than zero" }
}
override fun Density.calculateCrossAxisCellSizes(
availableSize: Int,
spacing: Int
): List {
return calculateCellsCrossAxisSizeImpl(availableSize, count, spacing)
}
override fun hashCode(): Int {
return -count // Different sign from Adaptive.
}
override fun equals(other: Any?): Boolean {
return other is Fixed && count == other.count
}
}
/**
* Defines a grid with as many rows or columns as possible on the condition that every cell has
* at least [minSize] space and all extra space distributed evenly.
*
* For example, for the vertical [LazyVerticalGrid] Adaptive(20.dp) would mean that there will
* be as many columns as possible and every column will be at least 20.dp and all the columns
* will have equal width. If the screen is 88.dp wide then there will be 4 columns 22.dp each.
*/
class Adaptive(private val minSize: Dp) : GridCells {
init {
requirePrecondition(minSize > 0.dp) { "Provided min size should be larger than zero." }
}
override fun Density.calculateCrossAxisCellSizes(
availableSize: Int,
spacing: Int
): List {
val count = maxOf((availableSize + spacing) / (minSize.roundToPx() + spacing), 1)
return calculateCellsCrossAxisSizeImpl(availableSize, count, spacing)
}
override fun hashCode(): Int {
return minSize.hashCode()
}
override fun equals(other: Any?): Boolean {
return other is Adaptive && minSize == other.minSize
}
}
/**
* Defines a grid with as many rows or columns as possible on the condition that every cell
* takes exactly [size] space. The remaining space will be arranged through [LazyGrid]
* arrangements on corresponding axis. If [size] is larger than container size, the cell will be
* size to match the container.
*
* For example, for the vertical [LazyGrid] FixedSize(20.dp) would mean that there will be as
* many columns as possible and every column will be exactly 20.dp. If the screen is 88.dp wide
* tne there will be 4 columns 20.dp each with remaining 8.dp distributed through
* [Arrangement.Horizontal].
*/
class FixedSize(private val size: Dp) : GridCells {
init {
requirePrecondition(size > 0.dp) { "Provided size should be larger than zero." }
}
override fun Density.calculateCrossAxisCellSizes(
availableSize: Int,
spacing: Int
): List {
val cellSize = size.roundToPx()
return if (cellSize + spacing < availableSize + spacing) {
val cellCount = (availableSize + spacing) / (cellSize + spacing)
List(cellCount) { cellSize }
} else {
List(1) { availableSize }
}
}
override fun hashCode(): Int {
return size.hashCode()
}
override fun equals(other: Any?): Boolean {
return other is FixedSize && size == other.size
}
}
}
private fun calculateCellsCrossAxisSizeImpl(
gridSize: Int,
slotCount: Int,
spacing: Int
): List {
val gridSizeWithoutSpacing = gridSize - spacing * (slotCount - 1)
val slotSize = gridSizeWithoutSpacing / slotCount
val remainingPixels = gridSizeWithoutSpacing % slotCount
return List(slotCount) { slotSize + if (it < remainingPixels) 1 else 0 }
}
/** Receiver scope which is used by [LazyVerticalGrid]. */
@LazyGridScopeMarker
sealed interface LazyGridScope {
/**
* Adds a single item to the scope.
*
* @param key a stable and unique key representing the item. Using the same key for multiple
* items in the grid is not allowed. Type of the key should be saveable via Bundle on Android.
* If null is passed the position in the grid will represent the key. When you specify the key
* the scroll position will be maintained based on the key, which means if you add/remove
* items before the current visible item the item with the given key will be kept as the first
* visible one. This can be overridden by calling [LazyGridState.requestScrollToItem].
* @param span the span of the item. Default is 1x1. It is good practice to leave it `null` when
* this matches the intended behavior, as providing a custom implementation impacts
* performance
* @param contentType the type of the content of this item. The item compositions of the same
* type could be reused more efficiently. Note that null is a valid type and items of such
* type will be considered compatible.
* @param content the content of the item
*/
fun item(
key: Any? = null,
span: (LazyGridItemSpanScope.() -> GridItemSpan)? = null,
contentType: Any? = null,
content: @Composable LazyGridItemScope.() -> Unit
)
/**
* Adds a [count] of items.
*
* @param count the items count
* @param key a factory of stable and unique keys representing the item. Using the same key for
* multiple items in the grid is not allowed. Type of the key should be saveable via Bundle on
* Android. If null is passed the position in the grid will represent the key. When you
* specify the key the scroll position will be maintained based on the key, which means if you
* add/remove items before the current visible item the item with the given key will be kept
* as the first visible one.This can be overridden by calling
* [LazyGridState.requestScrollToItem].
* @param span define custom spans for the items. Default is 1x1. It is good practice to leave
* it `null` when this matches the intended behavior, as providing a custom implementation
* impacts performance
* @param contentType a factory of the content types for the item. The item compositions of the
* same type could be reused more efficiently. Note that null is a valid type and items of
* such type will be considered compatible.
* @param itemContent the content displayed by a single item
*/
fun items(
count: Int,
key: ((index: Int) -> Any)? = null,
span: (LazyGridItemSpanScope.(index: Int) -> GridItemSpan)? = null,
contentType: (index: Int) -> Any? = { null },
itemContent: @Composable LazyGridItemScope.(index: Int) -> Unit
)
/**
* Adds a sticky header item, which will remain pinned even when scrolling after it. The header
* will remain pinned until the next header will take its place. Sticky Headers are full span
* items, that is, they will occupy [LazyGridItemSpanScope.maxLineSpan].
*
* @sample androidx.compose.foundation.samples.StickyHeaderGridSample
* @param key a stable and unique key representing the item. Using the same key for multiple
* items in the list is not allowed. Type of the key should be saveable via Bundle on Android.
* If null is passed the position in the list will represent the key. When you specify the key
* the scroll position will be maintained based on the key, which means if you add/remove
* items before the current visible item the item with the given key will be kept as the first
* visible one. This can be overridden by calling 'requestScrollToItem' on the
* 'LazyGridState'.
* @param contentType the type of the content of this item. The item compositions of the same
* type could be reused more efficiently. Note that null is a valid type and items of such
* type will be considered compatible.
* @param content the content of the header. The header index is provided, this is the item
* position within the total set of items in this lazy list (the global index).
*/
fun stickyHeader(
key: Any? = null,
contentType: Any? = null,
content: @Composable LazyGridItemScope.(Int) -> Unit
)
}
/**
* Adds a list of items.
*
* @param items the data list
* @param key a factory of stable and unique keys representing the item. Using the same key for
* multiple items in the grid is not allowed. Type of the key should be saveable via Bundle on
* Android. If null is passed the position in the grid will represent the key. When you specify
* the key the scroll position will be maintained based on the key, which means if you add/remove
* items before the current visible item the item with the given key will be kept as the first
* visible one. This can be overridden by calling [LazyGridState.requestScrollToItem].
* @param span define custom spans for the items. Default is 1x1. It is good practice to leave it
* `null` when this matches the intended behavior, as providing a custom implementation impacts
* performance
* @param contentType a factory of the content types for the item. The item compositions of the same
* type could be reused more efficiently. Note that null is a valid type and items of such type
* will be considered compatible.
* @param itemContent the content displayed by a single item
*/
inline fun LazyGridScope.items(
items: List,
noinline key: ((item: T) -> Any)? = null,
noinline span: (LazyGridItemSpanScope.(item: T) -> GridItemSpan)? = null,
noinline contentType: (item: T) -> Any? = { null },
crossinline itemContent: @Composable LazyGridItemScope.(item: T) -> Unit
) =
items(
count = items.size,
key = if (key != null) { index: Int -> key(items[index]) } else null,
span =
if (span != null) {
{ span(items[it]) }
} else null,
contentType = { index: Int -> contentType(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 key a factory of stable and unique keys representing the item. Using the same key for
* multiple items in the grid is not allowed. Type of the key should be saveable via Bundle on
* Android. If null is passed the position in the grid will represent the key. When you specify
* the key the scroll position will be maintained based on the key, which means if you add/remove
* items before the current visible item the item with the given key will be kept as the first
* visible one. This can be overridden by calling [LazyGridState.requestScrollToItem].
* @param span define custom spans for the items. Default is 1x1. It is good practice to leave it
* `null` when this matches the intended behavior, as providing a custom implementation impacts
* performance
* @param contentType a factory of the content types for the item. The item compositions of the same
* type could be reused more efficiently. Note that null is a valid type and items of such type
* will be considered compatible.
* @param itemContent the content displayed by a single item
*/
inline fun LazyGridScope.itemsIndexed(
items: List,
noinline key: ((index: Int, item: T) -> Any)? = null,
noinline span: (LazyGridItemSpanScope.(index: Int, item: T) -> GridItemSpan)? = null,
crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
crossinline itemContent: @Composable LazyGridItemScope.(index: Int, item: T) -> Unit
) =
items(
count = items.size,
key = if (key != null) { index: Int -> key(index, items[index]) } else null,
span =
if (span != null) {
{ span(it, items[it]) }
} else null,
contentType = { index -> contentType(index, items[index]) }
) {
itemContent(it, items[it])
}
/**
* Adds an array of items.
*
* @param items the data array
* @param key a factory of stable and unique keys representing the item. Using the same key for
* multiple items in the grid is not allowed. Type of the key should be saveable via Bundle on
* Android. If null is passed the position in the grid will represent the key. When you specify
* the key the scroll position will be maintained based on the key, which means if you add/remove
* items before the current visible item the item with the given key will be kept as the first
* visible one.This can be overridden by calling [LazyGridState.requestScrollToItem].
* @param span define custom spans for the items. Default is 1x1. It is good practice to leave it
* `null` when this matches the intended behavior, as providing a custom implementation impacts
* performance
* @param contentType a factory of the content types for the item. The item compositions of the same
* type could be reused more efficiently. Note that null is a valid type and items of such type
* will be considered compatible.
* @param itemContent the content displayed by a single item
*/
inline fun LazyGridScope.items(
items: Array,
noinline key: ((item: T) -> Any)? = null,
noinline span: (LazyGridItemSpanScope.(item: T) -> GridItemSpan)? = null,
noinline contentType: (item: T) -> Any? = { null },
crossinline itemContent: @Composable LazyGridItemScope.(item: T) -> Unit
) =
items(
count = items.size,
key = if (key != null) { index: Int -> key(items[index]) } else null,
span =
if (span != null) {
{ span(items[it]) }
} else null,
contentType = { index: Int -> contentType(items[index]) }
) {
itemContent(items[it])
}
/**
* Adds an array of items where the content of an item is aware of its index.
*
* @param items the data array
* @param key a factory of stable and unique keys representing the item. Using the same key for
* multiple items in the grid is not allowed. Type of the key should be saveable via Bundle on
* Android. If null is passed the position in the grid will represent the key. When you specify
* the key the scroll position will be maintained based on the key, which means if you add/remove
* items before the current visible item the item with the given key will be kept as the first
* visible one. This can be overridden by calling [LazyGridState.requestScrollToItem].
* @param span define custom spans for the items. Default is 1x1. It is good practice to leave it
* `null` when this matches the intended behavior, as providing a custom implementation impacts
* performance
* @param contentType a factory of the content types for the item. The item compositions of the same
* type could be reused more efficiently. Note that null is a valid type and items of such type
* will be considered compatible.
* @param itemContent the content displayed by a single item
*/
inline fun LazyGridScope.itemsIndexed(
items: Array,
noinline key: ((index: Int, item: T) -> Any)? = null,
noinline span: (LazyGridItemSpanScope.(index: Int, item: T) -> GridItemSpan)? = null,
crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
crossinline itemContent: @Composable LazyGridItemScope.(index: Int, item: T) -> Unit
) =
items(
count = items.size,
key = if (key != null) { index: Int -> key(index, items[index]) } else null,
span =
if (span != null) {
{ span(it, items[it]) }
} else null,
contentType = { index -> contentType(index, items[index]) }
) {
itemContent(it, items[it])
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy