blob: fc3190dbc468134264b0cb770e7feeb2a5653339 [file]
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.foundation.layout
import androidx.compose.foundation.layout.internal.checkPrecondition
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.util.fastCoerceAtLeast
import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.fastRoundToInt
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sign
internal interface RowColumnMeasurePolicy {
fun Placeable.mainAxisSize(): Int
fun Placeable.crossAxisSize(): Int
fun populateMainAxisPositions(
mainAxisLayoutSize: Int,
childrenMainAxisSize: IntArray,
mainAxisPositions: IntArray,
measureScope: MeasureScope,
)
fun placeHelper(
placeables: Array<Placeable?>,
measureScope: MeasureScope,
beforeCrossAxisAlignmentLine: Int,
mainAxisPositions: IntArray,
mainAxisLayoutSize: Int,
crossAxisLayoutSize: Int,
crossAxisOffset: IntArray?,
currentLineIndex: Int,
startIndex: Int,
endIndex: Int,
): MeasureResult
fun createConstraints(
mainAxisMin: Int,
crossAxisMin: Int,
mainAxisMax: Int,
crossAxisMax: Int,
isPrioritizing: Boolean = false,
): Constraints
}
/**
* Measures the row and column
*
* @param measureScope The measure scope to retrieve density
* @param startIndex The startIndex (inclusive) when examining measurables, placeable and parentData
* @param endIndex The ending index (exclusive) when examining measurable, placeable and parentData
* @param crossAxisOffset The offset to apply to the cross axis when placing
* @param currentLineIndex The index of the current line if in a multi-row/column setting like
* [FlowRow]
*/
internal fun RowColumnMeasurePolicy.measure(
mainAxisMin: Int,
crossAxisMin: Int,
mainAxisMax: Int,
crossAxisMax: Int,
arrangementSpacingInt: Int,
measureScope: MeasureScope,
measurables: List<Measurable>,
placeables: Array<Placeable?>,
startIndex: Int,
endIndex: Int,
crossAxisOffset: IntArray? = null,
currentLineIndex: Int = 0,
): MeasureResult {
val arrangementSpacingPx = arrangementSpacingInt.toLong()
var totalWeight = 0f
var fixedSpace = 0
var crossAxisSpace = 0
var weightChildrenCount = 0
var anyAlignBy = false
val subSize = endIndex - startIndex
val childrenMainAxisSize = IntArray(subSize)
var beforeCrossAxisAlignmentLine = 0
var afterCrossAxisAlignmentLine = 0
// First measure children with zero weight.
var spaceAfterLastNoWeight = 0
for (i in startIndex until endIndex) {
val child = measurables[i]
val parentData = child.rowColumnParentData
val weight = parentData.weight
anyAlignBy = anyAlignBy || parentData.isRelative
if (weight > 0f) {
totalWeight += weight
++weightChildrenCount
} else {
val crossAxisDesiredSize =
if (crossAxisMax == Constraints.Infinity) null
else
parentData?.flowLayoutData?.let {
(it.fillCrossAxisFraction * crossAxisMax).fastRoundToInt()
}
val remaining = mainAxisMax - fixedSpace
val placeable =
placeables[i]
?: child.measure(
// Ask for preferred main axis size.
createConstraints(
mainAxisMin = 0,
crossAxisMin = crossAxisDesiredSize ?: 0,
mainAxisMax =
if (mainAxisMax == Constraints.Infinity) {
Constraints.Infinity
} else {
remaining.fastCoerceAtLeast(0)
},
crossAxisMax = crossAxisDesiredSize ?: crossAxisMax,
)
)
val placeableMainAxisSize = placeable.mainAxisSize()
val placeableCrossAxisSize = placeable.crossAxisSize()
childrenMainAxisSize[i - startIndex] = placeableMainAxisSize
spaceAfterLastNoWeight =
min(arrangementSpacingInt, (remaining - placeableMainAxisSize).fastCoerceAtLeast(0))
fixedSpace += placeableMainAxisSize + spaceAfterLastNoWeight
crossAxisSpace = max(crossAxisSpace, placeableCrossAxisSize)
placeables[i] = placeable
}
}
var weightedSpace = 0
if (weightChildrenCount == 0) {
// fixedSpace contains an extra spacing after the last non-weight child.
fixedSpace -= spaceAfterLastNoWeight
} else {
// Measure the rest according to their weights in the remaining main axis space.
val targetSpace =
if (mainAxisMax != Constraints.Infinity) {
mainAxisMax
} else {
mainAxisMin
}
val arrangementSpacingTotal = arrangementSpacingPx * (weightChildrenCount - 1)
val remainingToTarget =
(targetSpace - fixedSpace - arrangementSpacingTotal).fastCoerceAtLeast(0)
val weightUnitSpace = remainingToTarget / totalWeight
var remainder = remainingToTarget
for (i in startIndex until endIndex) {
val measurable = measurables[i]
val itemWeight = measurable.rowColumnParentData.weight
val weightedSize = (weightUnitSpace * itemWeight)
remainder -= weightedSize.fastRoundToInt()
}
for (i in startIndex until endIndex) {
if (placeables[i] == null) {
val child = measurables[i]
val parentData = child.rowColumnParentData
val weight = parentData.weight
val crossAxisDesiredSize =
if (crossAxisMax == Constraints.Infinity) null
else
parentData?.flowLayoutData?.let {
(it.fillCrossAxisFraction * crossAxisMax).fastRoundToInt()
}
checkPrecondition(weight > 0) { "All weights <= 0 should have placeables" }
// After the weightUnitSpace rounding, the total space going to be occupied
// can be smaller or larger than remainingToTarget. Here we distribute the
// loss or gain remainder evenly to the first children.
val remainderUnit = remainder.sign
remainder -= remainderUnit
val weightedSize = (weightUnitSpace * weight)
val childMainAxisSize = max(0, weightedSize.fastRoundToInt() + remainderUnit)
val childConstraints: Constraints =
createConstraints(
mainAxisMin =
if (parentData.fill && childMainAxisSize != Constraints.Infinity) {
childMainAxisSize
} else {
0
},
crossAxisMin = crossAxisDesiredSize ?: 0,
mainAxisMax = childMainAxisSize,
crossAxisMax = crossAxisDesiredSize ?: crossAxisMax,
isPrioritizing = true,
)
val placeable = child.measure(childConstraints)
val placeableMainAxisSize = placeable.mainAxisSize()
val placeableCrossAxisSize = placeable.crossAxisSize()
childrenMainAxisSize[i - startIndex] = placeableMainAxisSize
weightedSpace += placeableMainAxisSize
crossAxisSpace = max(crossAxisSpace, placeableCrossAxisSize)
placeables[i] = placeable
}
}
weightedSpace =
(weightedSpace + arrangementSpacingTotal)
.toInt()
.fastCoerceIn(0, mainAxisMax - fixedSpace)
}
// we've done this check in weights as to avoid going through another loop
if (anyAlignBy) {
for (i in startIndex until endIndex) {
val placeable = placeables[i]
val parentData = placeable!!.rowColumnParentData
val alignmentLinePosition =
parentData.crossAxisAlignment?.calculateAlignmentLinePosition(placeable)
alignmentLinePosition?.let {
val placeableCrossAxisSize = placeable.crossAxisSize()
beforeCrossAxisAlignmentLine =
max(
beforeCrossAxisAlignmentLine,
if (it != AlignmentLine.Unspecified) alignmentLinePosition else 0,
)
afterCrossAxisAlignmentLine =
max(
afterCrossAxisAlignmentLine,
placeableCrossAxisSize -
if (it != AlignmentLine.Unspecified) {
it
} else {
placeableCrossAxisSize
},
)
}
}
}
// Compute the Row or Column size and position the children.
val mainAxisLayoutSize = max((fixedSpace + weightedSpace).fastCoerceAtLeast(0), mainAxisMin)
val crossAxisLayoutSize =
maxOf(
crossAxisSpace,
crossAxisMin,
beforeCrossAxisAlignmentLine + afterCrossAxisAlignmentLine,
)
val mainAxisPositions = IntArray(subSize)
populateMainAxisPositions(
mainAxisLayoutSize,
childrenMainAxisSize,
mainAxisPositions,
measureScope,
)
return placeHelper(
placeables,
measureScope,
beforeCrossAxisAlignmentLine,
mainAxisPositions,
mainAxisLayoutSize,
crossAxisLayoutSize,
crossAxisOffset,
currentLineIndex,
startIndex,
endIndex,
)
}