blob: 360aa6e2187dfb16b2bbbcecacde0a109d8ad58b [file]
/*
* Copyright 2023 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.
*/
@file:Suppress("DEPRECATION")
package androidx.compose.foundation.layout
import androidx.annotation.FloatRange
import androidx.compose.foundation.layout.internal.requirePrecondition
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.layout.SubcomposeMeasureScope
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/**
* [ContextualFlowRow] is a specialized version of the [FlowRow] layout. It is designed to enable
* users to make contextual decisions during the construction of [FlowRow] layouts.
*
* This component is particularly advantageous when dealing with a large collection of items,
* allowing for efficient management and display. Unlike traditional [FlowRow] that composes all
* items regardless of their visibility, ContextualFlowRow smartly limits composition to only those
* items that are visible within its constraints, such as [maxLines] or `maxHeight`. This approach
* ensures optimal performance and resource utilization by composing fewer items than the total
* number available, based on the current context and display parameters.
*
* While maintaining the core functionality of the standard [FlowRow], [ContextualFlowRow] operates
* on an index-based system and composes items sequentially, one after another. This approach
* provides a perfect way to make contextual decisions and can be an easier way to handle problems
* such as dynamic see more buttons such as (N+ buttons).
*
* Example:
*
* @sample androidx.compose.foundation.layout.samples.ContextualFlowRowMaxLineDynamicSeeMore
* @param itemCount The total number of item composable
* @param modifier The modifier to be applied to the Row.
* @param horizontalArrangement The horizontal arrangement of the layout's children.
* @param verticalArrangement The vertical arrangement of the layout's virtual rows.
* @param itemVerticalAlignment The cross axis/vertical alignment of an item in the column.
* @param maxItemsInEachRow The maximum number of items per row
* @param maxLines The maximum number of rows
* @param overflow The strategy to handle overflowing items
* @param content The indexed-based content of [ContextualFlowRowScope]
* @see FlowRow
* @see ContextualFlowColumn
*/
@Deprecated("ContextualFlowLayouts are no longer maintained")
@Composable
@ExperimentalLayoutApi
fun ContextualFlowRow(
itemCount: Int,
modifier: Modifier = Modifier,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
itemVerticalAlignment: Alignment.Vertical = Alignment.Top,
maxItemsInEachRow: Int = Int.MAX_VALUE,
maxLines: Int = Int.MAX_VALUE,
overflow: ContextualFlowRowOverflow = ContextualFlowRowOverflow.Clip,
content: @Composable ContextualFlowRowScope.(index: Int) -> Unit,
) {
val overflowState = remember(overflow) { overflow.createOverflowState() }
val list: List<@Composable () -> Unit> =
remember(overflow) {
val mutableList: MutableList<@Composable () -> Unit> = mutableListOf()
overflow.addOverflowComposables(overflowState, mutableList)
mutableList
}
val measurePolicy =
contextualRowMeasurementHelper(
horizontalArrangement,
verticalArrangement,
itemVerticalAlignment,
maxItemsInEachRow,
maxLines,
overflowState,
itemCount,
list,
) { index, info ->
val scope =
ContextualFlowRowScopeImpl(
info.lineIndex,
info.positionInLine,
maxWidthInLine = info.maxMainAxisSize,
maxHeight = info.maxCrossAxisSize,
)
scope.content(index)
}
SubcomposeLayout(modifier = modifier, measurePolicy = measurePolicy)
}
/**
* [ContextualFlowColumn] is a specialized version of the [FlowColumn] layout. It is designed to
* enable users to make contextual decisions during the construction of [FlowColumn] layouts.
*
* This component is particularly advantageous when dealing with a large collection of items,
* allowing for efficient management and display. Unlike traditional [FlowColumn] that composes all
* items regardless of their visibility, ContextualFlowColumn smartly limits composition to only
* those items that are visible within its constraints, such as [maxLines] or `maxWidth`. This
* approach ensures optimal performance and resource utilization by composing fewer items than the
* total number available, based on the current context and display parameters.
*
* While maintaining the core functionality of the standard [FlowColumn], [ContextualFlowColumn]
* operates on an index-based system and composes items sequentially, one after another. This
* approach provides a perfect way to make contextual decisions and can be an easier way to handle
* problems such as dynamic see more buttons such as (N+ buttons).
*
* Example:
*
* @sample androidx.compose.foundation.layout.samples.ContextualFlowColMaxLineDynamicSeeMore
* @param itemCount The total number of item composable
* @param modifier The modifier to be applied to the Row.
* @param verticalArrangement The vertical arrangement of the layout's virtual column.
* @param horizontalArrangement The horizontal arrangement of the layout's children.
* @param itemHorizontalAlignment The cross axis/horizontal alignment of an item in the column.
* @param maxItemsInEachColumn The maximum number of items per column
* @param maxLines The maximum number of columns
* @param overflow The straoadtegy to handle overflowing items
* @param content The indexed-based content of [ContextualFlowColumnScope]
* @see FlowColumn
* @see ContextualFlowRow
*/
@Deprecated("ContextualFlowLayouts are no longer maintained")
@Composable
@ExperimentalLayoutApi
fun ContextualFlowColumn(
itemCount: Int,
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
itemHorizontalAlignment: Alignment.Horizontal = Alignment.Start,
maxItemsInEachColumn: Int = Int.MAX_VALUE,
maxLines: Int = Int.MAX_VALUE,
overflow: ContextualFlowColumnOverflow = ContextualFlowColumnOverflow.Clip,
content: @Composable ContextualFlowColumnScope.(index: Int) -> Unit,
) {
val overflowState = remember(overflow) { overflow.createOverflowState() }
val list: List<@Composable () -> Unit> =
remember(overflow) {
val mutableList: MutableList<@Composable () -> Unit> = mutableListOf()
overflow.addOverflowComposables(overflowState, mutableList)
mutableList
}
val measurePolicy =
contextualColumnMeasureHelper(
verticalArrangement,
horizontalArrangement,
itemHorizontalAlignment,
maxItemsInEachColumn,
maxLines,
overflowState,
itemCount,
list,
) { index, info ->
val scope =
ContextualFlowColumnScopeImpl(
info.lineIndex,
info.positionInLine,
maxHeightInLine = info.maxMainAxisSize,
maxWidth = info.maxCrossAxisSize,
)
scope.content(index)
}
SubcomposeLayout(modifier = modifier, measurePolicy = measurePolicy)
}
/** Defines the scope for items within a [ContextualFlowRow]. */
@Deprecated("ContextualFlowLayouts are no longer maintained")
@LayoutScopeMarker
@Stable
@ExperimentalLayoutApi
interface ContextualFlowRowScope : RowScope {
/**
* Have the item fill (possibly only partially) the max height of the tallest item in the row it
* was placed in, within the [FlowRow].
*
* @param fraction The fraction of the max height of the tallest item between `0` and `1`,
* inclusive.
*
* Example usage:
*
* @sample androidx.compose.foundation.layout.samples.SimpleFlowRow_EqualHeight
*/
@ExperimentalLayoutApi
fun Modifier.fillMaxRowHeight(@FloatRange(from = 0.0, to = 1.0) fraction: Float = 1f): Modifier
/**
* Identifies the row or column index where the UI component(s) are to be placed, provided they
* do not exceed the specified [maxWidthInLine] and [maxHeight] for that row or column.
*
* Should the component(s) surpass these dimensions, their placement may shift to the subsequent
* row/column or they may be omitted from display, contingent upon the defined constraints.
*
* Example:
*
* @sample androidx.compose.foundation.layout.samples.ContextualFlowRow_ItemPosition
* @sample androidx.compose.foundation.layout.samples.ContextualFlowColumn_ItemPosition
*/
val lineIndex: Int
/**
* Marks the index within the current row/column where the next component is to be inserted,
* assuming it conforms to the row's or column's [maxWidthInLine] and [maxHeight] limitations.
*
* In scenarios where multiple UI components are returned in one index call, this parameter is
* relevant solely to the first returned UI component, presuming it complies with the row's or
* column's defined constraints.
*
* Example:
*
* @sample androidx.compose.foundation.layout.samples.ContextualFlowRow_ItemPosition
* @sample androidx.compose.foundation.layout.samples.ContextualFlowColumn_ItemPosition
*/
val indexInLine: Int
/**
* Specifies the maximum permissible width (main-axis) for the upcoming UI component at the
* given [lineIndex] and [indexInLine]. Exceeding this width may result in the component being
* reallocated to the following row within the [ContextualFlowRow] structure, subject to
* existing constraints.
*/
val maxWidthInLine: Dp
/**
* Determines the maximum allowable height (cross-axis) for the forthcoming UI component,
* aligned with its [lineIndex] and [indexInLine]. Should this height threshold be exceeded, the
* component's visibility will depend on the overflow settings, potentially leading to its
* exclusion.
*/
val maxHeight: Dp
}
/** Scope for the overflow [ContextualFlowRow]. */
@Deprecated("ContextualFlowLayouts are no longer maintained")
@LayoutScopeMarker
@Stable
@ExperimentalLayoutApi
interface ContextualFlowRowOverflowScope : FlowRowOverflowScope
/** Scope for the overflow [ContextualFlowColumn]. */
@Deprecated("ContextualFlowLayouts are no longer maintained")
@LayoutScopeMarker
@Stable
@ExperimentalLayoutApi
interface ContextualFlowColumnOverflowScope : FlowColumnOverflowScope
/** Provides a scope for items within a [ContextualFlowColumn]. */
@Deprecated("ContextualFlowLayouts are no longer maintained")
@LayoutScopeMarker
@Stable
@ExperimentalLayoutApi
interface ContextualFlowColumnScope : ColumnScope {
/**
* Have the item fill (possibly only partially) the max width of the widest item in the column
* it was placed in, within the [FlowColumn].
*
* @param fraction The fraction of the max width of the widest item between `0` and `1`,
* inclusive.
*
* Example usage:
*
* @sample androidx.compose.foundation.layout.samples.SimpleFlowColumn_EqualWidth
*/
@ExperimentalLayoutApi
fun Modifier.fillMaxColumnWidth(
@FloatRange(from = 0.0, to = 1.0) fraction: Float = 1f
): Modifier
/**
* Identifies the row or column index where the UI component(s) are to be placed, provided they
* do not exceed the specified [maxWidth] and [maxHeightInLine] for that row or column.
*
* Should the component(s) surpass these dimensions, their placement may shift to the subsequent
* row/column or they may be omitted from display, contingent upon the defined constraints.
*
* Example:
*
* @sample androidx.compose.foundation.layout.samples.ContextualFlowRow_ItemPosition
* @sample androidx.compose.foundation.layout.samples.ContextualFlowColumn_ItemPosition
*/
val lineIndex: Int
/**
* Marks the index within the current row/column where the next component is to be inserted,
* assuming it conforms to the row's or column's [maxWidth] and [maxHeightInLine] limitations.
*
* In scenarios where multiple UI components are returned in one index call, this parameter is
* relevant solely to the first returned UI component, presuming it complies with the row's or
* column's defined constraints.
*
* Example:
*
* @sample androidx.compose.foundation.layout.samples.ContextualFlowRow_ItemPosition
* @sample androidx.compose.foundation.layout.samples.ContextualFlowColumn_ItemPosition
*/
val indexInLine: Int
/**
* Sets the maximum width (cross-axis dimension) that the upcoming UI component can occupy,
* based on its [lineIndex] and [indexInLine]. Exceeding this width might result in the
* component not being displayed, depending on the [ContextualFlowColumnOverflow.Visible]
* overflow configuration.
*/
val maxWidth: Dp
/**
* Establishes the maximum height (main-axis dimension) permissible for the next UI component,
* aligned with its [lineIndex] and [indexInLine]. Should the component's height exceed this
* limit, it may be shifted to the subsequent column in [ContextualFlowColumn], subject to the
* predefined constraints.
*/
val maxHeightInLine: Dp
}
@OptIn(ExperimentalLayoutApi::class)
internal class ContextualFlowRowScopeImpl(
override val lineIndex: Int,
override val indexInLine: Int,
override val maxWidthInLine: Dp,
override val maxHeight: Dp,
) : RowScope by RowScopeInstance, ContextualFlowRowScope {
override fun Modifier.fillMaxRowHeight(fraction: Float): Modifier {
requirePrecondition(fraction in 0.0f..1.0f) {
"invalid fraction $fraction; must be >= 0 and <= 1.0"
}
return this.then(FillCrossAxisSizeElement(fraction = fraction))
}
}
@OptIn(ExperimentalLayoutApi::class)
internal class ContextualFlowColumnScopeImpl(
override val lineIndex: Int,
override val indexInLine: Int,
override val maxWidth: Dp,
override val maxHeightInLine: Dp,
) : ColumnScope by ColumnScopeInstance, ContextualFlowColumnScope {
override fun Modifier.fillMaxColumnWidth(fraction: Float): Modifier {
requirePrecondition(fraction in 0.0f..1.0f) {
"invalid fraction $fraction; must be >= 0 and <= 1.0"
}
return this.then(FillCrossAxisSizeElement(fraction = fraction))
}
}
@ExperimentalLayoutApi
internal class ContextualFlowRowOverflowScopeImpl(private val state: FlowLayoutOverflowState) :
FlowRowOverflowScope by FlowRowOverflowScopeImpl(state), ContextualFlowRowOverflowScope
@ExperimentalLayoutApi
internal class ContextualFlowColumnOverflowScopeImpl(private val state: FlowLayoutOverflowState) :
FlowColumnOverflowScope by FlowColumnOverflowScopeImpl(state), ContextualFlowColumnOverflowScope
@Composable
internal fun contextualRowMeasurementHelper(
horizontalArrangement: Arrangement.Horizontal,
verticalArrangement: Arrangement.Vertical,
itemVerticalAlignment: Alignment.Vertical,
maxItemsInMainAxis: Int,
maxLines: Int,
overflowState: FlowLayoutOverflowState,
itemCount: Int,
overflowComposables: List<@Composable () -> Unit>,
getComposable: @Composable (index: Int, info: FlowLineInfo) -> Unit,
): (SubcomposeMeasureScope, Constraints) -> MeasureResult {
return remember(
horizontalArrangement,
verticalArrangement,
itemVerticalAlignment,
maxItemsInMainAxis,
maxLines,
overflowState,
itemCount,
getComposable,
) {
FlowMeasureLazyPolicy(
isHorizontal = true,
horizontalArrangement = horizontalArrangement,
mainAxisSpacing = horizontalArrangement.spacing,
crossAxisAlignment = CrossAxisAlignment.vertical(itemVerticalAlignment),
verticalArrangement = verticalArrangement,
crossAxisArrangementSpacing = verticalArrangement.spacing,
maxItemsInMainAxis = maxItemsInMainAxis,
itemCount = itemCount,
overflow = overflowState,
maxLines = maxLines,
getComposable = getComposable,
overflowComposables = overflowComposables,
)
.getMeasurePolicy()
}
}
@Composable
internal fun contextualColumnMeasureHelper(
verticalArrangement: Arrangement.Vertical,
horizontalArrangement: Arrangement.Horizontal,
itemHorizontalAlignment: Alignment.Horizontal,
maxItemsInMainAxis: Int,
maxLines: Int,
overflowState: FlowLayoutOverflowState,
itemCount: Int,
overflowComposables: List<@Composable () -> Unit>,
getComposable: @Composable (index: Int, info: FlowLineInfo) -> Unit,
): (SubcomposeMeasureScope, Constraints) -> MeasureResult {
return remember(
verticalArrangement,
horizontalArrangement,
itemHorizontalAlignment,
maxItemsInMainAxis,
maxLines,
overflowState,
itemCount,
getComposable,
) {
FlowMeasureLazyPolicy(
isHorizontal = false,
verticalArrangement = verticalArrangement,
mainAxisSpacing = verticalArrangement.spacing,
crossAxisAlignment = CrossAxisAlignment.horizontal(itemHorizontalAlignment),
horizontalArrangement = horizontalArrangement,
crossAxisArrangementSpacing = horizontalArrangement.spacing,
maxItemsInMainAxis = maxItemsInMainAxis,
itemCount = itemCount,
overflow = overflowState,
maxLines = maxLines,
overflowComposables = overflowComposables,
getComposable = getComposable,
)
.getMeasurePolicy()
}
}
/** Returns a Flow Measure Policy */
@OptIn(ExperimentalLayoutApi::class)
private data class FlowMeasureLazyPolicy(
override val isHorizontal: Boolean,
override val horizontalArrangement: Arrangement.Horizontal,
override val verticalArrangement: Arrangement.Vertical,
private val mainAxisSpacing: Dp,
override val crossAxisAlignment: CrossAxisAlignment,
private val crossAxisArrangementSpacing: Dp,
private val itemCount: Int,
private val maxLines: Int,
private val maxItemsInMainAxis: Int,
private val overflow: FlowLayoutOverflowState,
private val overflowComposables: List<@Composable () -> Unit>,
private val getComposable: @Composable (index: Int, info: FlowLineInfo) -> Unit,
) : FlowLineMeasurePolicy {
fun getMeasurePolicy(): (SubcomposeMeasureScope, Constraints) -> MeasureResult {
return { measureScope, constraints -> measureScope.measure(constraints) }
}
private fun SubcomposeMeasureScope.measure(constraints: Constraints): MeasureResult {
if (
itemCount <= 0 ||
(maxLines == 0 ||
maxItemsInMainAxis == 0 ||
constraints.maxHeight == 0 &&
overflow.type != FlowLayoutOverflow.OverflowType.Visible)
) {
return layout(0, 0) {}
}
val measurablesIterator =
ContextualFlowItemIterator(itemCount) { index, info ->
@Suppress("ComposableLambdaInMeasurePolicy")
this.subcompose(index) { getComposable(index, info) }
}
overflow.itemCount = itemCount
overflow.setOverflowMeasurables(this@FlowMeasureLazyPolicy, constraints) {
canExpand,
shownItemCount ->
val composableIndex = if (canExpand) 0 else 1
overflowComposables.getOrNull(composableIndex)?.run {
this@measure.subcompose("$canExpand$itemCount$shownItemCount", this).getOrNull(0)
}
}
return breakDownItems(
this@FlowMeasureLazyPolicy,
measurablesIterator,
mainAxisSpacing,
crossAxisArrangementSpacing,
OrientationIndependentConstraints(
constraints,
if (isHorizontal) {
LayoutOrientation.Horizontal
} else {
LayoutOrientation.Vertical
},
),
maxItemsInMainAxis,
maxLines,
overflow,
)
}
}
internal class ContextualFlowItemIterator(
private val itemCount: Int,
private val getMeasurables: (index: Int, info: FlowLineInfo) -> List<Measurable>,
) : Iterator<Measurable> {
private val _list: MutableList<Measurable> = mutableListOf()
private var itemIndex: Int = 0
private var listIndex = 0
val list: List<Measurable>
get() = _list
override fun hasNext(): Boolean {
return listIndex < list.size || itemIndex < itemCount
}
override fun next(): Measurable {
return getNext()
}
internal fun getNext(info: FlowLineInfo = FlowLineInfo()): Measurable {
// when we are at the end of the list, we fetch a new item from getMeasurables
// and add to the list.
// otherwise, we continue through the list.
return if (listIndex < list.size) {
val measurable = list[listIndex]
listIndex++
measurable
} else if (itemIndex < itemCount) {
val measurables = getMeasurables(itemIndex, info)
itemIndex++
if (measurables.isEmpty()) {
next()
} else {
val measurable = measurables.first()
_list.addAll(measurables)
listIndex++
measurable
}
} else {
throw IndexOutOfBoundsException("No item returned at index call. Index: $itemIndex")
}
}
}
/**
* Contextual Line Info for the current lazy call for [ContextualFlowRow] or [ContextualFlowColumn]
*/
internal class FlowLineInfo(
internal var lineIndex: Int = 0,
internal var positionInLine: Int = 0,
internal var maxMainAxisSize: Dp = 0.dp,
internal var maxCrossAxisSize: Dp = 0.dp,
) {
/** To allow reuse of the same object to reduce allocation, simply update the same value */
internal fun update(
lineIndex: Int,
positionInLine: Int,
maxMainAxisSize: Dp,
maxCrossAxisSize: Dp,
) {
this.lineIndex = lineIndex
this.positionInLine = positionInLine
this.maxMainAxisSize = maxMainAxisSize
this.maxCrossAxisSize = maxCrossAxisSize
}
}