blob: c613567f87fa375809286f2d2ec45bee05912f57 [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.collection.IntIntPair
import androidx.collection.mutableIntListOf
import androidx.collection.mutableIntObjectMapOf
import androidx.collection.mutableIntSetOf
import androidx.compose.foundation.layout.internal.requirePrecondition
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collection.MutableVector
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.IntrinsicMeasureScope
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.MultiContentMeasurePolicy
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.ParentDataModifierNode
import androidx.compose.ui.platform.InspectorInfo
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.util.fastCoerceAtLeast
import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.fastForEachIndexed
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
/**
* [FlowRow] is a layout that fills items from left to right (ltr) in LTR layouts or right to left
* (rtl) in RTL layouts and when it runs out of space, moves to the next "row" or "line" positioned
* on the bottom, and then continues filling items until the items run out.
*
* Example:
*
* @sample androidx.compose.foundation.layout.samples.SimpleFlowRow
*
* When a Modifier [RowScope.weight] is provided, it scales the item based on the number items that
* fall on the row it was placed in.
*
* Note that if two or more Text components are placed in a [Row], normally they should be aligned
* by their first baselines. [FlowRow] as a general purpose container does not do it automatically
* so developers need to handle this manually. This is achieved by adding a
* [RowScope.alignByBaseline] modifier to every such Text component. By default this modifier aligns
* by [androidx.compose.ui.layout.FirstBaseline]. If, however, you need to align Texts by
* [androidx.compose.ui.layout.LastBaseline] for example, use a more general [RowScope.alignBy]
* modifier.
*
* @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 max number of rows
* @param overflow The strategy to handle overflowing items
* @param content The content as a [RowScope]
* @see FlowColumn
* @see [androidx.compose.foundation.layout.Row]
*/
@Deprecated("The overflow parameter has been deprecated")
@Composable
@ExperimentalLayoutApi
fun FlowRow(
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: FlowRowOverflow = FlowRowOverflow.Clip,
content: @Composable FlowRowScope.() -> Unit,
) {
val overflowState = remember(overflow) { overflow.createOverflowState() }
val measurePolicy =
rowMeasurementMultiContentHelper(
horizontalArrangement,
verticalArrangement,
itemVerticalAlignment,
maxItemsInEachRow,
maxLines,
overflowState,
)
val list: List<@Composable () -> Unit> =
remember(overflow, content, maxLines) {
val mutableList: MutableList<@Composable () -> Unit> = mutableListOf()
mutableList.add { FlowRowScopeInstance.content() }
overflow.addOverflowComposables(overflowState, mutableList)
mutableList
}
Layout(contents = list, measurePolicy = measurePolicy, modifier = modifier)
}
/**
* [FlowRow] is a layout that fills items from left to right (ltr) in LTR layouts or right to left
* (rtl) in RTL layouts and when it runs out of space, moves to the next "row" or "line" positioned
* on the bottom, and then continues filling items until the items run out.
*
* Example:
*
* @sample androidx.compose.foundation.layout.samples.SimpleFlowRow
*
* When a Modifier [RowScope.weight] is provided, it scales the item based on the number items that
* fall on the row it was placed in.
*
* Note that if two or more Text components are placed in a [Row], normally they should be aligned
* by their first baselines. [FlowRow] as a general purpose container does not do it automatically
* so developers need to handle this manually. This is achieved by adding a
* [RowScope.alignByBaseline] modifier to every such Text component. By default this modifier aligns
* by [androidx.compose.ui.layout.FirstBaseline]. If, however, you need to align Texts by
* [androidx.compose.ui.layout.LastBaseline] for example, use a more general [RowScope.alignBy]
* modifier.
*
* @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 max number of rows
* @param content The content as a [RowScope]
* @see FlowColumn
* @see [androidx.compose.foundation.layout.Row]
*/
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun FlowRow(
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,
content: @Composable FlowRowScope.() -> Unit,
) =
FlowRow(
modifier,
horizontalArrangement,
verticalArrangement,
itemVerticalAlignment,
maxItemsInEachRow,
maxLines,
FlowRowOverflow.Clip,
content,
)
/**
* [FlowColumn] is a layout that fills items from top to bottom, and when it runs out of space on
* the bottom, moves to the next "column" or "line" on the right or left based on ltr or rtl
* layouts, and then continues filling items from top to bottom.
*
* It supports ltr in LTR layouts, by placing the first column to the left, and then moving to the
* right It supports rtl in RTL layouts, by placing the first column to the right, and then moving
* to the left
*
* Example:
*
* @sample androidx.compose.foundation.layout.samples.SimpleFlowColumn
*
* When a Modifier [ColumnScope.weight] is provided, it scales the item based on the number items
* that fall on the column it was placed in.
*
* @param modifier The modifier to be applied to the Row.
* @param verticalArrangement The vertical arrangement of the layout's children.
* @param horizontalArrangement The horizontal arrangement of the layout's virtual columns
* @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 max number of rows
* @param overflow The strategy to handle overflowing items
* @param content The content as a [ColumnScope]
* @see FlowRow
* @see ContextualFlowColumn
* @see [androidx.compose.foundation.layout.Column]
*/
@Deprecated("The overflow parameter has been deprecated")
@Composable
@ExperimentalLayoutApi
fun FlowColumn(
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: FlowColumnOverflow = FlowColumnOverflow.Clip,
content: @Composable FlowColumnScope.() -> Unit,
) {
val overflowState = remember(overflow) { overflow.createOverflowState() }
val measurePolicy =
columnMeasurementMultiContentHelper(
verticalArrangement,
horizontalArrangement,
itemHorizontalAlignment,
maxItemsInEachColumn,
maxLines,
overflowState,
)
val list: List<@Composable () -> Unit> =
remember(overflow, content, maxLines) {
val mutableList: MutableList<@Composable () -> Unit> = mutableListOf()
mutableList.add { FlowColumnScopeInstance.content() }
overflow.addOverflowComposables(overflowState, mutableList)
mutableList
}
Layout(contents = list, measurePolicy = measurePolicy, modifier = modifier)
}
/**
* [FlowColumn] is a layout that fills items from top to bottom, and when it runs out of space on
* the bottom, moves to the next "column" or "line" on the right or left based on ltr or rtl
* layouts, and then continues filling items from top to bottom.
*
* It supports ltr in LTR layouts, by placing the first column to the left, and then moving to the
* right It supports rtl in RTL layouts, by placing the first column to the right, and then moving
* to the left
*
* Example:
*
* @sample androidx.compose.foundation.layout.samples.SimpleFlowColumn
*
* When a Modifier [ColumnScope.weight] is provided, it scales the item based on the number items
* that fall on the column it was placed in.
*
* @param modifier The modifier to be applied to the Row.
* @param verticalArrangement The vertical arrangement of the layout's children.
* @param horizontalArrangement The horizontal arrangement of the layout's virtual columns
* @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 max number of rows
* @param content The content as a [ColumnScope]
* @see FlowRow
* @see [androidx.compose.foundation.layout.Column]
*/
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun FlowColumn(
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,
content: @Composable FlowColumnScope.() -> Unit,
) =
FlowColumn(
modifier,
verticalArrangement,
horizontalArrangement,
itemHorizontalAlignment,
maxItemsInEachColumn,
maxLines,
FlowColumnOverflow.Clip,
content,
)
/** Scope for the children of [FlowRow]. */
@LayoutScopeMarker
@Stable
interface FlowRowScope : 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
}
/** Scope for the overflow [FlowRow]. */
@LayoutScopeMarker
@Stable
@ExperimentalLayoutApi
interface FlowRowOverflowScope : FlowRowScope {
/**
* Total Number of Items available to show in [FlowRow] This includes items that may not be
* displayed.
*
* In [ContextualFlowRow], this matches the [ContextualFlowRow]'s `itemCount` parameter
*/
@ExperimentalLayoutApi val totalItemCount: Int
/** Total Number of Items displayed in the [FlowRow] */
@ExperimentalLayoutApi val shownItemCount: Int
}
/** Scope for the children of [FlowColumn]. */
@LayoutScopeMarker
@Stable
interface FlowColumnScope : 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
}
/** Scope for the overflow [FlowColumn]. */
@LayoutScopeMarker
@Stable
@ExperimentalLayoutApi
interface FlowColumnOverflowScope : FlowColumnScope {
/**
* Total Number of Items available to show in [FlowColumn] This includes items that may not be
* displayed.
*
* In [ContextualFlowColumn], this matches the [ContextualFlowColumn]'s `itemCount` parameter
*/
@ExperimentalLayoutApi val totalItemCount: Int
/** Total Number of Items displayed in the [FlowColumn] */
@ExperimentalLayoutApi val shownItemCount: Int
}
@OptIn(ExperimentalLayoutApi::class)
internal object FlowRowScopeInstance : RowScope by RowScopeInstance, FlowRowScope {
override fun Modifier.fillMaxRowHeight(fraction: Float): Modifier {
requirePrecondition(fraction >= 0.0f && fraction <= 1.0f) {
"invalid fraction $fraction; must be >= 0 and <= 1.0"
}
return this.then(FillCrossAxisSizeElement(fraction = fraction))
}
}
@OptIn(ExperimentalLayoutApi::class)
internal class FlowRowOverflowScopeImpl(private val state: FlowLayoutOverflowState) :
FlowRowScope by FlowRowScopeInstance, FlowRowOverflowScope {
override val totalItemCount: Int by lazyInt { state.itemCount }
override val shownItemCount: Int by lazyInt(state.shownItemLazyErrorMessage) { state.itemShown }
}
@OptIn(ExperimentalLayoutApi::class)
internal class FlowColumnOverflowScopeImpl(private val state: FlowLayoutOverflowState) :
FlowColumnScope by FlowColumnScopeInstance, FlowColumnOverflowScope {
override val totalItemCount: Int by lazyInt { state.itemCount }
override val shownItemCount: Int by lazyInt(state.shownItemLazyErrorMessage) { state.itemShown }
}
@OptIn(ExperimentalLayoutApi::class)
internal object FlowColumnScopeInstance : ColumnScope by ColumnScopeInstance, FlowColumnScope {
override fun Modifier.fillMaxColumnWidth(fraction: Float): Modifier {
requirePrecondition(fraction >= 0.0f && fraction <= 1.0f) {
"invalid fraction $fraction; must be >= 0 and <= 1.0"
}
return this.then(FillCrossAxisSizeElement(fraction = fraction))
}
}
internal data class FlowLayoutData(var fillCrossAxisFraction: Float)
internal class FillCrossAxisSizeNode(var fraction: Float) :
ParentDataModifierNode, Modifier.Node() {
override fun Density.modifyParentData(parentData: Any?) =
((parentData as? RowColumnParentData) ?: RowColumnParentData()).also {
it.flowLayoutData = it.flowLayoutData ?: FlowLayoutData(fraction)
it.flowLayoutData!!.fillCrossAxisFraction = fraction
}
}
internal class FillCrossAxisSizeElement(val fraction: Float) :
ModifierNodeElement<FillCrossAxisSizeNode>() {
override fun create(): FillCrossAxisSizeNode {
return FillCrossAxisSizeNode(fraction)
}
override fun update(node: FillCrossAxisSizeNode) {
node.fraction = fraction
}
override fun InspectorInfo.inspectableProperties() {
name = "fraction"
value = fraction
properties["fraction"] = fraction
}
override fun hashCode(): Int {
var result = fraction.hashCode()
result *= 31
return result
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherModifier = other as? FillCrossAxisSizeNode ?: return false
return fraction == otherModifier.fraction
}
}
@OptIn(ExperimentalLayoutApi::class)
@PublishedApi
@Composable
internal fun rowMeasurementHelper(
horizontalArrangement: Arrangement.Horizontal,
verticalArrangement: Arrangement.Vertical,
maxItemsInMainAxis: Int,
): MeasurePolicy {
return remember(horizontalArrangement, verticalArrangement, maxItemsInMainAxis) {
val measurePolicy =
FlowMeasurePolicy(
isHorizontal = true,
horizontalArrangement = horizontalArrangement,
mainAxisSpacing = horizontalArrangement.spacing,
crossAxisAlignment = CROSS_AXIS_ALIGNMENT_TOP,
verticalArrangement = verticalArrangement,
crossAxisArrangementSpacing = verticalArrangement.spacing,
maxItemsInMainAxis = maxItemsInMainAxis,
maxLines = Int.MAX_VALUE,
overflow = FlowRowOverflow.Visible.createOverflowState(),
)
as MultiContentMeasurePolicy
MeasurePolicy { measurables, constraints ->
with(measurePolicy) { this@MeasurePolicy.measure(listOf(measurables), constraints) }
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
internal fun rowMeasurementMultiContentHelper(
horizontalArrangement: Arrangement.Horizontal,
verticalArrangement: Arrangement.Vertical,
itemVerticalAlignment: Alignment.Vertical,
maxItemsInMainAxis: Int,
maxLines: Int,
overflowState: FlowLayoutOverflowState,
): MultiContentMeasurePolicy {
return remember(
horizontalArrangement,
verticalArrangement,
itemVerticalAlignment,
maxItemsInMainAxis,
maxLines,
overflowState,
) {
FlowMeasurePolicy(
isHorizontal = true,
horizontalArrangement = horizontalArrangement,
mainAxisSpacing = horizontalArrangement.spacing,
crossAxisAlignment = CrossAxisAlignment.vertical(itemVerticalAlignment),
verticalArrangement = verticalArrangement,
crossAxisArrangementSpacing = verticalArrangement.spacing,
maxItemsInMainAxis = maxItemsInMainAxis,
maxLines = maxLines,
overflow = overflowState,
)
}
}
@OptIn(ExperimentalLayoutApi::class)
@PublishedApi
@Composable
internal fun columnMeasurementHelper(
verticalArrangement: Arrangement.Vertical,
horizontalArrangement: Arrangement.Horizontal,
maxItemsInMainAxis: Int,
): MeasurePolicy {
return remember(verticalArrangement, horizontalArrangement, maxItemsInMainAxis) {
val measurePolicy =
FlowMeasurePolicy(
isHorizontal = false,
verticalArrangement = verticalArrangement,
mainAxisSpacing = verticalArrangement.spacing,
crossAxisAlignment = CROSS_AXIS_ALIGNMENT_START,
horizontalArrangement = horizontalArrangement,
crossAxisArrangementSpacing = horizontalArrangement.spacing,
maxItemsInMainAxis = maxItemsInMainAxis,
maxLines = Int.MAX_VALUE,
overflow = FlowRowOverflow.Visible.createOverflowState(),
)
MeasurePolicy { measurables, constraints ->
with(measurePolicy) { this@MeasurePolicy.measure(listOf(measurables), constraints) }
}
}
}
@Composable
internal fun columnMeasurementMultiContentHelper(
verticalArrangement: Arrangement.Vertical,
horizontalArrangement: Arrangement.Horizontal,
itemHorizontalAlignment: Alignment.Horizontal,
maxItemsInMainAxis: Int,
maxLines: Int,
overflowState: FlowLayoutOverflowState,
): MultiContentMeasurePolicy {
return remember(
verticalArrangement,
horizontalArrangement,
itemHorizontalAlignment,
maxItemsInMainAxis,
maxLines,
overflowState,
) {
FlowMeasurePolicy(
isHorizontal = false,
verticalArrangement = verticalArrangement,
mainAxisSpacing = verticalArrangement.spacing,
crossAxisAlignment = CrossAxisAlignment.horizontal(itemHorizontalAlignment),
horizontalArrangement = horizontalArrangement,
crossAxisArrangementSpacing = horizontalArrangement.spacing,
maxItemsInMainAxis = maxItemsInMainAxis,
maxLines = maxLines,
overflow = overflowState,
)
}
}
internal interface FlowLineMeasurePolicy : RowColumnMeasurePolicy {
val isHorizontal: Boolean
val horizontalArrangement: Arrangement.Horizontal
val verticalArrangement: Arrangement.Vertical
val crossAxisAlignment: CrossAxisAlignment
override fun Placeable.mainAxisSize() = if (isHorizontal) measuredWidth else measuredHeight
override fun Placeable.crossAxisSize() = if (isHorizontal) measuredHeight else measuredWidth
override fun createConstraints(
mainAxisMin: Int,
crossAxisMin: Int,
mainAxisMax: Int,
crossAxisMax: Int,
isPrioritizing: Boolean,
): Constraints {
return if (isHorizontal) {
createRowConstraints(
isPrioritizing,
mainAxisMin,
crossAxisMin,
mainAxisMax,
crossAxisMax,
)
} else {
createColumnConstraints(
isPrioritizing,
mainAxisMin,
crossAxisMin,
mainAxisMax,
crossAxisMax,
)
}
}
override fun placeHelper(
placeables: Array<Placeable?>,
measureScope: MeasureScope,
beforeCrossAxisAlignmentLine: Int,
mainAxisPositions: IntArray,
mainAxisLayoutSize: Int,
crossAxisLayoutSize: Int,
crossAxisOffset: IntArray?,
currentLineIndex: Int,
startIndex: Int,
endIndex: Int,
): MeasureResult {
with(measureScope) {
val width: Int
val height: Int
if (isHorizontal) {
width = mainAxisLayoutSize
height = crossAxisLayoutSize
} else {
width = crossAxisLayoutSize
height = mainAxisLayoutSize
}
val layoutDirection =
if (isHorizontal) {
LayoutDirection.Ltr
} else {
layoutDirection
}
return layout(width, height) {
val crossAxisLineOffset = crossAxisOffset?.get(currentLineIndex) ?: 0
for (i in startIndex until endIndex) {
val placeable = placeables[i]!!
val crossAxisPosition =
getCrossAxisPosition(
placeable,
crossAxisLayoutSize,
layoutDirection,
beforeCrossAxisAlignmentLine,
) + crossAxisLineOffset
if (isHorizontal) {
placeable.place(mainAxisPositions[i - startIndex], crossAxisPosition)
} else {
placeable.place(crossAxisPosition, mainAxisPositions[i - startIndex])
}
}
}
}
}
fun getCrossAxisPosition(
placeable: Placeable,
crossAxisLayoutSize: Int,
layoutDirection: LayoutDirection,
beforeCrossAxisAlignmentLine: Int,
): Int {
val childCrossAlignment =
placeable.rowColumnParentData?.crossAxisAlignment ?: crossAxisAlignment
return childCrossAlignment.align(
size = crossAxisLayoutSize,
itemCrossAxisSize = placeable.crossAxisSize(),
layoutDirection = layoutDirection,
placeable = placeable,
beforeCrossAxisAlignmentLine = beforeCrossAxisAlignmentLine,
)
}
override fun populateMainAxisPositions(
mainAxisLayoutSize: Int,
childrenMainAxisSize: IntArray,
mainAxisPositions: IntArray,
measureScope: MeasureScope,
) {
if (isHorizontal) {
with(horizontalArrangement) {
measureScope.arrange(
mainAxisLayoutSize,
childrenMainAxisSize,
measureScope.layoutDirection,
mainAxisPositions,
)
}
} else {
with(verticalArrangement) {
measureScope.arrange(mainAxisLayoutSize, childrenMainAxisSize, mainAxisPositions)
}
}
}
}
/** Returns a Flow Measure Policy */
@OptIn(ExperimentalLayoutApi::class)
private data class FlowMeasurePolicy(
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 maxItemsInMainAxis: Int,
private val maxLines: Int,
private val overflow: FlowLayoutOverflowState,
) : MultiContentMeasurePolicy, FlowLineMeasurePolicy {
override fun MeasureScope.measure(
measurables: List<List<Measurable>>,
constraints: Constraints,
): MeasureResult {
if (
maxLines == 0 ||
maxItemsInMainAxis == 0 ||
measurables.isEmpty() ||
constraints.maxHeight == 0 &&
overflow.type != FlowLayoutOverflow.OverflowType.Visible
) {
return layout(0, 0) {}
}
val list = measurables.first()
if (list.isEmpty()) {
return layout(0, 0) {}
}
val seeMoreMeasurable = measurables.getOrNull(1)?.firstOrNull()
val collapseMeasurable = measurables.getOrNull(2)?.firstOrNull()
overflow.itemCount = list.size
overflow.setOverflowMeasurables(
this@FlowMeasurePolicy,
seeMoreMeasurable,
collapseMeasurable,
constraints,
)
return breakDownItems(
this@FlowMeasurePolicy,
list.iterator(),
mainAxisSpacing,
crossAxisArrangementSpacing,
OrientationIndependentConstraints(
constraints,
if (isHorizontal) {
LayoutOrientation.Horizontal
} else {
LayoutOrientation.Vertical
},
),
maxItemsInMainAxis,
maxLines,
overflow,
)
}
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List<List<IntrinsicMeasurable>>,
height: Int,
): Int {
overflow.setOverflowMeasurables(
seeMoreMeasurable = measurables.getOrNull(1)?.firstOrNull(),
collapseMeasurable = measurables.getOrNull(2)?.firstOrNull(),
isHorizontal,
constraints = Constraints(maxHeight = height),
)
return if (isHorizontal) {
minIntrinsicMainAxisSize(
measurables.firstOrNull() ?: listOf(),
height,
mainAxisSpacing.roundToPx(),
crossAxisArrangementSpacing.roundToPx(),
maxLines = maxLines,
maxItemsInMainAxis = maxItemsInMainAxis,
overflow = overflow,
)
} else {
intrinsicCrossAxisSize(
measurables.firstOrNull() ?: listOf(),
height,
mainAxisSpacing.roundToPx(),
crossAxisArrangementSpacing.roundToPx(),
maxLines = maxLines,
maxItemsInMainAxis = maxItemsInMainAxis,
overflow = overflow,
)
}
}
override fun IntrinsicMeasureScope.minIntrinsicHeight(
measurables: List<List<IntrinsicMeasurable>>,
width: Int,
): Int {
overflow.setOverflowMeasurables(
seeMoreMeasurable = measurables.getOrNull(1)?.firstOrNull(),
collapseMeasurable = measurables.getOrNull(2)?.firstOrNull(),
isHorizontal,
constraints = Constraints(maxWidth = width),
)
return if (isHorizontal) {
intrinsicCrossAxisSize(
measurables.firstOrNull() ?: listOf(),
width,
mainAxisSpacing.roundToPx(),
crossAxisArrangementSpacing.roundToPx(),
maxLines = maxLines,
maxItemsInMainAxis = maxItemsInMainAxis,
overflow = overflow,
)
} else {
minIntrinsicMainAxisSize(
measurables.firstOrNull() ?: listOf(),
width,
mainAxisSpacing.roundToPx(),
crossAxisArrangementSpacing.roundToPx(),
maxLines = maxLines,
maxItemsInMainAxis = maxItemsInMainAxis,
overflow = overflow,
)
}
}
override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurables: List<List<IntrinsicMeasurable>>,
width: Int,
): Int {
overflow.setOverflowMeasurables(
seeMoreMeasurable = measurables.getOrNull(1)?.firstOrNull(),
collapseMeasurable = measurables.getOrNull(2)?.firstOrNull(),
isHorizontal,
constraints = Constraints(maxWidth = width),
)
return if (isHorizontal) {
intrinsicCrossAxisSize(
measurables.firstOrNull() ?: listOf(),
width,
mainAxisSpacing.roundToPx(),
crossAxisArrangementSpacing.roundToPx(),
maxLines = maxLines,
maxItemsInMainAxis = maxItemsInMainAxis,
overflow = overflow,
)
} else {
maxIntrinsicMainAxisSize(
measurables.firstOrNull() ?: listOf(),
width,
mainAxisSpacing.roundToPx(),
)
}
}
override fun IntrinsicMeasureScope.maxIntrinsicWidth(
measurables: List<List<IntrinsicMeasurable>>,
height: Int,
): Int {
overflow.setOverflowMeasurables(
seeMoreMeasurable = measurables.getOrNull(1)?.firstOrNull(),
collapseMeasurable = measurables.getOrNull(2)?.firstOrNull(),
isHorizontal,
constraints = Constraints(maxHeight = height),
)
return if (isHorizontal) {
maxIntrinsicMainAxisSize(
measurables.firstOrNull() ?: listOf(),
height,
mainAxisSpacing.roundToPx(),
)
} else {
intrinsicCrossAxisSize(
measurables.firstOrNull() ?: listOf(),
height,
mainAxisSpacing.roundToPx(),
crossAxisArrangementSpacing.roundToPx(),
maxLines = maxLines,
maxItemsInMainAxis = maxItemsInMainAxis,
overflow = overflow,
)
}
}
fun minIntrinsicMainAxisSize(
measurables: List<IntrinsicMeasurable>,
crossAxisAvailable: Int,
mainAxisSpacing: Int,
crossAxisSpacing: Int,
maxItemsInMainAxis: Int,
maxLines: Int,
overflow: FlowLayoutOverflowState,
) =
minIntrinsicMainAxisSize(
measurables,
mainAxisSize = { _, size -> minMainAxisIntrinsicItemSize(size) },
crossAxisSize = { _, size -> minCrossAxisIntrinsicItemSize(size) },
crossAxisAvailable,
mainAxisSpacing,
crossAxisSpacing,
maxItemsInMainAxis,
maxLines,
overflow,
)
fun maxIntrinsicMainAxisSize(
measurables: List<IntrinsicMeasurable>,
height: Int,
arrangementSpacing: Int,
) =
maxIntrinsicMainAxisSize(
measurables,
{ _, size -> maxMainAxisIntrinsicItemSize(size) },
height,
arrangementSpacing,
maxItemsInMainAxis,
)
fun intrinsicCrossAxisSize(
measurables: List<IntrinsicMeasurable>,
mainAxisAvailable: Int,
mainAxisSpacing: Int,
crossAxisSpacing: Int,
maxItemsInMainAxis: Int,
maxLines: Int,
overflow: FlowLayoutOverflowState,
) =
intrinsicCrossAxisSize(
measurables,
mainAxisSize = { _, size -> minMainAxisIntrinsicItemSize(size) },
crossAxisSize = { _, size -> minCrossAxisIntrinsicItemSize(size) },
mainAxisAvailable,
mainAxisSpacing,
crossAxisSpacing,
maxItemsInMainAxis = maxItemsInMainAxis,
overflow = overflow,
maxLines = maxLines,
)
.first
fun IntrinsicMeasurable.maxMainAxisIntrinsicItemSize(size: Int): Int =
if (isHorizontal) maxIntrinsicWidth(size) else maxIntrinsicHeight(size)
fun IntrinsicMeasurable.minCrossAxisIntrinsicItemSize(size: Int): Int =
if (isHorizontal) minIntrinsicHeight(size) else minIntrinsicWidth(size)
fun IntrinsicMeasurable.minMainAxisIntrinsicItemSize(size: Int): Int =
if (isHorizontal) minIntrinsicWidth(size) else minIntrinsicHeight(size)
}
private inline fun maxIntrinsicMainAxisSize(
children: List<IntrinsicMeasurable>,
mainAxisSize: IntrinsicMeasurable.(Int, Int) -> Int,
crossAxisAvailable: Int,
mainAxisSpacing: Int,
maxItemsInMainAxis: Int,
): Int {
var fixedSpace = 0
var currentFixedSpace = 0
var lastBreak = 0
children.fastForEachIndexed { index, child ->
val size = child.mainAxisSize(index, crossAxisAvailable) + mainAxisSpacing
if (index + 1 - lastBreak == maxItemsInMainAxis || index + 1 == children.size) {
lastBreak = index
currentFixedSpace += size
currentFixedSpace -= mainAxisSpacing // no mainAxisSpacing for last item in main axis
fixedSpace = max(fixedSpace, currentFixedSpace)
currentFixedSpace = 0
} else {
currentFixedSpace += size
}
}
return fixedSpace
}
/**
* Slower algorithm but needed to determine the minimum main axis size Uses a binary search to
* search different scenarios to see the minimum main axis size
*/
@ExperimentalLayoutApi
private inline fun minIntrinsicMainAxisSize(
children: List<IntrinsicMeasurable>,
mainAxisSize: IntrinsicMeasurable.(Int, Int) -> Int,
crossAxisSize: IntrinsicMeasurable.(Int, Int) -> Int,
crossAxisAvailable: Int,
mainAxisSpacing: Int,
crossAxisSpacing: Int,
maxItemsInMainAxis: Int,
maxLines: Int,
overflow: FlowLayoutOverflowState,
): Int {
if (children.isEmpty()) {
return 0
}
val mainAxisSizes = IntArray(children.size)
val crossAxisSizes = IntArray(children.size)
for (index in children.indices) {
val child = children[index]
val mainAxisItemSize = child.mainAxisSize(index, crossAxisAvailable)
mainAxisSizes[index] = mainAxisItemSize
crossAxisSizes[index] = child.crossAxisSize(index, mainAxisItemSize)
}
var maxItemsThatCanBeShown =
if (maxLines != Int.MAX_VALUE && maxItemsInMainAxis != Int.MAX_VALUE) {
maxItemsInMainAxis * maxLines
} else {
Int.MAX_VALUE
}
val mustHaveEllipsis =
when {
maxItemsThatCanBeShown < children.size &&
(overflow.type == FlowLayoutOverflow.OverflowType.ExpandIndicator ||
overflow.type == FlowLayoutOverflow.OverflowType.ExpandOrCollapseIndicator) ->
true
maxItemsThatCanBeShown >= children.size &&
maxLines >= overflow.minLinesToShowCollapse &&
overflow.type == FlowLayoutOverflow.OverflowType.ExpandOrCollapseIndicator -> true
else -> false
}
maxItemsThatCanBeShown -= if (mustHaveEllipsis) 1 else 0
maxItemsThatCanBeShown = min(maxItemsThatCanBeShown, children.size)
val maxMainAxisSize = mainAxisSizes.sum().run { this + ((children.size - 1) * mainAxisSpacing) }
var mainAxisUsed = maxMainAxisSize
var crossAxisUsed = crossAxisSizes.maxOf { it }
val minimumItemSize = mainAxisSizes.maxOf { it }
var low = minimumItemSize
var high = maxMainAxisSize
while (low <= high) {
if (crossAxisUsed == crossAxisAvailable) {
return mainAxisUsed
}
val mid = (low + high) / 2
mainAxisUsed = mid
val pair =
intrinsicCrossAxisSize(
children,
mainAxisSizes,
crossAxisSizes,
mainAxisUsed,
mainAxisSpacing,
crossAxisSpacing,
maxItemsInMainAxis,
maxLines,
overflow,
)
crossAxisUsed = pair.first
val itemShown = pair.second
if (crossAxisUsed > crossAxisAvailable || itemShown < maxItemsThatCanBeShown) {
low = mid + 1
if (low > high) {
return low
}
} else if (crossAxisUsed < crossAxisAvailable) {
high = mid - 1
} else {
return mainAxisUsed
}
}
return mainAxisUsed
}
/**
* FlowRow: Intrinsic height (cross Axis) is based on a specified width FlowColumn: Intrinsic width
* (crossAxis) based on a specified height
*/
private fun intrinsicCrossAxisSize(
children: List<IntrinsicMeasurable>,
mainAxisSizes: IntArray,
crossAxisSizes: IntArray,
mainAxisAvailable: Int,
mainAxisSpacing: Int,
crossAxisSpacing: Int,
maxItemsInMainAxis: Int,
maxLines: Int,
overflow: FlowLayoutOverflowState,
): IntIntPair {
return intrinsicCrossAxisSize(
children,
{ index, _ -> mainAxisSizes[index] },
{ index, _ -> crossAxisSizes[index] },
mainAxisAvailable,
mainAxisSpacing,
crossAxisSpacing,
maxItemsInMainAxis,
maxLines,
overflow,
)
}
/**
* FlowRow: Intrinsic height (cross Axis) is based on a specified width
* * FlowColumn: Intrinsic width (crossAxis) based on a specified height
*/
private inline fun intrinsicCrossAxisSize(
children: List<IntrinsicMeasurable>,
mainAxisSize: IntrinsicMeasurable.(Int, Int) -> Int,
crossAxisSize: IntrinsicMeasurable.(Int, Int) -> Int,
mainAxisAvailable: Int,
mainAxisSpacing: Int,
crossAxisSpacing: Int,
maxItemsInMainAxis: Int,
maxLines: Int,
overflow: FlowLayoutOverflowState,
): IntIntPair {
if (children.isEmpty()) {
return IntIntPair(0, 0)
}
val buildingBlocks =
FlowLayoutBuildingBlocks(
maxItemsInMainAxis = maxItemsInMainAxis,
overflow = overflow,
maxLines = maxLines,
constraints =
OrientationIndependentConstraints(
mainAxisMin = 0,
mainAxisMax = mainAxisAvailable,
crossAxisMin = 0,
crossAxisMax = Constraints.Infinity,
),
mainAxisSpacing = mainAxisSpacing,
crossAxisSpacing = crossAxisSpacing,
)
var nextChild = children.getOrNull(0)
var nextCrossAxisSize = nextChild?.crossAxisSize(0, mainAxisAvailable) ?: 0
var nextMainAxisSize = nextChild?.mainAxisSize(0, nextCrossAxisSize) ?: 0
var remaining = mainAxisAvailable
var currentCrossAxisSize = 0
var totalCrossAxisSize = 0
var lastBreak = 0
var lineIndex = 0
var wrapInfo =
buildingBlocks.getWrapInfo(
nextItemHasNext = children.size > 1,
nextIndexInLine = 0,
leftOver = IntIntPair(remaining, Constraints.Infinity),
nextSize =
if (nextChild == null) null else IntIntPair(nextMainAxisSize, nextCrossAxisSize),
lineIndex = lineIndex,
totalCrossAxisSize = totalCrossAxisSize,
currentLineCrossAxisSize = currentCrossAxisSize,
isWrappingRound = false,
isEllipsisWrap = false,
)
if (wrapInfo.isLastItemInContainer) {
val size =
overflow
.ellipsisSize(hasNext = nextChild != null, lineIndex = 0, totalCrossAxisSize = 0)
?.second ?: 0
val noOfItemsShown = 0
return IntIntPair(size, noOfItemsShown)
}
var noOfItemsShown = 0
for (index in children.indices) {
val childCrossAxisSize = nextCrossAxisSize
val childMainAxisSize = nextMainAxisSize
remaining -= childMainAxisSize
noOfItemsShown = index + 1
currentCrossAxisSize = maxOf(currentCrossAxisSize, childCrossAxisSize)
// look ahead to simplify logic
nextChild = children.getOrNull(index + 1)
nextCrossAxisSize = nextChild?.crossAxisSize(index + 1, mainAxisAvailable) ?: 0
nextMainAxisSize =
nextChild?.mainAxisSize(index + 1, nextCrossAxisSize)?.plus(mainAxisSpacing) ?: 0
wrapInfo =
buildingBlocks.getWrapInfo(
nextItemHasNext = index + 2 < children.size,
nextIndexInLine = (index + 1) - lastBreak,
leftOver = IntIntPair(remaining, Constraints.Infinity),
nextSize =
if (nextChild == null) {
null
} else {
IntIntPair(nextMainAxisSize, nextCrossAxisSize)
},
lineIndex = lineIndex,
totalCrossAxisSize = totalCrossAxisSize,
currentLineCrossAxisSize = currentCrossAxisSize,
isWrappingRound = false,
isEllipsisWrap = false,
)
if (wrapInfo.isLastItemInLine) {
totalCrossAxisSize += currentCrossAxisSize + crossAxisSpacing
val ellipsisWrapInfo =
buildingBlocks.getWrapEllipsisInfo(
wrapInfo,
hasNext = nextChild != null,
leftOverMainAxis = remaining,
lastContentLineIndex = lineIndex,
totalCrossAxisSize = totalCrossAxisSize,
nextIndexInLine = (index + 1) - lastBreak,
)
currentCrossAxisSize = 0
remaining = mainAxisAvailable
lastBreak = index + 1
nextMainAxisSize -= mainAxisSpacing
lineIndex++
if (wrapInfo.isLastItemInContainer) {
ellipsisWrapInfo?.ellipsisSize?.let {
if (!ellipsisWrapInfo.placeEllipsisOnLastContentLine) {
totalCrossAxisSize += it.second + crossAxisSpacing
}
}
break
}
}
}
// remove the last spacing for the last row or column
totalCrossAxisSize -= crossAxisSpacing
return IntIntPair(totalCrossAxisSize, noOfItemsShown)
}
/**
* Breaks down items based on space, size and maximum items in main axis. When items run out of
* space or the maximum items to fit in the main axis is reached, it moves to the next "line" and
* moves the next batch of items to a new list of items
*/
internal fun MeasureScope.breakDownItems(
measurePolicy: FlowLineMeasurePolicy,
measurablesIterator: Iterator<Measurable>,
mainAxisSpacingDp: Dp,
crossAxisSpacingDp: Dp,
constraints: OrientationIndependentConstraints,
maxItemsInMainAxis: Int,
maxLines: Int,
overflow: FlowLayoutOverflowState,
): MeasureResult {
val items = mutableVectorOf<MeasureResult>()
val mainAxisMax = constraints.mainAxisMax
val mainAxisMin = constraints.mainAxisMin
val crossAxisMax = constraints.crossAxisMax
val placeables = mutableIntObjectMapOf<Placeable?>()
val measurables = mutableListOf<Measurable>()
val spacing = ceil(mainAxisSpacingDp.toPx()).toInt()
val crossAxisSpacing = ceil(crossAxisSpacingDp.toPx()).toInt()
val subsetConstraints = OrientationIndependentConstraints(0, mainAxisMax, 0, crossAxisMax)
val measureConstraints =
subsetConstraints
.copy(mainAxisMin = 0)
.toBoxConstraints(
if (measurePolicy.isHorizontal) LayoutOrientation.Horizontal
else LayoutOrientation.Vertical
)
var index = 0
var measurable: Measurable?
var placeableItem: Placeable? = null
var lineIndex = 0
var leftOver = mainAxisMax
var leftOverCrossAxis = crossAxisMax
val lineInfo =
if (measurablesIterator is ContextualFlowItemIterator) {
FlowLineInfo(
lineIndex = lineIndex,
positionInLine = 0,
maxMainAxisSize = leftOver.toDp(),
maxCrossAxisSize = leftOverCrossAxis.toDp(),
)
} else {
null
}
var nextSize =
measurablesIterator.hasNext().run {
measurable = if (!this) null else measurablesIterator.safeNext(lineInfo)
measurable?.measureAndCache(measurePolicy, measureConstraints) { placeable ->
placeableItem = placeable
}
}
var nextMainAxisSize: Int? = nextSize?.first
var nextCrossAxisSize: Int? = nextSize?.second
var startBreakLineIndex = 0
val endBreakLineList = mutableIntListOf()
val crossAxisSizes = mutableIntListOf()
val crossAxisFillEnabledLines = mutableIntSetOf()
val buildingBlocks =
FlowLayoutBuildingBlocks(
maxItemsInMainAxis = maxItemsInMainAxis,
mainAxisSpacing = spacing,
crossAxisSpacing = crossAxisSpacing,
constraints = constraints,
maxLines = maxLines,
overflow = overflow,
)
var ellipsisWrapInfo: FlowLayoutBuildingBlocks.WrapEllipsisInfo? = null
var wrapInfo =
buildingBlocks
.getWrapInfo(
nextItemHasNext = measurablesIterator.hasNext(),
leftOver = IntIntPair(leftOver, leftOverCrossAxis),
totalCrossAxisSize = 0,
nextSize = nextSize,
currentLineCrossAxisSize = 0,
nextIndexInLine = 0,
isWrappingRound = false,
isEllipsisWrap = false,
lineIndex = 0,
)
.also { wrapInfo ->
if (wrapInfo.isLastItemInContainer) {
ellipsisWrapInfo =
buildingBlocks.getWrapEllipsisInfo(
wrapInfo,
nextSize != null,
lastContentLineIndex = -1,
totalCrossAxisSize = 0,
leftOver,
nextIndexInLine = 0,
)
}
}
// figure out the mainAxisTotalSize which will be minMainAxis when measuring the row/column
var mainAxisTotalSize = mainAxisMin
var crossAxisTotalSize = 0
var currentLineMainAxisSize = 0
var currentLineCrossAxisSize = 0
var currentLineCrossAxisFillEnabled = false
while (!wrapInfo.isLastItemInContainer && measurable != null) {
val itemMainAxisSize = nextMainAxisSize!!
val itemCrossAxisSize = nextCrossAxisSize!!
currentLineMainAxisSize += itemMainAxisSize
currentLineCrossAxisSize = maxOf(currentLineCrossAxisSize, itemCrossAxisSize)
leftOver -= itemMainAxisSize
overflow.itemShown = index + 1
measurables.add(measurable!!)
placeables[index] = placeableItem
if (
(measurable.parentData as? RowColumnParentData)
?.flowLayoutData
?.fillCrossAxisFraction != null
) {
currentLineCrossAxisFillEnabled = true
}
val nextIndexInLine = (index + 1) - startBreakLineIndex
val willFitLine = nextIndexInLine < maxItemsInMainAxis
lineInfo?.update(
lineIndex = if (willFitLine) lineIndex else lineIndex + 1,
positionInLine = if (willFitLine) nextIndexInLine else 0,
maxMainAxisSize =
if (willFitLine) {
(leftOver - spacing).fastCoerceAtLeast(0)
} else {
mainAxisMax
}
.toDp(),
maxCrossAxisSize =
if (willFitLine) {
leftOverCrossAxis
} else {
(leftOverCrossAxis - currentLineCrossAxisSize - crossAxisSpacing)
.fastCoerceAtLeast(0)
}
.toDp(),
)
nextSize =
measurablesIterator.hasNext().run {
measurable = if (!this) null else measurablesIterator.safeNext(lineInfo)
placeableItem = null
measurable?.measureAndCache(measurePolicy, measureConstraints) { placeable ->
placeableItem = placeable
}
}
nextMainAxisSize = nextSize?.first?.plus(spacing)
nextCrossAxisSize = nextSize?.second
wrapInfo =
buildingBlocks.getWrapInfo(
nextItemHasNext = measurablesIterator.hasNext(),
leftOver = IntIntPair(leftOver, leftOverCrossAxis),
totalCrossAxisSize = crossAxisTotalSize,
nextSize =
if (nextSize == null) null
else IntIntPair(nextMainAxisSize!!, nextCrossAxisSize!!),
currentLineCrossAxisSize = currentLineCrossAxisSize,
nextIndexInLine = nextIndexInLine,
isWrappingRound = false,
isEllipsisWrap = false,
lineIndex = lineIndex,
)
if (wrapInfo.isLastItemInLine) {
mainAxisTotalSize = maxOf(mainAxisTotalSize, currentLineMainAxisSize)
mainAxisTotalSize = minOf(mainAxisTotalSize, mainAxisMax)
crossAxisTotalSize += currentLineCrossAxisSize
ellipsisWrapInfo =
buildingBlocks.getWrapEllipsisInfo(
wrapInfo,
nextSize != null,
lastContentLineIndex = lineIndex,
totalCrossAxisSize = crossAxisTotalSize,
leftOver,
(index + 1) - startBreakLineIndex,
)
crossAxisSizes.add(currentLineCrossAxisSize)
if (currentLineCrossAxisFillEnabled) crossAxisFillEnabledLines += lineIndex
currentLineCrossAxisFillEnabled = false
leftOver = mainAxisMax
leftOverCrossAxis = crossAxisMax - crossAxisTotalSize - crossAxisSpacing
startBreakLineIndex = index + 1
endBreakLineList.add(index + 1)
currentLineMainAxisSize = 0
currentLineCrossAxisSize = 0
// only add spacing for next items in the row or column, not the starting indexes
nextMainAxisSize = nextMainAxisSize?.minus(spacing)
lineIndex++
crossAxisTotalSize += crossAxisSpacing
}
index++
}
ellipsisWrapInfo?.let {
measurables.add(it.ellipsis)
placeables[measurables.size - 1] = it.placeable
lineIndex = endBreakLineList.lastIndex
if (it.placeEllipsisOnLastContentLine) {
val lastIndex = endBreakLineList.size - 1
val lastLineCrossAxis = crossAxisSizes[lineIndex]
crossAxisSizes[lineIndex] = max(lastLineCrossAxis, it.ellipsisSize.second)
endBreakLineList[lastIndex] = endBreakLineList.last() + 1
} else {
crossAxisSizes.add(it.ellipsisSize.second)
endBreakLineList.add(endBreakLineList.last() + 1)
}
}
val arrayOfPlaceables: Array<Placeable?> = Array(measurables.size) { placeables[it] }
val crossAxisOffsets = IntArray(endBreakLineList.size)
val crossAxisSizesArray = IntArray(endBreakLineList.size)
crossAxisTotalSize = 0
val subMeasureScope =
object : MeasureScope {
override val density: Float
get() = this@breakDownItems.density
override val fontScale: Float
get() = this@breakDownItems.fontScale
override val layoutDirection: LayoutDirection
get() = this@breakDownItems.layoutDirection
}
var startIndex = 0
endBreakLineList.forEachIndexed { currentLineIndex, endIndex ->
var crossAxisSize = crossAxisSizes[currentLineIndex]
val crossAxisMaxSize =
when {
crossAxisFillEnabledLines.contains(currentLineIndex) -> crossAxisSize
subsetConstraints.crossAxisMax == Constraints.Infinity -> Constraints.Infinity
else -> subsetConstraints.crossAxisMax - crossAxisTotalSize
}
val result =
measurePolicy.measure(
mainAxisMin = mainAxisTotalSize,
crossAxisMin = subsetConstraints.crossAxisMin,
mainAxisMax = subsetConstraints.mainAxisMax,
crossAxisMax = crossAxisMaxSize,
spacing,
subMeasureScope,
measurables,
arrayOfPlaceables,
startIndex,
endIndex,
crossAxisOffsets,
currentLineIndex,
)
val mainAxisSize: Int
if (measurePolicy.isHorizontal) {
mainAxisSize = result.width
crossAxisSize = result.height
} else {
mainAxisSize = result.height
crossAxisSize = result.width
}
crossAxisSizesArray[currentLineIndex] = crossAxisSize
crossAxisTotalSize += crossAxisSize
mainAxisTotalSize = maxOf(mainAxisTotalSize, mainAxisSize)
items.add(result)
startIndex = endIndex
}
if (items.isEmpty()) {
mainAxisTotalSize = 0
crossAxisTotalSize = 0
}
return placeHelper(
constraints,
mainAxisTotalSize,
crossAxisTotalSize,
crossAxisSizesArray,
items,
measurePolicy,
crossAxisOffsets,
)
}
private fun Iterator<Measurable>.safeNext(info: FlowLineInfo?): Measurable? {
return try {
if (this is ContextualFlowItemIterator) {
this.getNext(info!!)
} else {
next()
}
} catch (e: IndexOutOfBoundsException) {
null
}
}
internal fun IntrinsicMeasurable.mainAxisMin(isHorizontal: Boolean, crossAxisSize: Int) =
if (isHorizontal) {
minIntrinsicWidth(crossAxisSize)
} else {
minIntrinsicHeight(crossAxisSize)
}
internal fun IntrinsicMeasurable.crossAxisMin(isHorizontal: Boolean, mainAxisSize: Int) =
if (isHorizontal) {
minIntrinsicHeight(mainAxisSize)
} else {
minIntrinsicWidth(mainAxisSize)
}
internal val CROSS_AXIS_ALIGNMENT_TOP = CrossAxisAlignment.vertical(Alignment.Top)
internal val CROSS_AXIS_ALIGNMENT_START = CrossAxisAlignment.horizontal(Alignment.Start)
// We measure and cache to improve performance dramatically, instead of using intrinsics
// This only works so far for fixed size items.
// For weighted items, we continue to use their intrinsic widths.
// This is because their fixed sizes are only determined after we determine
// the number of items that can fit in the row/column it only lies on.
internal fun Measurable.measureAndCache(
measurePolicy: FlowLineMeasurePolicy,
constraints: Constraints,
storePlaceable: (Placeable?) -> Unit,
): IntIntPair {
return if (
rowColumnParentData.weight == 0f &&
rowColumnParentData?.flowLayoutData?.fillCrossAxisFraction == null
) {
// fixed sizes: measure once
val placeable = measure(constraints).also(storePlaceable)
with(measurePolicy) {
val mainAxis = placeable.mainAxisSize()
val crossAxis = placeable.crossAxisSize()
IntIntPair(mainAxis, crossAxis)
}
} else {
val mainAxis = mainAxisMin(measurePolicy.isHorizontal, Constraints.Infinity)
val crossAxis = crossAxisMin(measurePolicy.isHorizontal, mainAxis)
IntIntPair(mainAxis, crossAxis)
}
}
internal fun MeasureScope.placeHelper(
constraints: OrientationIndependentConstraints,
mainAxisTotalSize: Int,
crossAxisTotalSize: Int,
crossAxisSizes: IntArray,
items: MutableVector<MeasureResult>,
measureHelper: FlowLineMeasurePolicy,
outPosition: IntArray,
): MeasureResult {
val isHorizontal = measureHelper.isHorizontal
val verticalArrangement = measureHelper.verticalArrangement
val horizontalArrangement = measureHelper.horizontalArrangement
// space in between children, except for the last child
var totalCrossAxisSize = crossAxisTotalSize
// cross axis arrangement
if (isHorizontal) {
with(verticalArrangement) {
val totalCrossAxisSpacing = spacing.roundToPx() * (items.size - 1)
totalCrossAxisSize += totalCrossAxisSpacing
totalCrossAxisSize =
totalCrossAxisSize.fastCoerceIn(constraints.crossAxisMin, constraints.crossAxisMax)
arrange(totalCrossAxisSize, crossAxisSizes, outPosition)
}
} else {
with(horizontalArrangement) {
val totalCrossAxisSpacing = spacing.roundToPx() * (items.size - 1)
totalCrossAxisSize += totalCrossAxisSpacing
totalCrossAxisSize =
totalCrossAxisSize.fastCoerceIn(constraints.crossAxisMin, constraints.crossAxisMax)
arrange(totalCrossAxisSize, crossAxisSizes, layoutDirection, outPosition)
}
}
val finalMainAxisTotalSize =
mainAxisTotalSize.fastCoerceIn(constraints.mainAxisMin, constraints.mainAxisMax)
val layoutWidth: Int
val layoutHeight: Int
if (isHorizontal) {
layoutWidth = finalMainAxisTotalSize
layoutHeight = totalCrossAxisSize
} else {
layoutWidth = totalCrossAxisSize
layoutHeight = finalMainAxisTotalSize
}
return layout(layoutWidth, layoutHeight) {
items.forEach { measureResult -> measureResult.placeChildren() }
}
}