[go: nahoru, domu]

blob: 1a506df0d171995b7a282db86eddc8b74138c132 [file] [log] [blame]
/*
* 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.staggeredgrid
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.ScrollableDefaults
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.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
/**
* Vertical staggered grid layout that composes and lays out only items currently visible on screen.
*
* @param columns description of the size and number of staggered grid columns.
* @param modifier modifier to apply to the layout.
* @param state state object that can be used to control and observe staggered grid state.
* @param contentPadding padding around the content.
* @param verticalArrangement arrangement specifying vertical spacing between items. The item
* arrangement specifics are ignored for now.
* @param horizontalArrangement arrangement specifying horizontal spacing between items. The item
* arrangement specifics are ignored for now.
* @param flingBehavior logic responsible for handling fling.
* @param userScrollEnabled whether scroll with gestures or accessibility actions are allowed. It is
* still possible to scroll programmatically through state when [userScrollEnabled] is set to false
* @param content a lambda describing the staggered grid content. Inside this block you can use
* [LazyStaggeredGridScope.items] to present list of items or [LazyStaggeredGridScope.item] for a
* single one.
*/
// todo(b/182882362): Reverse layout and arrangement support
@ExperimentalFoundationApi
@Composable
fun LazyVerticalStaggeredGrid(
columns: StaggeredGridCells,
modifier: Modifier = Modifier,
state: LazyStaggeredGridState = rememberLazyStaggeredGridState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(0.dp),
horizontalArrangement: Arrangement.Horizontal = Arrangement.spacedBy(0.dp),
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
content: LazyStaggeredGridScope.() -> Unit
) {
LazyStaggeredGrid(
modifier = modifier,
orientation = Orientation.Vertical,
state = state,
verticalArrangement = verticalArrangement,
horizontalArrangement = horizontalArrangement,
contentPadding = contentPadding,
flingBehavior = flingBehavior,
userScrollEnabled = userScrollEnabled,
slotSizesSums = rememberColumnWidthSums(columns, horizontalArrangement, contentPadding),
content = content
)
}
/** calculates prefix sums for columns used in staggered grid measure */
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun rememberColumnWidthSums(
columns: StaggeredGridCells,
horizontalArrangement: Arrangement.Horizontal,
contentPadding: PaddingValues
) = remember<Density.(Constraints) -> IntArray>(
columns,
horizontalArrangement,
contentPadding,
) {
{ constraints ->
require(constraints.maxWidth != Constraints.Infinity) {
"LazyVerticalStaggeredGrid'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()
).run {
val result = IntArray(size) { this[it] }
for (i in 1 until size) {
result[i] += result[i - 1]
}
result
}
}
}
}
/**
* Horizontal staggered grid layout that composes and lays out only items currently
* visible on screen.
*
* @param rows description of the size and number of staggered grid columns.
* @param modifier modifier to apply to the layout.
* @param state state object that can be used to control and observe staggered grid state.
* @param contentPadding padding around the content.
* @param verticalArrangement arrangement specifying vertical spacing between items. The item
* arrangement specifics are ignored for now.
* @param horizontalArrangement arrangement specifying horizontal spacing between items. The item
* arrangement specifics are ignored for now.
* @param flingBehavior logic responsible for handling fling.
* @param userScrollEnabled whether scroll with gestures or accessibility actions are allowed. It is
* still possible to scroll programmatically through state when [userScrollEnabled] is set to false
* @param content a lambda describing the staggered grid content. Inside this block you can use
* [LazyStaggeredGridScope.items] to present list of items or [LazyStaggeredGridScope.item] for a
* single one.
*/
// todo(b/182882362): Reverse layout and arrangement support
@ExperimentalFoundationApi
@Composable
fun LazyHorizontalStaggeredGrid(
rows: StaggeredGridCells,
modifier: Modifier = Modifier,
state: LazyStaggeredGridState = rememberLazyStaggeredGridState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(0.dp),
horizontalArrangement: Arrangement.Horizontal = Arrangement.spacedBy(0.dp),
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
content: LazyStaggeredGridScope.() -> Unit
) {
LazyStaggeredGrid(
modifier = modifier,
orientation = Orientation.Horizontal,
state = state,
contentPadding = contentPadding,
verticalArrangement = verticalArrangement,
horizontalArrangement = horizontalArrangement,
flingBehavior = flingBehavior,
userScrollEnabled = userScrollEnabled,
slotSizesSums = rememberRowHeightSums(rows, verticalArrangement, contentPadding),
content = content
)
}
/** calculates prefix sums for rows used in staggered grid measure */
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun rememberRowHeightSums(
rows: StaggeredGridCells,
verticalArrangement: Arrangement.Vertical,
contentPadding: PaddingValues
) = remember<Density.(Constraints) -> IntArray>(
rows,
verticalArrangement,
contentPadding,
) {
{ constraints ->
require(constraints.maxHeight != Constraints.Infinity) {
"LazyHorizontalStaggeredGrid'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()
).run {
val result = IntArray(size) { this[it] }
for (i in 1 until size) {
result[i] += result[i - 1]
}
result
}
}
}
}
/** Dsl marker for [LazyStaggeredGridScope] below **/
@DslMarker
internal annotation class LazyStaggeredGridScopeMarker
/**
* Receiver scope for itemContent in [LazyStaggeredGridScope.item]
*/
@ExperimentalFoundationApi
sealed interface LazyStaggeredGridItemScope
/**
* Receiver scope for [LazyVerticalStaggeredGrid] and [LazyHorizontalStaggeredGrid]
*/
@ExperimentalFoundationApi
@LazyStaggeredGridScopeMarker
sealed interface LazyStaggeredGridScope {
/**
* Add a single item to the staggered grid.
*
* @param key a stable and unique key representing the item. The key
* MUST be saveable via Bundle on Android. If set to null (by default), the position of the
* item will be used as a key instead.
* Using the same key for multiple items in the staggered grid is not allowed.
*
* 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.
* @param contentType a content type representing the item. Content for item of
* the same type can be reused more efficiently. null is a valid type as well and items
* of such type will be considered compatible.
* @param span a custom span for this item. Spans configure how many lanes defined by
* [StaggeredGridCells] the item will occupy. By default each item will take one lane.
* @param content composable content displayed by current item
*/
@ExperimentalFoundationApi
fun item(
key: Any? = null,
contentType: Any? = null,
span: StaggeredGridItemSpan? = null,
content: @Composable LazyStaggeredGridItemScope.() -> Unit
)
/**
* Add a [count] of items to the staggered grid.
*
* @param count number of items to add.
* @param key a factory of stable and unique keys representing the item. The key
* MUST be saveable via Bundle on Android. If set to null (by default), the position of the
* item will be used as a key instead.
* Using the same key for multiple items in the staggered grid is not allowed.
*
* 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.
* @param contentType a factory of content types representing the item. Content for item of
* the same type can be reused more efficiently. null is a valid type as well and items
* of such type will be considered compatible.
* @param span a factory of custom spans for this item. Spans configure how many lanes defined
* by [StaggeredGridCells] the item will occupy. By default each item will take one lane.
* @param itemContent composable content displayed by item on provided position
*/
fun items(
count: Int,
key: ((index: Int) -> Any)? = null,
contentType: (index: Int) -> Any? = { null },
span: ((index: Int) -> StaggeredGridItemSpan)? = null,
itemContent: @Composable LazyStaggeredGridItemScope.(index: Int) -> Unit
)
}
/**
* Add a list of items to the staggered grid.
*
* @param items a data list to present
* @param key a factory of stable and unique keys representing the item. The key
* MUST be saveable via Bundle on Android. If set to null (by default), the position of the
* item will be used as a key instead.
* Using the same key for multiple items in the staggered grid is not allowed.
*
* 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.
* @param contentType a factory of content types representing the item. Content for item of
* the same type can be reused more efficiently. null is a valid type as well and items
* of such type will be considered compatible.
* @param span a factory of custom spans for this item. Spans configure how many lanes defined
* by [StaggeredGridCells] the item will occupy. By default each item will take one lane.
* @param itemContent composable content displayed by the provided item
*/
@ExperimentalFoundationApi
inline fun <T> LazyStaggeredGridScope.items(
items: List<T>,
noinline key: ((item: T) -> Any)? = null,
crossinline contentType: (item: T) -> Any? = { null },
noinline span: ((item: T) -> StaggeredGridItemSpan)? = null,
crossinline itemContent: @Composable LazyStaggeredGridItemScope.(item: T) -> Unit
) {
items(
count = items.size,
key = key?.let {
{ index -> key(items[index]) }
},
contentType = { index -> contentType(items[index]) },
span = span?.let {
{ index -> span(items[index]) }
},
itemContent = { index -> itemContent(items[index]) }
)
}
/**
* Add a list of items with index-aware content to the staggered grid.
*
* @param items a data list to present
* @param key a factory of stable and unique keys representing the item. The key
* MUST be saveable via Bundle on Android. If set to null (by default), the position of the
* item will be used as a key instead.
* Using the same key for multiple items in the staggered grid is not allowed.
*
* 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.
* @param contentType a factory of content types representing the item. Content for item of
* the same type can be reused more efficiently. null is a valid type as well and items
* of such type will be considered compatible.
* @param span a factory of custom spans for this item. Spans configure how many lanes defined
* by [StaggeredGridCells] the item will occupy. By default each item will take one lane.
* @param itemContent composable content displayed given item and index
*/
@ExperimentalFoundationApi
inline fun <T> LazyStaggeredGridScope.itemsIndexed(
items: List<T>,
noinline key: ((index: Int, item: T) -> Any)? = null,
crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
noinline span: ((index: Int, item: T) -> StaggeredGridItemSpan)? = null,
crossinline itemContent: @Composable LazyStaggeredGridItemScope.(index: Int, item: T) -> Unit
) {
items(
count = items.size,
key = key?.let {
{ index -> key(index, items[index]) }
},
contentType = { index -> contentType(index, items[index]) },
span = span?.let {
{ index -> span(index, items[index]) }
},
itemContent = { index -> itemContent(index, items[index]) }
)
}
/**
* Add an array of items to the staggered grid.
*
* @param items a data array to present
* @param key a factory of stable and unique keys representing the item. The key
* MUST be saveable via Bundle on Android. If set to null (by default), the position of the
* item will be used as a key instead.
* Using the same key for multiple items in the staggered grid is not allowed.
*
* 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.
* @param contentType a factory of content types representing the item. Content for item of
* the same type can be reused more efficiently. null is a valid type as well and items
* of such type will be considered compatible.
* @param span a factory of custom spans for this item. Spans configure how many lanes defined
* by [StaggeredGridCells] the item will occupy. By default each item will take one lane.
* @param itemContent composable content displayed by the provided item
*/
@ExperimentalFoundationApi
inline fun <T> LazyStaggeredGridScope.items(
items: Array<T>,
noinline key: ((item: T) -> Any)? = null,
crossinline contentType: (item: T) -> Any? = { null },
noinline span: ((item: T) -> StaggeredGridItemSpan)? = null,
crossinline itemContent: @Composable LazyStaggeredGridItemScope.(item: T) -> Unit
) {
items(
count = items.size,
key = key?.let {
{ index -> key(items[index]) }
},
contentType = { index -> contentType(items[index]) },
span = span?.let {
{ index -> span(items[index]) }
},
itemContent = { index -> itemContent(items[index]) }
)
}
/**
* Add an array of items with index-aware content to the staggered grid.
*
* @param items a data array to present
* @param key a factory of stable and unique keys representing the item. The key
* MUST be saveable via Bundle on Android. If set to null (by default), the position of the
* item will be used as a key instead.
* Using the same key for multiple items in the staggered grid is not allowed.
*
* 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.
* @param contentType a factory of content types representing the item. Content for item of
* the same type can be reused more efficiently. null is a valid type as well and items
* of such type will be considered compatible.
* @param span a factory of custom spans for this item. Spans configure how many lanes defined
* by [StaggeredGridCells] the item will occupy. By default each item will take one lane.
* @param itemContent composable content displayed given item and index
*/
@ExperimentalFoundationApi
inline fun <T> LazyStaggeredGridScope.itemsIndexed(
items: Array<T>,
noinline key: ((index: Int, item: T) -> Any)? = null,
crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
noinline span: ((index: Int, item: T) -> StaggeredGridItemSpan)? = null,
crossinline itemContent: @Composable LazyStaggeredGridItemScope.(index: Int, item: T) -> Unit
) {
items(
count = items.size,
key = key?.let {
{ index -> key(index, items[index]) }
},
contentType = { index -> contentType(index, items[index]) },
span = span?.let {
{ index -> span(index, items[index]) }
},
itemContent = { index -> itemContent(index, items[index]) }
)
}