blob: 111e248242ca4f7bbacf05b1c08ade247692da30 [file]
/*
* Copyright 2025 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.layout
import androidx.annotation.FloatRange
import androidx.compose.foundation.layout.internal.JvmDefaultWithCompatibility
import androidx.compose.foundation.layout.internal.requirePrecondition
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.FirstBaseline
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.IntrinsicMeasureScope
import androidx.compose.ui.layout.LastBaseline
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.Measured
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.VerticalAlignmentLine
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.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastCoerceAtLeast
import androidx.compose.ui.util.fastCoerceAtMost
import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import androidx.compose.ui.util.fastMaxBy
import androidx.compose.ui.util.fastRoundToInt
import androidx.compose.ui.util.fastSumBy
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.jvm.JvmInline
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.roundToInt
/**
* A layout that aligns its children in a single direction (the main axis) and allows them to wrap
* onto multiple lines. [FlexBox] provides a highly configurable layout system, serving as a
* flexible superset of [Row], [Column], [FlowRow], and [FlowColumn].
*
* The layout behavior of the container is controlled by the [config] parameter, which dictates the
* flex direction, wrapping behavior, alignment, and spacing. Individual children can further
* control their own flexibility (grow, shrink, and base size) and alignment using the
* [FlexBoxScope.flex] modifier.
*
* Understanding FlexBox requires familiarity with its axes:
* - **Main Axis**: The primary direction along which items are laid out, determined by the
* [FlexBoxConfigScope.direction]. Items are placed starting from the **`main-start`** edge and
* flowing toward the **`main-end`** edge. Defaults to [FlexDirection.Row].
* - For [FlexDirection.Row]: `main-start` is the layout's start edge (left in LTR, right in
* RTL) and `main-end` is the end edge (right in LTR, left in RTL).
* - For [FlexDirection.RowReverse]: `main-start` is the layout's end edge (right in LTR,
* left in
* * RTL) and `main-end` is the start edge (left in LTR, right in RTL).
* - For [FlexDirection.Column]: `main-start` is the top edge and `main-end` is the bottom edge.
* - For [FlexDirection.ColumnReverse]: `main-start` is the bottom edge and `main-end` is
* the top edge.
* - **Cross Axis**: The axis perpendicular to the main axis. Wrapped lines are added, and items are
* aligned within their lines, starting from the **`cross-start`** edge and flowing toward the
* **`cross-end`** edge.
* - For horizontal directions ([FlexDirection.Row] and [FlexDirection.RowReverse]):
* `cross-start` is the top edge and `cross-end` is the bottom edge.
* - For vertical directions ([FlexDirection.Column] and [FlexDirection.ColumnReverse]):
* `cross-start` is the layout's start edge and `cross-end` is the end edge.
*
* Children can dictate how they share available space using the [FlexBoxScope.flex] modifier:
* - [FlexConfigScope.grow]: Defines how much of the remaining positive free space the item should
* consume relative to its siblings. Defaults to 0f (no growth).
* - [FlexConfigScope.shrink]: Defines how much the item should shrink when the combined sizes of
* the items exceed the container's main axis size. Defaults to 1f.
* - [FlexConfigScope.basis]: Sets the initial main axis size of the item before any free space
* distribution (grow or shrink) is calculated. Defaults to [FlexBasis.Auto].
*
* [FlexBox] provides granular control over the placement of items and lines:
* - [FlexBoxConfigScope.wrap]: Controls whether items are forced onto a single line or allowed to
* wrap onto multiple lines when they exceed the available space. Defaults to [FlexWrap.NoWrap].
* - [FlexBoxConfigScope.justifyContent]: Distributes items along the main axis (for example,
* spacing them evenly). Defaults to [FlexJustifyContent.Start].
* - [FlexBoxConfigScope.alignItems]: Aligns items within a specific line along the cross axis (for
* example, centering them vertically within a Row). Defaults to [FlexAlignItems.Start].
* - [FlexConfigScope.alignSelf]: Allows an individual item to override the container's
* [FlexBoxConfigScope.alignItems]. Defaults to [FlexAlignSelf.Auto].
* - [FlexBoxConfigScope.alignContent]:Distributes multiple wrapped lines along the cross axis. This
* only applies when wrapping is enabled. Defaults to [FlexAlignContent.Start].
*
* By default, children are placed in a horizontal row without wrapping. If wrapping is disabled
* ([FlexWrap.NoWrap]), children will shrink to fit the container if they have a shrink factor > 0.
* If children cannot shrink enough due to their minimum intrinsic sizes, they will visually
* overflow the container's bounds along the main axis. You can explicitly apply
* [Modifier.clipToBounds][androidx.compose.ui.draw.clipToBounds] on the FlexBox if you wish to hide
* overflowing content.
*
* @sample androidx.compose.foundation.layout.samples.SimpleFlexBox
* @param modifier The modifier to be applied to the FlexBox container.
* @param config A [FlexBoxConfig] that configures the container's layout properties. Defaults to a
* horizontal row layout without wrapping, with items aligned to the start on both axes and no
* gaps between items.
* @param content The content of the FlexBox, defined within a [FlexBoxScope].
* @see FlexBoxConfig
* @see FlexBoxScope
*/
@Composable
@ExperimentalFlexBoxApi
inline fun FlexBox(
modifier: Modifier = Modifier,
config: FlexBoxConfig = FlexBoxConfig,
content: @Composable FlexBoxScope.() -> Unit,
) {
val currentConfig = rememberUpdatedState(config)
Layout(
modifier = modifier,
content = { FlexBoxScopeInstance.content() },
measurePolicy = flexMultiContentMeasurePolicy(flexBoxConfigState = currentConfig),
)
}
/**
* Creates a [MeasurePolicy] for FlexBox layout.
*
* This function is exposed to allow caching of measure policies for common configurations.
*/
@PublishedApi
@Composable
@ExperimentalFlexBoxApi
internal fun flexMultiContentMeasurePolicy(
flexBoxConfigState: State<FlexBoxConfig>
): MeasurePolicy {
return remember(flexBoxConfigState) {
FlexBoxMeasurePolicy(flexBoxConfigState = flexBoxConfigState)
}
}
@OptIn(ExperimentalFlexBoxApi::class)
private class FlexBoxMeasurePolicy(private val flexBoxConfigState: State<FlexBoxConfig>) :
MeasurePolicy {
private val resolvedFlexBoxConfig = ResolvedFlexBoxConfig()
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints,
): MeasureResult {
if (measurables.isEmpty()) {
return layout(constraints.minWidth, constraints.minHeight) {}
}
val flexBoxConfig = resolveFlexBoxConfig(flexBoxConfigState.value, this, constraints)
return measureFlexBox(
flexBoxConfig,
measurables,
OrientationIndependentConstraints(
constraints,
if (flexBoxConfig.isHorizontal) {
LayoutOrientation.Horizontal
} else {
LayoutOrientation.Vertical
},
),
flexBoxConfig.isHorizontal,
)
}
// Main FlexBox layout algorithm.
private fun MeasureScope.measureFlexBox(
flexBoxConfig: ResolvedFlexBoxConfig,
measurables: List<Measurable>,
constraints: OrientationIndependentConstraints,
isHorizontal: Boolean,
): MeasureResult {
// Create all item states in single pass
val items = ArrayList<ResolvedFlexItemInfo>(measurables.size)
// Determine if we need upfront cross-axis calculation
var needsUpfrontCrossAxisCalculation =
flexBoxConfig.needUpfrontCrossAxisCalculation(constraints)
var needsSorting = false
measurables.fastForEach { measurable ->
val itemState = createFlexItem(measurable = measurable, isHorizontal, constraints)
if (itemState.order != 0) needsSorting = true
if (
itemState.alignSelf == FlexAlignSelf.Baseline ||
itemState.alignSelf == FlexAlignSelf.Stretch
) {
needsUpfrontCrossAxisCalculation = true
}
items.add(itemState)
}
if (needsSorting) {
items.sortBy { it.order }
}
val mainAxisGap = flexBoxConfig.mainAxisGap()
val crossAxisGap = flexBoxConfig.crossAxisGap()
var totalLinesCrossSize = 0
// Build lines
val lines =
buildFlexLines(
flexBoxConfig = flexBoxConfig,
items = items,
constraints = constraints,
mainAxisGap = mainAxisGap,
crossAxisGap = crossAxisGap,
needsUpfrontCrossAxisCalculation = needsUpfrontCrossAxisCalculation,
) {
totalLinesCrossSize = it
}
// If we have single line and constraints are defined then the size of line is the
// constraints instead of tallest item in the line.
if (lines.size == 1) {
val constrainedCrossSize = max(lines[0].crossAxisSize, constraints.crossAxisMin)
lines[0].crossAxisSize = constrainedCrossSize
}
// handle `align-content: stretch`
totalLinesCrossSize =
applyAlignContentStretch(
flexBoxConfig = flexBoxConfig,
lines = lines,
constraints = constraints,
totalLinesCrossSize = totalLinesCrossSize,
crossAxisGap = crossAxisGap,
)
// measure Items
totalLinesCrossSize =
measureFlexItems(
lines = lines,
items = items,
flexBoxConfig = flexBoxConfig,
totalLinesCrossSize = totalLinesCrossSize,
needsUpfrontCrossAxisCalculation = needsUpfrontCrossAxisCalculation,
constraints = constraints,
)
// calculate the cross position for each line.
calculateLineCrossPositions(
flexBoxConfig = flexBoxConfig,
totalCrossAxisSpace = max(totalLinesCrossSize, constraints.crossAxisMin),
lines = lines,
totalLinesCrossSize = totalLinesCrossSize,
crossAxisGap = crossAxisGap,
)
totalLinesCrossSize = lines.totalCrossAxisSize(isReverse = flexBoxConfig.isCrossAxisReverse)
totalLinesCrossSize = max(totalLinesCrossSize, constraints.crossAxisMin)
val maxLineMainSize =
lines.fastMaxBy { it.mainAxisSize }?.mainAxisSize ?: constraints.mainAxisMin
val mainAxisSize =
max(maxLineMainSize, constraints.mainAxisMin)
.fastCoerceIn(constraints.mainAxisMin, constraints.mainAxisMax)
val layoutWidth: Int
val layoutHeight: Int
if (isHorizontal) {
layoutWidth = mainAxisSize
layoutHeight =
totalLinesCrossSize.fastCoerceIn(constraints.crossAxisMin, constraints.crossAxisMax)
} else {
layoutWidth =
totalLinesCrossSize.fastCoerceIn(constraints.crossAxisMin, constraints.crossAxisMax)
layoutHeight = mainAxisSize
}
return layout(layoutWidth, layoutHeight) {
placeFlexItems(
lines = lines,
items = items,
layoutDirection = layoutDirection,
flexBoxConfig = flexBoxConfig,
layoutWidth = layoutWidth,
layoutHeight = layoutHeight,
mainAxisGap = mainAxisGap,
isHorizontal = isHorizontal,
)
}
}
// Places all flex items in the layout.
private fun Placeable.PlacementScope.placeFlexItems(
lines: ArrayList<FlexLine>,
items: ArrayList<ResolvedFlexItemInfo>,
layoutDirection: LayoutDirection,
flexBoxConfig: ResolvedFlexBoxConfig,
layoutWidth: Int,
layoutHeight: Int,
mainAxisGap: Int,
isHorizontal: Boolean,
) {
lines.fastForEach { line ->
positionItemsOnMainAxis(
items = items,
flexBoxConfig = flexBoxConfig,
containerMainAxisSize = if (isHorizontal) layoutWidth else layoutHeight,
line = line,
mainAxisGap = mainAxisGap,
isMainAxisReverse = isMainAxisReversedForLayout(flexBoxConfig = flexBoxConfig),
)
items.fastForEachUntil(line.startIndex, line.endIndex) { item ->
val x =
if (isHorizontal) {
item.mainPosition
} else {
item.crossPosition
}
val y =
if (isHorizontal) {
item.crossPosition
} else {
item.mainPosition
}
item.placeable?.placeRelative(x = x, y = y)
}
}
}
private fun MeasureScope.createFlexItem(
measurable: Measurable,
isHorizontal: Boolean,
constraints: OrientationIndependentConstraints,
): ResolvedFlexItemInfo {
val node = measurable.parentData as? FlexBoxChildDataNode
val resolvedItemInfo = ResolvedFlexItemInfo()
if (node != null) {
resolvedItemInfo.prepare(this, constraints)
with(node.config) { resolvedItemInfo.configure() }
}
resolvedItemInfo.measurable = measurable
// Calculate flex base size
val minMainAxisSize = resolvedItemInfo.getMinMainAxisSize(isHorizontal)
val flexBaseSize =
when {
resolvedItemInfo.basis.isDp -> resolvedItemInfo.basis.value.dp.roundToPx()
resolvedItemInfo.basis.isPercent -> {
if (
constraints.mainAxisMax == Constraints.Infinity ||
resolvedItemInfo.basis.value.isNaN()
) {
resolvedItemInfo.getMaxContentSize(isHorizontal)
} else {
(constraints.mainAxisMax * resolvedItemInfo.basis.value).toInt()
}
}
resolvedItemInfo.basis.isAuto -> resolvedItemInfo.getMaxContentSize(isHorizontal)
else -> resolvedItemInfo.getMaxContentSize(isHorizontal)
}
resolvedItemInfo.flexBaseSize = flexBaseSize
resolvedItemInfo.hypotheticalMainSize = flexBaseSize.fastCoerceAtLeast(minMainAxisSize)
resolvedItemInfo.targetMainSize = resolvedItemInfo.hypotheticalMainSize
return resolvedItemInfo
}
private fun ResolvedFlexBoxConfig.needUpfrontCrossAxisCalculation(
constraints: OrientationIndependentConstraints
) =
(alignItems == FlexAlignItems.Stretch) ||
(alignItems == FlexAlignItems.Baseline) ||
(isWrapEnabled &&
alignContent == FlexAlignContent.Stretch &&
constraints.crossAxisMax != Constraints.Infinity)
// Builds flex lines by distributing items according to wrap settings
private inline fun buildFlexLines(
flexBoxConfig: ResolvedFlexBoxConfig,
items: ArrayList<ResolvedFlexItemInfo>,
constraints: OrientationIndependentConstraints,
mainAxisGap: Int,
crossAxisGap: Int,
needsUpfrontCrossAxisCalculation: Boolean,
updateTotalCrossSize: (Int) -> Unit,
): ArrayList<FlexLine> {
val lines = ArrayList<FlexLine>(8)
var currentLine = FlexLine()
var currentLineHypotheticalMainAxisSize = 0
var currentCrossPosition = 0
var totalLinesCrossSize = 0
var lineStartIndex = 0
var remainingCrossAxisSize = constraints.crossAxisMax
items.fastForEachIndexed { index, item ->
if (
flexBoxConfig.isWrapEnabled &&
index > lineStartIndex &&
currentLineHypotheticalMainAxisSize + item.hypotheticalMainSize >
constraints.mainAxisMax
) {
currentLine.startIndex = lineStartIndex
currentLine.endIndex = index
// it doesn't fit. process the completed line.
processFlexLine(
line = currentLine,
items = items,
flexBoxConfig = flexBoxConfig,
// Subtract the trailing mainAxisGap here
currentLineHypotheticalMainAxisSize =
currentLineHypotheticalMainAxisSize - mainAxisGap,
needsUpfrontCrossAxisCalculation = needsUpfrontCrossAxisCalculation,
constraints = constraints,
remainingCrossAxisSize = remainingCrossAxisSize,
)
totalLinesCrossSize += currentLine.crossAxisSize
currentLine.crossStart = currentCrossPosition
currentCrossPosition += currentLine.crossAxisSize + crossAxisGap
remainingCrossAxisSize =
(remainingCrossAxisSize - (currentLine.crossAxisSize + crossAxisGap))
.fastCoerceAtLeast(0)
lines.add(currentLine)
// start a new line with the current item
currentLine = FlexLine()
lineStartIndex = index
currentLineHypotheticalMainAxisSize = item.hypotheticalMainSize + mainAxisGap
} else {
currentLineHypotheticalMainAxisSize += item.hypotheticalMainSize + mainAxisGap
}
}
if (lineStartIndex < items.size) {
currentLine.startIndex = lineStartIndex
currentLine.endIndex = items.size
processFlexLine(
line = currentLine,
items = items,
flexBoxConfig = flexBoxConfig,
// Subtract the trailing mainAxisGap here
currentLineHypotheticalMainAxisSize =
currentLineHypotheticalMainAxisSize - mainAxisGap,
needsUpfrontCrossAxisCalculation = needsUpfrontCrossAxisCalculation,
constraints = constraints,
remainingCrossAxisSize = remainingCrossAxisSize,
)
totalLinesCrossSize += currentLine.crossAxisSize
currentLine.crossStart = currentCrossPosition
lines.add(currentLine)
}
updateTotalCrossSize(totalLinesCrossSize)
return lines
}
private fun processFlexLine(
line: FlexLine,
items: ArrayList<ResolvedFlexItemInfo>,
flexBoxConfig: ResolvedFlexBoxConfig,
currentLineHypotheticalMainAxisSize: Int,
needsUpfrontCrossAxisCalculation: Boolean,
constraints: OrientationIndependentConstraints,
remainingCrossAxisSize: Int,
) {
// resolve main-axis sizes (flex-grow/shrink).
line.mainAxisSize =
resolveFlexibleLengths(
isHorizontal = flexBoxConfig.isHorizontal,
items = items,
flexBoxConfig = flexBoxConfig,
startIndex = line.startIndex,
endIndex = line.endIndex,
hypotheticalLineSize = currentLineHypotheticalMainAxisSize,
containerMainAxisSize = constraints.mainAxisMax,
)
// calculate the line's height.
if (needsUpfrontCrossAxisCalculation) {
calculateLineCrossAxisSize(
flexBoxConfig = flexBoxConfig,
line = line,
items = items,
remainingCrossAxisSize = remainingCrossAxisSize,
)
}
}
private fun resolveFlexibleLengths(
isHorizontal: Boolean,
items: ArrayList<ResolvedFlexItemInfo>,
flexBoxConfig: ResolvedFlexBoxConfig,
startIndex: Int,
endIndex: Int,
hypotheticalLineSize: Int,
containerMainAxisSize: Int,
): Int {
val itemCount = endIndex - startIndex
val totalGap = if (itemCount > 0) (itemCount - 1) * flexBoxConfig.mainAxisGap() else 0
if (containerMainAxisSize == Constraints.Infinity) {
var lineMainAxisSize = totalGap
items.fastForEachUntil(startIndex, endIndex) { item ->
item.targetMainSize = item.hypotheticalMainSize
lineMainAxisSize += item.targetMainSize
}
return lineMainAxisSize
}
val isGrowing = hypotheticalLineSize < containerMainAxisSize
var unfrozenCount = 0
var sumGrow = 0f
var sumScaledShrink = 0f
var sumFlexBaseSize = 0
var sumFrozenTargetSize = 0
var sumFactors = 0f
// Initial Pass
items.fastForEachUntil(startIndex, endIndex) { item ->
val flexFactor = if (isGrowing) item.grow else item.shrink
if (flexFactor == 0f || (!isGrowing && item.flexBaseSize < item.hypotheticalMainSize)) {
item.targetMainSize = item.hypotheticalMainSize
item.isFrozen = true
sumFrozenTargetSize += item.targetMainSize
} else {
item.isFrozen = false
unfrozenCount++
sumFlexBaseSize += item.flexBaseSize
sumFactors += flexFactor
if (isGrowing) {
sumGrow += item.grow
} else {
sumScaledShrink += (item.shrink * item.flexBaseSize)
}
}
}
val initialFreeSpace =
(containerMainAxisSize - totalGap - sumFrozenTargetSize - sumFlexBaseSize).toFloat()
var allocatedUnfrozenSize = 0
if (isGrowing) {
// For growth, there isn't any upper bound so we only need single pass
val sumSizes = totalGap + sumFrozenTargetSize + sumFlexBaseSize
var freeSpace = (containerMainAxisSize - sumSizes).toFloat()
if (sumFactors < 1f) {
val fractionalSpace = initialFreeSpace * sumFactors
if (abs(fractionalSpace) < abs(freeSpace)) {
freeSpace = fractionalSpace
}
}
var currentFreeSpace = freeSpace
var currentSumGrow = sumGrow
items.fastForEachUntil(startIndex, endIndex) { item ->
if (!item.isFrozen) {
val share = if (currentSumGrow > 0f) item.grow / currentSumGrow else 0f
val spaceToAllocate = (currentFreeSpace * share).fastRoundToInt()
currentFreeSpace -= spaceToAllocate
currentSumGrow -= item.grow
item.targetMainSize = item.flexBaseSize + spaceToAllocate
allocatedUnfrozenSize += item.targetMainSize
}
}
} else {
var needsRedistribution = true
var loopCount = 0
while (needsRedistribution && loopCount < itemCount) {
needsRedistribution = false
loopCount++
allocatedUnfrozenSize = 0
if (unfrozenCount == 0) break
val sumSizes = totalGap + sumFrozenTargetSize + sumFlexBaseSize
var freeSpace = (containerMainAxisSize - sumSizes).toFloat()
if (sumFactors < 1f) {
val fractionalSpace = initialFreeSpace * sumFactors
if (abs(fractionalSpace) < abs(freeSpace)) {
freeSpace = fractionalSpace
}
}
var currentFreeSpace = abs(freeSpace)
var currentSumScaledShrink = sumScaledShrink
items.fastForEachUntil(startIndex, endIndex) { item ->
if (!item.isFrozen) {
val scaledShrink = item.shrink * item.flexBaseSize
val share =
if (currentSumScaledShrink > 0f) scaledShrink / currentSumScaledShrink
else 0f
val spaceToTake = (currentFreeSpace * share).fastRoundToInt()
val targetSize = item.flexBaseSize - spaceToTake
val minSize = item.getMinMainAxisSize(isHorizontal)
if (targetSize < minSize) {
item.isFrozen = true
needsRedistribution = true
val actualSpaceTaken = item.flexBaseSize - minSize
currentFreeSpace -= actualSpaceTaken
currentSumScaledShrink -= scaledShrink
item.targetMainSize = minSize
unfrozenCount--
sumFrozenTargetSize += minSize
sumFlexBaseSize -= item.flexBaseSize
sumScaledShrink -= scaledShrink
sumFactors -= item.shrink
} else {
currentFreeSpace -= spaceToTake
currentSumScaledShrink -= scaledShrink
item.targetMainSize = targetSize
allocatedUnfrozenSize += item.targetMainSize
}
}
}
}
}
return totalGap + sumFrozenTargetSize + allocatedUnfrozenSize
}
// stretch to distribute extra cross-axis space to lines.
private fun applyAlignContentStretch(
flexBoxConfig: ResolvedFlexBoxConfig,
lines: ArrayList<FlexLine>,
constraints: OrientationIndependentConstraints,
totalLinesCrossSize: Int,
crossAxisGap: Int,
): Int {
if (
flexBoxConfig.alignContent != FlexAlignContent.Stretch ||
constraints.crossAxisMin == Constraints.Infinity ||
lines.isEmpty() ||
lines.size == 1
)
return totalLinesCrossSize
val totalSpacing = (lines.size - 1) * crossAxisGap
val containerCrossAxisSize = constraints.crossAxisMin
if ((totalLinesCrossSize + totalSpacing) >= containerCrossAxisSize)
return totalLinesCrossSize
var updatedTotalCrossAxisSize = totalLinesCrossSize
val extraSpace =
(containerCrossAxisSize - totalLinesCrossSize - totalSpacing).fastCoerceAtLeast(0)
val spacePerLine = extraSpace / lines.size
var currentY = 0
lines.fastForEach { line ->
line.crossStart = currentY
line.crossAxisSize += spacePerLine
currentY += line.crossAxisSize + crossAxisGap
updatedTotalCrossAxisSize += spacePerLine
}
return updatedTotalCrossAxisSize
}
// Measures all flex items that haven't been measured yet.
private fun measureFlexItems(
lines: ArrayList<FlexLine>,
items: ArrayList<ResolvedFlexItemInfo>,
flexBoxConfig: ResolvedFlexBoxConfig,
totalLinesCrossSize: Int,
needsUpfrontCrossAxisCalculation: Boolean,
constraints: OrientationIndependentConstraints,
): Int {
var updatedTotalCrossSize = totalLinesCrossSize
var remainingCrossAxisSize: Int = constraints.crossAxisMax
lines.fastForEach { line ->
var lineCrossAxisSize = if (needsUpfrontCrossAxisCalculation) line.crossAxisSize else 0
items.fastForEachUntil(line.startIndex, line.endIndex) { item ->
if (item.placeable != null) {
if (!needsUpfrontCrossAxisCalculation) {
lineCrossAxisSize = max(lineCrossAxisSize, item.crossAxisSize)
}
return@fastForEachUntil
}
val shouldStretch =
item.alignSelf == FlexAlignSelf.Stretch ||
(item.alignSelf == FlexAlignSelf.Auto &&
(flexBoxConfig.alignItems == FlexAlignItems.Stretch ||
flexBoxConfig.alignContent == FlexAlignContent.Stretch))
val crossAxisSize =
measureItem(
item = item,
flexBoxConfig = flexBoxConfig,
lineCrossAxisSize = if (shouldStretch) line.crossAxisSize else 0,
shouldStretch = shouldStretch,
remainingCrossAxisSize = remainingCrossAxisSize,
)
if (!needsUpfrontCrossAxisCalculation) {
lineCrossAxisSize = max(lineCrossAxisSize, crossAxisSize)
}
// If we have single line and constraints are defined then the size of line is the
// constraints instead of tallest item in the line.
if (lines.size == 1) {
lineCrossAxisSize = max(lineCrossAxisSize, constraints.crossAxisMin)
}
}
if (!needsUpfrontCrossAxisCalculation) {
line.crossAxisSize = lineCrossAxisSize
updatedTotalCrossSize += lineCrossAxisSize
}
remainingCrossAxisSize =
(remainingCrossAxisSize - line.crossAxisSize - flexBoxConfig.crossAxisGap())
.fastCoerceAtLeast(0)
}
return if (needsUpfrontCrossAxisCalculation) totalLinesCrossSize else updatedTotalCrossSize
}
// Calculates cross-axis positions for all lines based on align-content.
private fun calculateLineCrossPositions(
flexBoxConfig: ResolvedFlexBoxConfig,
totalCrossAxisSpace: Int,
lines: ArrayList<FlexLine>,
totalLinesCrossSize: Int,
crossAxisGap: Int,
) {
if (lines.isEmpty() || lines.size == 1) return
val totalGap = (lines.size - 1) * crossAxisGap
val freeSpace = totalCrossAxisSpace - totalLinesCrossSize - totalGap
val spaceInBetweenLines =
when (flexBoxConfig.alignContent) {
FlexAlignContent.SpaceAround -> freeSpace / (lines.size)
FlexAlignContent.SpaceBetween -> freeSpace / (lines.size - 1)
else -> 0
}
var crossPosition =
when (flexBoxConfig.alignContent) {
FlexAlignContent.End -> if (flexBoxConfig.isCrossAxisReverse) 0 else freeSpace
FlexAlignContent.Center -> freeSpace / 2
FlexAlignContent.SpaceAround -> spaceInBetweenLines / 2
else -> if (flexBoxConfig.isCrossAxisReverse) freeSpace else 0
}
val indices =
if (flexBoxConfig.isCrossAxisReverse) lines.indices.reversed() else lines.indices
for (index in indices) {
val line = lines[index]
line.crossStart = crossPosition
crossPosition += line.crossAxisSize + spaceInBetweenLines + crossAxisGap
}
}
private fun ArrayList<FlexLine>.totalCrossAxisSize(isReverse: Boolean): Int {
if (isEmpty()) return 0
val index = if (isReverse) 0 else lastIndex
return this[index].crossStart + this[index].crossAxisSize
}
// Positions items within a line along the main axis based on justify-content.
private fun positionItemsOnMainAxis(
items: ArrayList<ResolvedFlexItemInfo>,
flexBoxConfig: ResolvedFlexBoxConfig,
containerMainAxisSize: Int,
line: FlexLine,
mainAxisGap: Int,
isMainAxisReverse: Boolean,
) {
val itemCount = line.endIndex - line.startIndex
if (itemCount == 0) return
val remainingSpace = containerMainAxisSize - line.mainAxisSize
val spaceBetweenItems =
when (flexBoxConfig.justifyContent) {
FlexJustifyContent.SpaceAround -> remainingSpace / itemCount
FlexJustifyContent.SpaceBetween ->
if (itemCount > 1) remainingSpace / (itemCount - 1) else 0
FlexJustifyContent.SpaceEvenly -> (remainingSpace) / (itemCount + 1)
else -> 0
}
var mainPosition =
when (flexBoxConfig.justifyContent) {
FlexJustifyContent.End -> if (isMainAxisReverse) 0 else remainingSpace
FlexJustifyContent.Center -> (remainingSpace) / 2
FlexJustifyContent.SpaceAround -> (spaceBetweenItems) / 2
FlexJustifyContent.SpaceEvenly -> spaceBetweenItems
FlexJustifyContent.SpaceBetween ->
if (itemCount == 1 && isMainAxisReverse) remainingSpace else 0
else -> if (isMainAxisReverse) remainingSpace else 0
}
val indices =
if (isMainAxisReverse) {
(line.endIndex - 1) downTo line.startIndex
} else {
line.startIndex until line.endIndex
}
for (index in indices) {
val item = items[index]
item.mainPosition = mainPosition
mainPosition += item.targetMainSize + spaceBetweenItems + mainAxisGap
// cross-axis position based on alignSelf, falling back to alignItems
val itemCrossAxisSize = item.crossAxisSize
item.crossPosition =
line.crossStart +
calculateItemCrossPosition(
flexConfig = item,
itemBaseline = item.baseline,
lineMaxAboveBaseline = line.maxAboveBaseline,
itemCrossAxisSize = itemCrossAxisSize,
lineCrossAxisSize = line.crossAxisSize,
containerAlignItems = flexBoxConfig.alignItems,
)
}
}
private fun calculateItemCrossPosition(
flexConfig: ResolvedFlexItemInfo,
itemBaseline: Int,
lineMaxAboveBaseline: Int,
itemCrossAxisSize: Int,
lineCrossAxisSize: Int,
containerAlignItems: FlexAlignItems,
): Int {
val effectiveAlignment =
if (flexConfig.alignSelf != FlexAlignSelf.Auto) {
flexConfig.alignSelf
} else {
// Convert AlignItems to equivalent AlignSelf
when (containerAlignItems) {
FlexAlignItems.Start -> FlexAlignSelf.Start
FlexAlignItems.End -> FlexAlignSelf.End
FlexAlignItems.Center -> FlexAlignSelf.Center
FlexAlignItems.Stretch -> FlexAlignSelf.Stretch
FlexAlignItems.Baseline -> FlexAlignSelf.Baseline
else -> FlexAlignSelf.Start
}
}
return when (effectiveAlignment) {
FlexAlignSelf.Start -> 0
FlexAlignSelf.End -> lineCrossAxisSize - itemCrossAxisSize
FlexAlignSelf.Center -> (lineCrossAxisSize - itemCrossAxisSize) / 2
FlexAlignSelf.Stretch -> 0
FlexAlignSelf.Baseline ->
if (itemBaseline != AlignmentLine.Unspecified) {
lineMaxAboveBaseline - itemBaseline
} else {
0
}
else -> 0
}
}
private fun isMainAxisReversedForLayout(flexBoxConfig: ResolvedFlexBoxConfig): Boolean {
return flexBoxConfig.direction == FlexDirection.RowReverse ||
flexBoxConfig.direction == FlexDirection.ColumnReverse
}
// calculate cross axis size for line
private fun calculateLineCrossAxisSize(
items: ArrayList<ResolvedFlexItemInfo>,
flexBoxConfig: ResolvedFlexBoxConfig,
line: FlexLine,
remainingCrossAxisSize: Int,
) {
var lineCrossAxisSize = 0
var maxAboveBaseline = 0
var maxBelowBaseline = 0
val isHorizontal = flexBoxConfig.isHorizontal
items.fastForEachUntil(line.startIndex, line.endIndex) { itemInfo ->
val crossAxisSize =
if (
itemInfo.hasBaseline ||
(flexBoxConfig.hasBaseline && itemInfo.alignSelf == FlexAlignSelf.Auto)
) {
measureItem(
itemInfo,
flexBoxConfig,
lineCrossAxisSize = 0,
shouldStretch = false,
remainingCrossAxisSize = remainingCrossAxisSize,
)
val baseline =
itemInfo.getBaseline(itemInfo.placeable!!, fallback = flexBoxConfig)
itemInfo.baseline = baseline
maxAboveBaseline = max(maxAboveBaseline, baseline)
maxBelowBaseline = max(maxBelowBaseline, itemInfo.crossAxisSize - baseline)
// line cross Axis size
maxAboveBaseline + maxBelowBaseline
} else {
itemInfo.crossAxisSize =
if (isHorizontal)
itemInfo.measurable?.maxIntrinsicHeight(width = itemInfo.targetMainSize)
?: 0
else
itemInfo.measurable?.maxIntrinsicWidth(height = itemInfo.targetMainSize)
?: 0
itemInfo.crossAxisSize
}
lineCrossAxisSize = max(lineCrossAxisSize, crossAxisSize)
}
line.maxAboveBaseline = maxAboveBaseline
line.crossAxisSize = lineCrossAxisSize
}
/**
* Measures an item and updates its crossAxisSize. Returns the cross axis size contribution for
* line height calculation.
*/
private fun measureItem(
item: ResolvedFlexItemInfo,
flexBoxConfig: ResolvedFlexBoxConfig,
lineCrossAxisSize: Int,
shouldStretch: Boolean,
remainingCrossAxisSize: Int,
): Int {
val isHorizontal = flexBoxConfig.isHorizontal
val itemConstraints =
if (shouldStretch && lineCrossAxisSize > 0) {
Constraints.fixed(
width =
if (isHorizontal) item.targetMainSize
else
lineCrossAxisSize.fastCoerceAtMost(
maximumValue = remainingCrossAxisSize
),
height =
if (isHorizontal)
lineCrossAxisSize.fastCoerceAtMost(
maximumValue = remainingCrossAxisSize
)
else item.targetMainSize,
)
} else {
if (isHorizontal) {
Constraints.fitPrioritizingWidth(
minWidth = item.targetMainSize,
maxWidth = item.targetMainSize,
minHeight = 0,
maxHeight = remainingCrossAxisSize,
)
} else {
Constraints.fitPrioritizingHeight(
minWidth = 0,
maxWidth = remainingCrossAxisSize,
minHeight = item.targetMainSize,
maxHeight = item.targetMainSize,
)
}
}
item.placeable = item.measurable?.measure(itemConstraints)
item.crossAxisSize =
if (isHorizontal) {
item.placeable?.height ?: 0
} else {
item.placeable?.width ?: 0
}
item.mainAxisSize =
if (isHorizontal) {
item.placeable?.width ?: 0
} else {
item.placeable?.height ?: 0
}
return item.crossAxisSize
}
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int,
): Int {
if (measurables.isEmpty()) return 0
val config =
resolveFlexBoxConfig(flexBoxConfigState.value, this, Constraints(maxHeight = height))
return if (config.isHorizontal) {
// Main axis. Min = narrowest without clipping.
// Wrap: widest child (each child could be its own line).
// NoWrap: sum of all children (they must all fit on one line).
val gap = config.mainAxisGap()
if (config.isWrapEnabled) {
var maxSize = 0
measurables.fastForEach { maxSize = max(maxSize, it.minIntrinsicWidth(height)) }
maxSize
} else {
measurables.fastSumBy { it.minIntrinsicWidth(height) } +
(measurables.size - 1).coerceAtLeast(0) * gap
}
} else {
// Cross axis. Simulate line breaks along the vertical main axis,
// then take the widest line.
intrinsicCrossAxisSize(
config = config,
measurables = measurables,
mainAxisAvailable = height,
mainAxisSize = { it.minIntrinsicHeight(Constraints.Infinity) },
crossAxisSize = { measurable, mainSize -> measurable.minIntrinsicWidth(mainSize) },
)
}
}
override fun IntrinsicMeasureScope.minIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int,
): Int {
if (measurables.isEmpty()) return 0
val config =
resolveFlexBoxConfig(flexBoxConfigState.value, this, Constraints(maxWidth = width))
return if (config.isHorizontal) {
// Cross axis. Simulate line breaks along the horizontal main axis,
// then sum the tallest-child-per-line heights + cross gaps.
intrinsicCrossAxisSize(
config = config,
measurables = measurables,
mainAxisAvailable = width,
mainAxisSize = { it.minIntrinsicWidth(Constraints.Infinity) },
crossAxisSize = { measurable, mainSize -> measurable.minIntrinsicHeight(mainSize) },
)
} else {
// Main axis.
val gap = config.mainAxisGap()
if (config.isWrapEnabled) {
var maxSize = 0
measurables.fastForEach { maxSize = max(maxSize, it.minIntrinsicHeight(width)) }
maxSize
} else {
measurables.fastSumBy { it.minIntrinsicHeight(width) } +
(measurables.size - 1).coerceAtLeast(0) * gap
}
}
}
override fun IntrinsicMeasureScope.maxIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int,
): Int {
if (measurables.isEmpty()) return 0
val config =
resolveFlexBoxConfig(flexBoxConfigState.value, this, Constraints(maxHeight = height))
return if (config.isHorizontal) {
// Main axis. Max = preferred unwrapped size, regardless of wrap setting.
val gap = config.mainAxisGap()
measurables.fastSumBy { it.maxIntrinsicWidth(height) } +
(measurables.size - 1).coerceAtLeast(0) * gap
} else {
// Cross axis.
intrinsicCrossAxisSize(
config = config,
measurables = measurables,
mainAxisAvailable = height,
mainAxisSize = { it.maxIntrinsicHeight(Constraints.Infinity) },
crossAxisSize = { measurable, mainSize -> measurable.maxIntrinsicWidth(mainSize) },
)
}
}
override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int,
): Int {
if (measurables.isEmpty()) return 0
val config =
resolveFlexBoxConfig(flexBoxConfigState.value, this, Constraints(maxWidth = width))
return if (config.isHorizontal) {
// Cross axis.
intrinsicCrossAxisSize(
config = config,
measurables = measurables,
mainAxisAvailable = width,
mainAxisSize = { it.maxIntrinsicWidth(Constraints.Infinity) },
crossAxisSize = { measurable, mainSize -> measurable.maxIntrinsicHeight(mainSize) },
)
} else {
// Main axis. Max = preferred unwrapped size.
val gap = config.mainAxisGap()
measurables.fastSumBy { it.maxIntrinsicHeight(width) } +
(measurables.size - 1).coerceAtLeast(0) * gap
}
}
/**
* Simulates line-breaking along the main axis and sums up per-line cross-axis sizes to compute
* the total cross-axis intrinsic size.
*
* This is an approximation: child cross sizes are queried at their unconstrained main-axis size
* rather than their post-flex-resolution size, since the full flex algorithm cannot run during
* intrinsic measurement.
*/
private inline fun intrinsicCrossAxisSize(
config: ResolvedFlexBoxConfig,
measurables: List<IntrinsicMeasurable>,
mainAxisAvailable: Int,
mainAxisSize: (IntrinsicMeasurable) -> Int,
crossAxisSize: (IntrinsicMeasurable, Int) -> Int,
): Int {
val mainAxisGap = config.mainAxisGap()
val crossAxisGap = config.crossAxisGap()
var currentLineMainAxisSize = 0
var currentLineCrossAxisSize = 0
var totalCrossAxisSize = 0
var isFirstItemInLine = true
measurables.fastForEach { measurable ->
val itemMainAxisSize = mainAxisSize(measurable)
val itemCrossAxisSize = crossAxisSize(measurable, itemMainAxisSize)
// Would adding this item (plus the gap before it) overflow the line?
val projectedLineSize =
if (isFirstItemInLine) {
itemMainAxisSize
} else {
currentLineMainAxisSize + mainAxisGap + itemMainAxisSize
}
if (
config.isWrapEnabled && !isFirstItemInLine && projectedLineSize > mainAxisAvailable
) {
// Finalize current line and start a new one.
totalCrossAxisSize += currentLineCrossAxisSize + crossAxisGap
currentLineMainAxisSize = itemMainAxisSize
currentLineCrossAxisSize = itemCrossAxisSize
// remains false for the next iteration, which will be the second item of this new
// line
} else {
currentLineMainAxisSize = projectedLineSize
currentLineCrossAxisSize = max(currentLineCrossAxisSize, itemCrossAxisSize)
isFirstItemInLine = false
}
}
// Add the last line (no trailing crossAxisGap).
totalCrossAxisSize += currentLineCrossAxisSize
return totalCrossAxisSize
}
/** Resolves and snapshots the container configuration. */
private fun resolveFlexBoxConfig(
flexBoxConfig: FlexBoxConfig,
density: Density,
constraints: Constraints,
): ResolvedFlexBoxConfig {
resolvedFlexBoxConfig.prepare(density, constraints)
with(flexBoxConfig) { resolvedFlexBoxConfig.configure() }
return resolvedFlexBoxConfig
}
}
/**
* Scope for the content of a [FlexBox]. Provides the [flex] modifier for configuring individual
* flex item properties.
*
* @sample androidx.compose.foundation.layout.samples.FlexBoxScopeSample
* @see FlexBox
* @see FlexConfig
*/
@LayoutScopeMarker
@Immutable
@JvmDefaultWithCompatibility
@ExperimentalFlexBoxApi
interface FlexBoxScope {
/**
* Configures the flex properties of this element within the [FlexBox] using the provided
* [FlexConfig].
*
* @sample androidx.compose.foundation.layout.samples.FlexModifierWithConfigSample
* @param flexConfig The flex configuration to apply.
* @see FlexConfig
*/
@Stable fun Modifier.flex(flexConfig: FlexConfig): Modifier
/**
* Configures the flex properties of this element within the [FlexBox] using a configuration
* lambda.
*
* This modifier allows you to specify how an individual item should share available space
* (grow, shrink, basis) and how it aligns itself along the cross axis (alignSelf).
*
* @sample androidx.compose.foundation.layout.samples.FlexModifierWithLambdaSample
* @param flexConfig A lambda that configures the flex properties within a [FlexConfigScope].
* @see FlexConfigScope
*/
@Stable
fun Modifier.flex(flexConfig: FlexConfigScope.() -> Unit): Modifier =
flex(FlexConfig(flexConfig))
}
@PublishedApi
@ExperimentalFlexBoxApi
internal object FlexBoxScopeInstance : FlexBoxScope {
@Stable
override fun Modifier.flex(flexConfig: FlexConfig): Modifier {
return this.then(FlexBoxChildElement(flexConfig))
}
}
/** ModifierNodeElement for flex item config. */
@OptIn(ExperimentalFlexBoxApi::class)
internal class FlexBoxChildElement(val config: FlexConfig) :
ModifierNodeElement<FlexBoxChildDataNode>() {
override fun create(): FlexBoxChildDataNode = FlexBoxChildDataNode(config)
override fun update(node: FlexBoxChildDataNode) {
node.config = config
}
override fun InspectorInfo.inspectableProperties() {
name = "flex"
properties["config"] = config
}
override fun hashCode(): Int = config.hashCode()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is FlexBoxChildElement) return false
return config == other.config
}
}
@OptIn(ExperimentalFlexBoxApi::class)
internal class FlexBoxChildDataNode(var config: FlexConfig) :
ParentDataModifierNode, Modifier.Node() {
override fun Density.modifyParentData(parentData: Any?): Any = this@FlexBoxChildDataNode
}
/**
* Defines the direction of the main axis in a [FlexBox] container.
*
* The main axis determines the primary direction in which children are laid out. It establishes the
* `main-start` and `main-end` edges of the container. The cross axis is always perpendicular to the
* main axis.
*
* @see FlexBoxConfigScope.direction
*/
@JvmInline
@ExperimentalFlexBoxApi
value class FlexDirection @PublishedApi internal constructor(private val bits: Int) {
override fun toString() =
when (bits) {
0 -> "Row"
1 -> "Column"
2 -> "RowReverse"
3 -> "ColumnReverse"
else -> "INVALID"
}
companion object {
/**
* The main axis is horizontal. Items are placed starting from the `main-start` edge and
* flowing toward the `main-end` edge.
*
* In a Left-To-Right (LTR) layout direction, `main-start` corresponds to the start (left)
* edge of the container. In a Right-To-Left (RTL) layout direction, `main-start`
* corresponds to the end (right).
*/
inline val Row
get() = FlexDirection(0)
/**
* The main axis is vertical. Items are placed starting from the `main-start` edge (the top
* of the container) and flowing toward the `main-end` edge (the bottom).
*/
inline val Column
get() = FlexDirection(1)
/**
* The main axis is horizontal, but the placement direction is reversed. The `main-start`
* and `main-end` edges are swapped.
*
* In a Left-To-Right (LTR) layout direction, `main-start` becomes the right edge of the
* container, and items flow leftward. In a Right-To-Left (RTL) layout direction,
* `main-start` becomes the left edge.
*/
inline val RowReverse
get() = FlexDirection(2)
/**
* The main axis is vertical, but the placement direction is reversed. The `main-start` edge
* becomes the bottom of the container, and items flow toward the `main-end` edge at the
* top.
*/
inline val ColumnReverse
get() = FlexDirection(3)
}
}
/**
* Defines whether flex items are forced onto a single line or can wrap onto multiple lines.
*
* @see FlexBoxConfigScope.wrap
*/
@JvmInline
@ExperimentalFlexBoxApi
value class FlexWrap @PublishedApi internal constructor(private val bits: Int) {
override fun toString(): String =
when (bits) {
0 -> "NoWrap"
1 -> "Wrap"
2 -> "WrapReverse"
else -> "INVALID"
}
companion object {
/**
* Items are laid out in a single line. Items will shrink to fit the container if their
* [FlexConfigScope.shrink] factor allows it. If they cannot shrink enough to fit the main
* axis (for example, due to their minimum intrinsic sizes), they will visually overflow on
* main axis of the container.
*/
inline val NoWrap
get() = FlexWrap(0)
/**
* Items wrap onto multiple lines if they exceed the main axis size. New lines are added
* along the cross axis, starting from the `cross-start` edge and flowing toward the
* `cross-end` edge. (For example, top-to-bottom in a [FlexDirection.Row]).
*/
inline val Wrap
get() = FlexWrap(1)
/**
* Items wrap onto multiple lines if they exceed the main axis size. New lines are added in
* the reverse direction along the cross axis, starting from the `cross-end` edge and
* flowing toward the `cross-start` edge. (For example, bottom-to-top in a
* [FlexDirection.Row]).
*/
inline val WrapReverse
get() = FlexWrap(2)
}
}
/**
* Defines the default alignment for items along the cross axis within their respective lines. This
* controls how items are positioned perpendicular to the main axis. This can be overridden for an
* individual item using [FlexConfigScope.alignSelf].
*
* @see FlexBoxConfigScope.alignItems
* @see FlexAlignSelf
*/
@JvmInline
@ExperimentalFlexBoxApi
value class FlexAlignItems @PublishedApi internal constructor(private val bits: Int) {
override fun toString(): String =
when (bits) {
0 -> "Start"
1 -> "End"
2 -> "Center"
3 -> "Stretch"
4 -> "Baseline"
else -> "INVALID"
}
companion object {
/** Items are aligned toward the cross-start edge of their line. */
inline val Start
get() = FlexAlignItems(0)
/** Items are aligned toward the cross-end edge of their line. */
inline val End
get() = FlexAlignItems(1)
/** Items are centered along the cross axis within their line. */
inline val Center
get() = FlexAlignItems(2)
/** Items are stretched to fill the cross axis size of their line. */
inline val Stretch
get() = FlexAlignItems(3)
/**
* Items are aligned such that their baselines match along the cross axis. Items without a
* baseline fall back to [Start] alignment.
*/
inline val Baseline
get() = FlexAlignItems(4)
}
}
/**
* Defines the cross-axis alignment for a single flex item, overriding the container's
* [FlexAlignItems].
*
* This controls how an individual item is positioned perpendicular to the main axis within its
* respective line.
*
* @see FlexConfigScope.alignSelf
* @see FlexAlignItems
*/
@JvmInline
@ExperimentalFlexBoxApi
value class FlexAlignSelf @PublishedApi internal constructor(private val bits: Int) {
override fun toString(): String =
when (bits) {
0 -> "Auto"
1 -> "Start"
2 -> "End"
3 -> "Center"
4 -> "Stretch"
5 -> "Baseline"
else -> "INVALID"
}
companion object {
/**
* Inherits the alignment from the container's [FlexBoxConfigScope.alignItems]. This is the
* default value.
*/
inline val Auto
get() = FlexAlignSelf(0)
/** The item is aligned toward the `cross-start` edge of its line. */
inline val Start
get() = FlexAlignSelf(1)
/** The item is aligned toward the `cross-end` edge of its line. */
inline val End
get() = FlexAlignSelf(2)
/** The item is centered along the cross axis within its line. */
inline val Center
get() = FlexAlignSelf(3)
/** The item is stretched to fill the cross axis size of its line. */
inline val Stretch
get() = FlexAlignSelf(4)
/**
* The item is aligned such that its baseline matches the baseline of other baseline-aligned
* items in the line. Items without a baseline fall back to [Start] alignment.
*/
inline val Baseline
get() = FlexAlignSelf(5)
}
}
/**
* Defines how multiple lines are distributed along the cross axis. This only applies when wrapping
* is enabled ([FlexWrap.Wrap] or [FlexWrap.WrapReverse]), the container has extra cross-axis space,
* and there is more than one line of items.
*
* @see FlexBoxConfigScope.alignContent
*/
@JvmInline
@ExperimentalFlexBoxApi
value class FlexAlignContent @PublishedApi internal constructor(private val bits: Int) {
override fun toString(): String =
when (bits) {
0 -> "Start"
1 -> "End"
2 -> "Center"
3 -> "Stretch"
4 -> "SpaceBetween"
5 -> "SpaceAround"
else -> "INVALID"
}
companion object {
/**
* Place lines such that they are as close as possible to the `cross-start` edge of the
* container.
*/
inline val Start
get() = FlexAlignContent(0)
/**
* Place lines such that they are as close as possible to the `cross-end` edge of the
* container.
*/
inline val End
get() = FlexAlignContent(1)
/**
* Place lines such that they are as close as possible to the middle of the container's
* cross axis.
*/
inline val Center
get() = FlexAlignContent(2)
/**
* Distribute remaining free space evenly among all lines, increasing their cross-axis size
* to fill the available space.
*/
inline val Stretch
get() = FlexAlignContent(3)
/**
* Place lines such that they are spaced evenly across the cross axis, without free space
* before the first line or after the last line.
*/
inline val SpaceBetween
get() = FlexAlignContent(4)
/**
* Place lines such that they are spaced evenly across the cross axis, including free space
* before the first line and after the last line, but half the amount of space existing
* otherwise between two consecutive lines.
*/
inline val SpaceAround
get() = FlexAlignContent(5)
}
}
/**
* Defines the arrangement of items along the main axis of their respective lines. This controls how
* free space is distributed between and around items after their main axis sizes have been
* resolved.
*
* @see FlexBoxConfigScope.justifyContent
*/
@JvmInline
@ExperimentalFlexBoxApi
value class FlexJustifyContent @PublishedApi internal constructor(private val bits: Int) {
override fun toString(): String =
when (bits) {
0 -> "Start"
1 -> "End"
2 -> "Center"
3 -> "SpaceBetween"
4 -> "SpaceAround"
5 -> "SpaceEvenly"
else -> "INVALID"
}
companion object {
/**
* Place items such that they are as close as possible to the `main-start` edge of their
* line.
*/
inline val Start
get() = FlexJustifyContent(0)
/**
* Place items such that they are as close as possible to the `main-end` edge of their line.
*/
inline val End
get() = FlexJustifyContent(1)
/**
* Place items such that they are as close as possible to the middle of the main axis within
* their line.
*/
inline val Center
get() = FlexJustifyContent(2)
/**
* Place items such that they are spaced evenly across the main axis, without free space
* before the first item or after the last item.
*/
inline val SpaceBetween
get() = FlexJustifyContent(3)
/**
* Place items such that they are spaced evenly across the main axis, including free space
* before the first item and after the last item, but half the amount of space existing
* otherwise between two consecutive items.
*/
inline val SpaceAround
get() = FlexJustifyContent(4)
/**
* Place items such that they are spaced evenly across the main axis, including free space
* before the first item and after the last item.
*/
inline val SpaceEvenly
get() = FlexJustifyContent(5)
}
}
/**
* Defines the initial main size of a flex item before free space distribution.
* - [Auto]: Uses the item's explicitly set size, or falls back to its natural content size.
* - [Dp]: Uses a fixed exact size in [androidx.compose.ui.unit.Dp].
* - [Percent]: Uses a fraction of the container's main axis size.
*
* @see FlexConfigScope.basis
*/
@JvmInline
@ExperimentalFlexBoxApi
value class FlexBasis
@PublishedApi
internal constructor(@PublishedApi internal val packedValue: Long) {
companion object {
private const val TypeShift = 32
private const val TypeAuto = 0L
private const val TypeDp = 1L
private const val TypePercent = 2L
/**
* Use the item's maximum intrinsic size as the basis.
*
* If the item has an explicitly set size modifier along the main axis (for example,
* `Modifier.width` in a [FlexDirection.Row]), that exact size will be used as the basis.
* Otherwise, it falls back to measuring the item's preferred natural content size without
* constraints.
*
* This is the default value.
*/
val Auto = FlexBasis(TypeAuto shl TypeShift)
/**
* Use a fixed size in [androidx.compose.ui.unit.Dp] as the basis.
*
* @sample androidx.compose.foundation.layout.samples.FlexBasisDpSample
* @param value The basis size in Dp.
*/
fun Dp(value: Dp): FlexBasis {
val valueBits = value.value.toBits().toLong() and 0xFFFFFFFFL
return FlexBasis((TypeDp shl TypeShift) or valueBits)
}
/**
* Use a fraction of the container's main axis size as the basis.
*
* @sample androidx.compose.foundation.layout.samples.FlexBasisPercentSample
* @param value A value between 0.0 and 1.0 representing the percentage.
*/
fun Percent(@FloatRange(0.0, 1.0) value: Float): FlexBasis {
val valueBits = value.toBits().toLong() and 0xFFFFFFFFL
return FlexBasis((TypePercent shl TypeShift) or valueBits)
}
}
internal val isAuto: Boolean
get() = (packedValue ushr TypeShift) == TypeAuto
internal val isDp: Boolean
get() = (packedValue ushr TypeShift) == TypeDp
internal val isPercent: Boolean
get() = (packedValue ushr TypeShift) == TypePercent
internal val value: Float
get() = Float.fromBits(packedValue.toInt())
override fun toString(): String =
when {
isAuto -> "FlexBasis.Auto"
isDp -> "FlexBasis.Dp(${value}.dp)"
isPercent -> "FlexBasis.Percent(${(value * 100).roundToInt()}%)"
else -> "FlexBasis.Unknown"
}
}
/**
* Represents a configuration for a [FlexBox] container.
*
* This configuration is defined via a lambda that operates on a [FlexBoxConfigScope]. Because this
* configuration block is executed during the layout phase rather than the composition phase,
* reading state variables inside the block will only trigger a layout pass, completely avoiding
* costly recompositions.
*
* Configuration properties are applied sequentially. If a property is configured multiple times
* within the block, the final call takes precedence.
*
* **Reusability and Responsiveness**
*
* Configurations can be extracted, saved, and reused across multiple [FlexBox] containers:
*
* @sample androidx.compose.foundation.layout.samples.FlexBoxConfigReusableSample
*
* Furthermore, because the [FlexBoxConfigScope] provides direct access to the incoming
* [Constraints][androidx.compose.ui.unit.Constraints], you can easily create responsive
* configurations that dynamically adapt their direction, wrapping, or gaps based on the available
* screen space:
*
* @sample androidx.compose.foundation.layout.samples.FlexBoxConfigResponsiveSample
* @see FlexBoxConfigScope
* @see FlexBox
*/
@Stable
@ExperimentalFlexBoxApi
fun interface FlexBoxConfig {
/**
* Applies the configuration to the given [FlexBoxConfigScope]. This method is invoked by the
* layout system during the measurement phase, not during composition.
*/
fun FlexBoxConfigScope.configure()
/**
* Merges this config with another. Configs further "to the right" will override properties to
* the left of them, on a per-property basis.
*
* @sample androidx.compose.foundation.layout.samples.FlexBoxConfigCombineSample
* @param other the config to merge into the receiver.
*/
infix fun then(other: FlexBoxConfig): FlexBoxConfig =
when {
(other === Companion) -> this
other is CombinedFlexBoxConfig -> CombinedFlexBoxConfig(this, *other.configs)
else -> CombinedFlexBoxConfig(this, other)
}
companion object : FlexBoxConfig {
/**
* A default configuration that lays out items in a horizontal row without wrapping, with
*
* items aligned to the start on both axes and no gaps.
*/
override fun FlexBoxConfigScope.configure() {}
/** Identity elision: merging the identity with any config yields that config. */
override fun then(other: FlexBoxConfig): FlexBoxConfig = other
}
}
/**
* Receiver scope for configuring [FlexBox] container properties.
*
* This scope is provided by [FlexBoxConfig]. All configuration functions are called during the
* layout/measure phase, not during composition. Changes to state-backed values read within this
* scope will trigger a relayout, entirely skipping recomposition.
*
* @see FlexBoxConfig
*/
@ExperimentalFlexBoxApi
sealed interface FlexBoxConfigScope : Density {
/**
* The layout constraints passed to this [FlexBox] from its parent.
*
* Use this for creating responsive layouts that dynamically adapt their properties (like
* direction, wrapping, or gaps) based on the available incoming space.
*
* @sample androidx.compose.foundation.layout.samples.FlexBoxConstraintsSample
* @see Constraints
*/
val constraints: Constraints
/**
* Sets the direction of the main axis along which children are laid out.
*
* The main axis determines the primary direction of item placement:
* - [FlexDirection.Row]: Items placed horizontally, `main-start` to `main-end` (end to start in
* RTL).
* - [FlexDirection.RowReverse]: Items placed horizontally, end to start (start to end in RTL).
* - [FlexDirection.Column]: Items placed vertically, top to bottom.
* - [FlexDirection.ColumnReverse]: Items placed vertically, bottom to top.
*
* @sample androidx.compose.foundation.layout.samples.FlexBoxDirectionSample
* @param value The flex direction. Default is [FlexDirection.Row].
* @see FlexDirection
*/
fun direction(value: FlexDirection)
/**
* Sets whether children are forced onto a single line or can wrap onto multiple lines.
* - [FlexWrap.NoWrap]: All items stay on one line. Items may visually overflow on main axis if
* they cannot shrink enough.
* - [FlexWrap.Wrap]: Items wrap to new lines toward the `cross-end` edge.
* - [FlexWrap.WrapReverse]: Items wrap to new lines toward the `cross-start` edge.
*
* @sample androidx.compose.foundation.layout.samples.FlexBoxWrapSample
* @param value The wrap behavior. Default is [FlexWrap.NoWrap].
* @see FlexWrap
*/
fun wrap(value: FlexWrap)
/**
* Sets how children are distributed along the main axis.
*
* This controls the spacing and positioning of items within each line after their main axis
* sizes have been resolved.
* - [FlexJustifyContent.Start]: Items packed toward the `main-start` edge.
* - [FlexJustifyContent.End]: Items packed toward the `main-end` edge.
* - [FlexJustifyContent.Center]: Items centered along the main axis.
* - [FlexJustifyContent.SpaceBetween]: Items evenly distributed; first at start, last at end.
* - [FlexJustifyContent.SpaceAround]: Items evenly distributed with half-size space at edges.
* - [FlexJustifyContent.SpaceEvenly]: Items evenly distributed with equal space everywhere.
*
* @sample androidx.compose.foundation.layout.samples.FlexBoxJustifyContentSample
* @param value The justify content value. Default is [FlexJustifyContent.Start].
* @see FlexJustifyContent
*/
fun justifyContent(value: FlexJustifyContent)
/**
* Sets the default alignment for children along the cross axis within each line.
*
* This controls how items are positioned perpendicular to the main axis. Individual items can
* override this default alignment using [FlexConfigScope.alignSelf].
* - [FlexAlignItems.Start]: Items aligned to the `cross-start` edge within the line.
* - [FlexAlignItems.End]: Items aligned to the `cross-end` edge within the line.
* - [FlexAlignItems.Center]: Items centered along the cross axis within the line.
* - [FlexAlignItems.Stretch]: Items stretched to fill the line's cross-axis size.
* - [FlexAlignItems.Baseline]: Items aligned by their baseline within the line.
*
* @sample androidx.compose.foundation.layout.samples.FlexBoxAlignItemsSample
* @param value The align items value. Default is [FlexAlignItems.Start].
* @see FlexAlignItems
* @see FlexConfigScope.alignSelf
*/
fun alignItems(value: FlexAlignItems)
/**
* Aligns all items to a specific baseline.
*
* This is equivalent to calling `alignItems(FlexAlignItems.Baseline)` but allows specifying
* exactly which alignment line to use (e.g., [FirstBaseline] or [LastBaseline]).
*
* @sample androidx.compose.foundation.layout.samples.FlexBoxAlignItemsBaselineSample
* @param alignmentLine The alignment line to use.
* @see AlignmentLine
*/
fun alignItems(alignmentLine: AlignmentLine)
/**
* Aligns all items to a custom baseline computed from each measured item.
*
* Use this when you need custom baseline calculation logic. This functions similarly to
* [RowScope.alignBy] and [ColumnScope.alignBy].
*
* @sample androidx.compose.foundation.layout.samples.FlexBoxAlignItemsCustomBaselineSample
* @param alignmentLineBlock A function that computes the baseline position from a [Measured]
* item.
* @see Measured
*/
fun alignItems(alignmentLineBlock: (Measured) -> Int)
/**
* Sets how multiple lines are distributed along the cross axis.
*
* This only applies when [wrap] is [FlexWrap.Wrap] or [FlexWrap.WrapReverse] and there are
* multiple lines of items.
* - [FlexAlignContent.Start]: Lines packed toward the `cross-start` edge.
* - [FlexAlignContent.End]: Lines packed toward the `cross-end` edge.
* - [FlexAlignContent.Center]: Lines centered along the cross axis.
* - [FlexAlignContent.Stretch]: Lines stretched to fill available cross-axis space.
* - [FlexAlignContent.SpaceBetween]: Lines evenly distributed; first at start, last at end.
* - [FlexAlignContent.SpaceAround]: Lines evenly distributed with half-size space at edges.
*
* @sample androidx.compose.foundation.layout.samples.FlexBoxAlignContentSample
* @param value The align content value. Default is [FlexAlignContent.Start].
* @see FlexAlignContent
* @see wrap
*/
fun alignContent(value: FlexAlignContent)
/**
* Sets the vertical spacing between items or lines.
*
* Regardless of the flex [direction], this always applies spacing along the vertical axis
* (Y-axis). In a horizontal layout with wrapping, this represents the space between wrapped
* lines. In a vertical layout, this represents the space between the items themselves.
*
* @sample androidx.compose.foundation.layout.samples.FlexBoxRowGapSample
* @param value The vertical gap size. Default is `0.dp`.
* @see columnGap
* @see gap
*/
fun rowGap(value: Dp)
/**
* Sets the horizontal spacing between items or columns.
*
* Regardless of the flex [direction], this always applies spacing along the horizontal axis
* (X-axis). In a horizontal layout, this represents the space between the items themselves. In
* a vertical layout with wrapping, this represents the space between wrapped columns.
*
* @sample androidx.compose.foundation.layout.samples.FlexBoxColumnGapSample
* @param value The horizontal gap size. Default is `0.dp`.
* @see rowGap
* @see gap
*/
fun columnGap(value: Dp)
/**
* Sets both [rowGap] and [columnGap] to the same value.
*
* This is a convenience function for uniform spacing across both axes.
*
* @sample androidx.compose.foundation.layout.samples.FlexBoxGapSample
* @param all The gap size to apply to both row and column gaps.
* @see rowGap
* @see columnGap
*/
fun gap(all: Dp)
/**
* Sets [rowGap] and [columnGap] to different values.
*
* @sample androidx.compose.foundation.layout.samples.FlexBoxGapDifferentSample
* @param row The vertical spacing (Y-axis).
* @param column The horizontal spacing (X-axis).
* @see rowGap
* @see columnGap
*/
fun gap(row: Dp, column: Dp)
}
@OptIn(ExperimentalFlexBoxApi::class)
internal class ResolvedFlexBoxConfig : FlexBoxConfigScope {
private var _density: Density = DefaultDensity
// Baseline alignment - null means use alignItems
var baselineAlignmentLine: AlignmentLine? = null
private set
var baselineAlignmentBlock: AlignmentLineProviderBlock? = null
private set
override val density: Float
get() = _density.density
override val fontScale: Float
get() = _density.fontScale
override fun Dp.toSp(): TextUnit = with(_density) { this@toSp.toSp() }
override fun TextUnit.toDp(): Dp = with(_density) { this@toDp.toDp() }
override var constraints: Constraints = Constraints()
private set
internal var direction: FlexDirection = FlexDirection.Row
internal var wrap: FlexWrap = FlexWrap.NoWrap
internal var justifyContent: FlexJustifyContent = FlexJustifyContent.Start
internal var alignItems: FlexAlignItems = FlexAlignItems.Start
internal var alignContent: FlexAlignContent = FlexAlignContent.Start
internal var rowGap: Dp = 0.dp
internal var columnGap: Dp = 0.dp
override fun direction(value: FlexDirection) {
this.direction = value
}
override fun wrap(value: FlexWrap) {
this.wrap = value
}
override fun justifyContent(value: FlexJustifyContent) {
this.justifyContent = value
}
override fun alignItems(value: FlexAlignItems) {
this.alignItems = value
}
override fun gap(all: Dp) {
rowGap = all
columnGap = all
}
override fun alignItems(alignmentLine: AlignmentLine) {
alignItems = FlexAlignItems.Baseline
baselineAlignmentLine = alignmentLine
baselineAlignmentBlock = null
}
override fun alignItems(alignmentLineBlock: (Measured) -> Int) {
alignItems = FlexAlignItems.Baseline
baselineAlignmentLine = null
baselineAlignmentBlock = AlignmentLineProviderBlock { alignmentLineBlock(it) }
}
override fun alignContent(value: FlexAlignContent) {
this.alignContent = value
}
override fun rowGap(value: Dp) {
this.rowGap = value
}
override fun columnGap(value: Dp) {
this.columnGap = value
}
override fun gap(row: Dp, column: Dp) {
this.rowGap = row
this.columnGap = column
}
internal fun getBaseline(placeable: Placeable): Int {
return when {
baselineAlignmentBlock != null ->
baselineAlignmentBlock!!.calculateAlignmentLinePosition(placeable)
baselineAlignmentLine != null -> {
val value = placeable[baselineAlignmentLine!!]
if (value != AlignmentLine.Unspecified) {
value
} else {
if (baselineAlignmentLine!! is VerticalAlignmentLine) {
placeable.width
} else {
placeable.height
}
}
}
else -> {
val value = placeable[FirstBaseline]
if (value != AlignmentLine.Unspecified) value else placeable.height
}
}
}
fun prepare(density: Density, constraints: Constraints) {
this._density = density
this.constraints = constraints
// Reset to defaults
direction = FlexDirection.Row
wrap = FlexWrap.NoWrap
justifyContent = FlexJustifyContent.Start
alignItems = FlexAlignItems.Start
alignContent = FlexAlignContent.Start
rowGap = 0.dp
columnGap = 0.dp
baselineAlignmentLine = null
baselineAlignmentBlock = null
}
inline val isHorizontal: Boolean
get() = direction == FlexDirection.Row || direction == FlexDirection.RowReverse
inline val isWrapEnabled: Boolean
get() = wrap == FlexWrap.Wrap || wrap == FlexWrap.WrapReverse
inline val isCrossAxisReverse: Boolean
get() = wrap == FlexWrap.WrapReverse
inline val hasBaseline: Boolean
get() = alignItems == FlexAlignItems.Baseline
/** Main axis gap (column gap for horizontal, row gap for vertical) */
fun mainAxisGap(): Int = if (isHorizontal) columnGap.roundToPx() else rowGap.roundToPx()
/** Cross axis gap (row gap for horizontal, column gap for vertical) */
fun crossAxisGap(): Int = if (isHorizontal) rowGap.roundToPx() else columnGap.roundToPx()
override fun toString(): String {
return """
FlexBoxConfig(
direction = ${direction},
wrap = ${wrap},
justifyContent = ${justifyContent},
alignItems = ${alignItems},
alignContent = ${alignContent},
rowGap = ${rowGap},
columnGap = $columnGap
)
"""
.trimIndent()
}
}
/**
* Represents a configuration for a flex item within a [FlexBox].
*
* This configuration is defined via a lambda that operates on a [FlexConfigScope]. Because this
* configuration block is executed during the layout phase rather than the composition phase,
* reading state variables inside the block will only trigger a layout pass, completely avoiding
* costly recompositions.
*
* Configuration properties are applied sequentially. If a property (such as
* [grow][FlexConfigScope.grow] or [shrink][FlexConfigScope.shrink]) is assigned multiple times
* within the configuration block, the final call takes precedence.
*
* @sample androidx.compose.foundation.layout.samples.FlexConfigSample
* @see FlexConfigScope
* @see FlexBoxScope.flex
*/
@Stable
@ExperimentalFlexBoxApi
fun interface FlexConfig {
/**
* Applies the configuration to the given [FlexConfigScope].This method is invoked by the layout
* system during the measurement phase, not during composition.
*/
fun FlexConfigScope.configure()
/**
* Merges this config with another. Configs further "to the right" will override properties to
* the left of them, on a per-property basis.
*
* @sample androidx.compose.foundation.layout.samples.FlexConfigCombineSample
* @param other the config to merge into the receiver.
*/
infix fun then(other: FlexConfig): FlexConfig =
when {
(other === Companion) -> this
other is CombinedFlexConfig -> CombinedFlexConfig(this, *other.configs)
else -> CombinedFlexConfig(this, other)
}
companion object : FlexConfig {
override fun FlexConfigScope.configure() {}
/** Merging the identity with any config yields that config. */
override fun then(other: FlexConfig): FlexConfig = other
}
}
/**
* Scope for configuring flex item properties within a [FlexBox].
*
* All configuration functions are called during the layout/measure phase, not during composition.
*
* @sample androidx.compose.foundation.layout.samples.FlexConfigScopeSample
* @see FlexConfig
*/
@ExperimentalFlexBoxApi
sealed interface FlexConfigScope : Density {
/**
* The maximum size of the FlexBox container along the main axis. Corresponds to
* [Constraints.maxWidth] for [FlexDirection.Row]/[FlexDirection.RowReverse], or
* [Constraints.maxHeight] for [FlexDirection.Column]/[FlexDirection.ColumnReverse]. Use this
* for responsive item sizing based on the container's available space.
*/
val flexBoxMainAxisMax: Int
/**
* The minimum size of the FlexBox container along the main axis. Corresponds to
* [Constraints.minWidth] for [FlexDirection.Row]/[FlexDirection.RowReverse], or
* [Constraints.minHeight] for [FlexDirection.Column]/[FlexDirection.ColumnReverse].
*/
val flexBoxMainAxisMin: Int
/**
* The maximum size of the FlexBox container along the cross axis. Corresponds to
* [Constraints.maxHeight] for [FlexDirection.Row]/[FlexDirection.RowReverse], or
* [Constraints.maxWidth] for [FlexDirection.Column]/[FlexDirection.ColumnReverse].
*/
val flexBoxCrossAxisMax: Int
/**
* The minimum size of the FlexBox container along the cross axis. Corresponds to
* [Constraints.minHeight] for [FlexDirection.Row]/[FlexDirection.RowReverse], or
* [Constraints.minWidth] for [FlexDirection.Column]/[FlexDirection.ColumnReverse].
*/
val flexBoxCrossAxisMin: Int
/**
* Overrides the container's [FlexBoxConfigScope.alignItems] for this specific item.
*
* This controls how the individual item is positioned perpendicular to the main axis within its
* respective line.
* - [FlexAlignSelf.Auto]: Inherits the container's alignment (default).
* - [FlexAlignSelf.Start]: Aligns to the `cross-start` edge of its line.
* - [FlexAlignSelf.End]: Aligns to the `cross-end` edge of its line.
* - [FlexAlignSelf.Center]: Centers along the cross axis within its line.
* - [FlexAlignSelf.Stretch]: Stretches to fill the line's cross-axis size.
* - [FlexAlignSelf.Baseline]: Aligns by baseline within its line.
*
* @sample androidx.compose.foundation.layout.samples.FlexAlignSelfSample
* @param value The alignment for this item. Default is [FlexAlignSelf.Auto].
* @see FlexAlignSelf
* @see FlexBoxConfigScope.alignItems
*/
fun alignSelf(value: FlexAlignSelf)
/**
* Aligns this item to a specific baseline within its line, overriding the container's
* alignment.
*
* @sample androidx.compose.foundation.layout.samples.FlexAlignSelfBaselineSample
* @param alignmentLine The alignment line to use (e.g., [FirstBaseline], [LastBaseline]).
* @see AlignmentLine
*/
fun alignSelf(alignmentLine: AlignmentLine)
/**
* Aligns this item to a custom baseline computed from the measured item within its line,
* overriding the container's alignment.
*
* @sample androidx.compose.foundation.layout.samples.FlexAlignSelfCustomBaselineSample
* @param alignmentLineBlock A function that computes the baseline from a [Measured] item.
*/
fun alignSelf(alignmentLineBlock: (Measured) -> Int)
/**
* ◦ Sets the visual order of this item relative to its siblings.
*
* Items are sorted by their order value in ascending order before layout. Lower values are
* placed first, starting from the main-start edge of the container. Note that in reverse
* directions (like [FlexDirection.RowReverse]), the main-start edge is visually flipped (e.g.,
* to the right side of the container).
*
* The sorting is stable; items with the same order maintain the exact sequence in which they
* were emitted in the composition. By default, all items have an order of 0. You can use
* negative values to move items before default-ordered items, or positive values to move them
* after.
*
* @sample androidx.compose.foundation.layout.samples.FlexOrderSample
* @param value The order value. Default is 0.
*/
fun order(value: Int)
/**
* Sets the flex grow factor, determining how much of the remaining positive free space this
* item should consume relative to its siblings.
*
* When the sum of all item base sizes is less than the container's main axis size, the leftover
* space is distributed among items proportional to their growth factors. An item with a grow
* factor of 0f (the default) will not grow beyond its base size.
*
* @sample androidx.compose.foundation.layout.samples.FlexGrowSample
* @param value The growth factor. Must be non-negative. Default is 0f.
* @throws IllegalArgumentException if [value] is negative.
* @see shrink
* @see basis
*/
fun grow(@FloatRange(from = 0.0) value: Float)
/**
* ◦ Sets the flex shrink factor, determining how much this item should shrink relative to its
* siblings when there is not enough space.
*
* When the sum of all item base sizes exceeds the container's main axis size, items will shrink
* proportionally based on their shrink factor multiplied by their base size. An item with a
* shrink factor of 0f will not shrink.
*
* **Note:** Items will never shrink below their minimum intrinsic size. If the total minimum
* size of all items exceeds the container's size, the items will overflow visually on main
* axis. Use [Modifier.clipToBounds][androidx.compose.ui.draw.clipToBounds] on the container if
* you need to hide the overflow.
*
* @sample androidx.compose.foundation.layout.samples.FlexShrinkSample
* @param value The shrink factor. Must be non-negative. Default is 1f.
* @throws IllegalArgumentException if [value] is negative.
* @see grow
* @see basis
*/
fun shrink(@FloatRange(from = 0.0) value: Float)
/**
* Sets the initial main axis size of this item before any free space distribution (grow or
* shrink) is calculated.
*
* The basis determines the starting size before [grow] and [shrink] are applied:
* - [FlexBasis.Auto]: Uses the item's explicitly set size, or falls back to its natural content
* size.
* - [FlexBasis.Dp]: Uses a fixed exact size in dp.
* - [FlexBasis.Percent]: Uses a fraction of the container's main axis size.
*
* @sample androidx.compose.foundation.layout.samples.FlexBasisSample
* @param value The basis value. Default is [FlexBasis.Auto].
* @see FlexBasis
*/
fun basis(value: FlexBasis)
/**
* Sets the basis to a fixed Dp value. This is a convenience function equivalent to
* `basis(FlexBasis.Dp(value))`.
*
* @param value The basis size in Dp.
* @see FlexBasis.Dp
*/
fun basis(value: Dp)
/**
* ◦ Sets the basis to a fraction of the container's main axis size.This is a convenience
* function equivalent to `basis(FlexBasis.Percent(value))`.
*
* @param value A value between 0.0 and 1.0 representing the fraction of the container's size.
* @see FlexBasis.Percent
*/
fun basis(@FloatRange(from = 0.0, to = 1.0) value: Float)
}
@OptIn(ExperimentalFlexBoxApi::class)
internal class ResolvedFlexItemInfo : FlexConfigScope {
var baselineAlignmentLine: AlignmentLine? = null
private set
var baselineAlignmentBlock: AlignmentLineProviderBlock? = null
private set
private var _density: Density = DefaultDensity
override val density: Float
get() = _density.density
override val fontScale: Float
get() = _density.fontScale
override fun Dp.toSp(): TextUnit = with(_density) { this@toSp.toSp() }
override fun TextUnit.toDp(): Dp = with(_density) { this@toDp.toDp() }
override var flexBoxMainAxisMax: Int = 0
private set
override var flexBoxMainAxisMin: Int = 0
private set
override var flexBoxCrossAxisMax: Int = 0
private set
override var flexBoxCrossAxisMin: Int = 0
private set
override fun alignSelf(value: FlexAlignSelf) {
this.alignSelf = value
}
override fun alignSelf(alignmentLine: AlignmentLine) {
this.alignSelf = FlexAlignSelf.Baseline
this.baselineAlignmentLine = alignmentLine
this.baselineAlignmentBlock = null
}
override fun alignSelf(alignmentLineBlock: (Measured) -> Int) {
this.alignSelf = FlexAlignSelf.Baseline
this.baselineAlignmentLine = null
this.baselineAlignmentBlock = AlignmentLineProviderBlock { alignmentLineBlock(it) }
}
override fun order(value: Int) {
this.order = value
}
override fun grow(value: Float) {
requirePrecondition(value >= 0f) { "Flex grow cannot be negative: $value" }
this.grow = value
}
override fun shrink(value: Float) {
requirePrecondition(value >= 0f) { "Flex shrink cannot be negative: $value" }
this.shrink = value
}
override fun basis(value: FlexBasis) {
this.basis = value
}
internal var alignSelf: FlexAlignSelf = FlexAlignSelf.Auto
internal var order: Int = 0
internal var grow: Float = 0f
internal var shrink: Float = 1f
internal var basis: FlexBasis = FlexBasis.Auto
override fun basis(value: Dp) {
basis = FlexBasis.Dp(value)
}
override fun basis(@FloatRange(from = 0.0, to = 1.0) value: Float) {
basis = FlexBasis.Percent(value)
}
fun getBaseline(placeable: Placeable, fallback: ResolvedFlexBoxConfig): Int {
return when {
baselineAlignmentBlock != null ->
baselineAlignmentBlock!!.calculateAlignmentLinePosition(placeable)
baselineAlignmentLine != null -> {
val value = placeable[baselineAlignmentLine!!]
if (value != AlignmentLine.Unspecified) {
value
} else {
if (baselineAlignmentLine!! is VerticalAlignmentLine) {
placeable.width
} else {
placeable.height
}
}
}
alignSelf == FlexAlignSelf.Baseline -> fallback.getBaseline(placeable)
fallback.hasBaseline && alignSelf == FlexAlignSelf.Auto ->
fallback.getBaseline(placeable)
else -> AlignmentLine.Unspecified
}
}
val hasBaseline: Boolean
get() = alignSelf == FlexAlignSelf.Baseline
fun prepare(density: Density, constraints: OrientationIndependentConstraints) {
this._density = density
this.flexBoxMainAxisMax = constraints.mainAxisMax
this.flexBoxMainAxisMin = constraints.mainAxisMin
this.flexBoxCrossAxisMax = constraints.crossAxisMax
this.flexBoxCrossAxisMin = constraints.crossAxisMin
}
// Measurable and measurement state
var measurable: Measurable? = null
private var _minMainAxisSize: Int = -1
private var _maxContentSize: Int = -1
// Returns the minimum main axis size (cached after first call).
internal fun getMinMainAxisSize(isHorizontal: Boolean): Int {
if (_minMainAxisSize == -1) {
_minMainAxisSize =
if (isHorizontal) {
measurable?.minIntrinsicWidth(Constraints.Infinity) ?: 0
} else {
measurable?.minIntrinsicHeight(Constraints.Infinity) ?: 0
}
}
return _minMainAxisSize
}
// Returns the max content size (cached after first call).
internal fun getMaxContentSize(isHorizontal: Boolean): Int {
if (_maxContentSize == -1) {
_maxContentSize =
if (isHorizontal) {
measurable?.maxIntrinsicWidth(Constraints.Infinity) ?: 0
} else {
measurable?.maxIntrinsicHeight(Constraints.Infinity) ?: 0
}
}
return _maxContentSize
}
// Flex algorithm state
var flexBaseSize: Int = 0
var hypotheticalMainSize: Int = 0
var targetMainSize: Int = 0
var isFrozen: Boolean = false
// Position state
var mainPosition: Int = 0
var crossPosition: Int = 0
// Measurement results
var placeable: Placeable? = null
var baseline: Int = AlignmentLine.Unspecified
var mainAxisSize: Int = 0
var crossAxisSize: Int = 0
}
internal val DefaultDensity =
object : Density {
override val density: Float = 1f
override val fontScale: Float = 1f
}
/** Holds state for a single flex line using indices */
private class FlexLine {
var startIndex: Int = 0
var endIndex: Int = 0
var mainAxisSize: Int = 0
var crossAxisSize: Int = 0
var crossStart: Int = 0
var maxAboveBaseline: Int = 0
}
/**
* Combine two [FlexBoxConfig] objects together. Configs further "to the right" will override
* properties to the left of them, on a per-property basis.
*/
@ExperimentalFlexBoxApi
fun FlexBoxConfig(first: FlexBoxConfig, second: FlexBoxConfig): FlexBoxConfig = first then second
/**
* Combine three [FlexBoxConfig] objects together. Configs further "to the right" will override
* properties to the left of them, on a per-property basis.
*/
@ExperimentalFlexBoxApi
fun FlexBoxConfig(
first: FlexBoxConfig,
second: FlexBoxConfig,
third: FlexBoxConfig,
): FlexBoxConfig =
when {
first === FlexBoxConfig -> FlexBoxConfig(second, third)
second === FlexBoxConfig -> FlexBoxConfig(first, third)
third === FlexBoxConfig -> FlexBoxConfig(first, second)
first is CombinedFlexBoxConfig &&
second is CombinedFlexBoxConfig &&
third is CombinedFlexBoxConfig ->
FlexBoxConfig(*first.configs, *second.configs, *third.configs)
first is CombinedFlexBoxConfig && second is CombinedFlexBoxConfig ->
FlexBoxConfig(*first.configs, *second.configs, third)
first is CombinedFlexBoxConfig && third is CombinedFlexBoxConfig ->
FlexBoxConfig(*first.configs, second, *third.configs)
second is CombinedFlexBoxConfig && third is CombinedFlexBoxConfig ->
FlexBoxConfig(first, *second.configs, *third.configs)
first is CombinedFlexBoxConfig -> FlexBoxConfig(*first.configs, second, third)
second is CombinedFlexBoxConfig -> FlexBoxConfig(first, *second.configs, third)
third is CombinedFlexBoxConfig -> FlexBoxConfig(first, second, *third.configs)
else -> CombinedFlexBoxConfig(first, second, third)
}
/**
* Combine multiple [FlexBoxConfig] objects together. Configs further "to the right" will override
* properties to the left of them, on a per-property basis.
*
* @sample androidx.compose.foundation.layout.samples.FlexBoxConfigCombineSample
*/
@ExperimentalFlexBoxApi
fun FlexBoxConfig(vararg configs: FlexBoxConfig): FlexBoxConfig =
if (configs.isEmpty()) {
FlexBoxConfig
} else if (configs.any { it === FlexBoxConfig }) {
val count = configs.count { it !== FlexBoxConfig }
when (count) {
0 -> FlexBoxConfig
1 -> configs.first { it !== FlexBoxConfig }
else -> {
val filtered = arrayOfNulls<FlexBoxConfig>(count)
var cursor = 0
configs.forEach { config ->
if (config !== FlexBoxConfig) {
filtered[cursor++] = config
}
}
@Suppress("UNCHECKED_CAST")
CombinedFlexBoxConfig(*(filtered as Array<FlexBoxConfig>))
}
}
} else {
CombinedFlexBoxConfig(*configs)
}
/**
* Internal representation for a composition of two or more [FlexBoxConfig] objects.
*
* This class holds a **flat** array of configs. The [FlexBoxConfig] factory functions ensure that
* [CombinedFlexBoxConfig] instances are never nested — if a factory receives a
* [CombinedFlexBoxConfig] as input, its [configs] array is spread into the new result. This
* guarantees that [configure] is always a single-pass flat iteration regardless of how many
* composition steps produced this instance.
*
* @property configs the flattened array of configs to apply in order. Later entries override
* earlier entries on a per-property basis.
*/
@ExperimentalFlexBoxApi
internal class CombinedFlexBoxConfig(vararg val configs: FlexBoxConfig) : FlexBoxConfig {
override fun FlexBoxConfigScope.configure() {
configs.forEach { config -> with(config) { configure() } }
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is CombinedFlexBoxConfig) return false
return configs.contentEquals(other.configs)
}
override fun hashCode(): Int = configs.contentHashCode()
}
/**
* Combine two [FlexConfig] objects together. Configs further "to the right" will override
* properties to the left of them, on a per-property basis.
*/
@ExperimentalFlexBoxApi
fun FlexConfig(first: FlexConfig, second: FlexConfig): FlexConfig = first then second
/**
* Combine three [FlexConfig] objects together. Configs further "to the right" will override
* properties to the left of them, on a per-property basis.
*/
@ExperimentalFlexBoxApi
fun FlexConfig(first: FlexConfig, second: FlexConfig, third: FlexConfig): FlexConfig =
when {
first === FlexConfig -> FlexConfig(second, third)
second === FlexConfig -> FlexConfig(first, third)
third === FlexConfig -> FlexConfig(first, second)
first is CombinedFlexConfig &&
second is CombinedFlexConfig &&
third is CombinedFlexConfig ->
FlexConfig(*first.configs, *second.configs, *third.configs)
first is CombinedFlexConfig && second is CombinedFlexConfig ->
FlexConfig(*first.configs, *second.configs, third)
first is CombinedFlexConfig && third is CombinedFlexConfig ->
FlexConfig(*first.configs, second, *third.configs)
second is CombinedFlexConfig && third is CombinedFlexConfig ->
FlexConfig(first, *second.configs, *third.configs)
first is CombinedFlexConfig -> FlexConfig(*first.configs, second, third)
second is CombinedFlexConfig -> FlexConfig(first, *second.configs, third)
third is CombinedFlexConfig -> FlexConfig(first, second, *third.configs)
else -> CombinedFlexConfig(first, second, third)
}
/**
* Combine multiple [FlexConfig] objects together. Configs further "to the right" will override
* properties to the left of them, on a per-property basis.
*
* @sample androidx.compose.foundation.layout.samples.FlexConfigCombineSample
*/
@ExperimentalFlexBoxApi
fun FlexConfig(vararg configs: FlexConfig): FlexConfig =
if (configs.isEmpty()) {
FlexConfig
} else if (configs.any { it === FlexConfig }) {
val count = configs.count { it !== FlexConfig }
when (count) {
0 -> FlexConfig
1 -> configs.first { it !== FlexConfig }
else -> {
val filtered = arrayOfNulls<FlexConfig>(count)
var cursor = 0
configs.forEach { config ->
if (config !== FlexConfig) {
filtered[cursor++] = config
}
}
@Suppress("UNCHECKED_CAST") CombinedFlexConfig(*(filtered as Array<FlexConfig>))
}
}
} else {
CombinedFlexConfig(*configs)
}
@OptIn(ExperimentalFlexBoxApi::class)
internal class CombinedFlexConfig(vararg val configs: FlexConfig) : FlexConfig {
override fun FlexConfigScope.configure() {
configs.forEach { config -> with(config) { configure() } }
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is CombinedFlexConfig) return false
return configs.contentEquals(other.configs)
}
override fun hashCode(): Int = configs.contentHashCode()
}
/**
* Iterates through a specific range of the [ArrayList] from [fromIndex] to [toIndex] (Exclusive)
* and calls [action] for each item.
*
* This avoids creating a subList wrapper or an iterator.
*/
@Suppress("BanInlineOptIn")
@OptIn(ExperimentalContracts::class)
private inline fun <T> ArrayList<T>.fastForEachUntil(
fromIndex: Int,
toIndex: Int,
action: (T) -> Unit,
) {
contract { callsInPlace(action) }
if (fromIndex !in 0..size) {
throw IndexOutOfBoundsException("fromIndex ($fromIndex) is out of bounds [0, $size]")
}
if (toIndex !in 0..size) {
throw IndexOutOfBoundsException("toIndex ($toIndex) is out of bounds [0, $size]")
}
var index = fromIndex
while (index < toIndex) {
action(get(index))
index++
}
}
@Suppress("BanInlineOptIn")
@OptIn(ExperimentalContracts::class)
private inline fun <T> ArrayList<T>.fastSumBy(
fromIndex: Int,
toIndex: Int,
selector: (T) -> Int,
): Int {
contract { callsInPlace(selector) }
var sum = 0
fastForEachUntil(fromIndex, toIndex) { sum += selector(it) }
return sum
}