[go: nahoru, domu]

blob: a828f9f9cac96e4cff2e641df5d482cc12316436 [file] [log] [blame]
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.foundation.layout
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
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 kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sign
/**
* This is a data class that holds the determined width, height of a row,
* and information on how to retrieve main axis and cross axis positions.
*/
internal class RowColumnMeasureHelperResult(
val crossAxisSize: Int,
val mainAxisSize: Int,
val startIndex: Int,
val endIndex: Int,
val beforeCrossAxisAlignmentLine: Int,
val mainAxisPositions: IntArray,
)
/**
* RowColumnMeasurementHelper
* Measures the row and column without placing, useful for reusing row/column logic
*/
internal class RowColumnMeasurementHelper(
val orientation: LayoutOrientation,
val arrangement: (Int, IntArray, LayoutDirection, Density, IntArray) -> Unit,
val arrangementSpacing: Dp,
val crossAxisSize: SizeMode,
val crossAxisAlignment: CrossAxisAlignment,
val measurables: List<Measurable>,
val placeables: Array<Placeable?>
) {
private val rowColumnParentData = Array(measurables.size) {
measurables[it].rowColumnParentData
}
fun Placeable.mainAxisSize() =
if (orientation == LayoutOrientation.Horizontal) width else height
fun Placeable.crossAxisSize() =
if (orientation == LayoutOrientation.Horizontal) height else width
/**
* Measures the row and column without placing, useful for reusing row/column logic
*
* @param measureScope The measure scope to retrieve density
* @param constraints The desired constraints for the startIndex and endIndex
* can hold null items if not measured.
* @param startIndex The startIndex (inclusive) when examining measurables, placeable
* and parentData
* @param endIndex The ending index (exclusive) when examinning measurable, placeable
* and parentData
*/
fun measureWithoutPlacing(
measureScope: MeasureScope,
constraints: Constraints,
startIndex: Int,
endIndex: Int
): RowColumnMeasureHelperResult {
@Suppress("NAME_SHADOWING")
val constraints = OrientationIndependentConstraints(constraints, orientation)
val arrangementSpacingPx = with(measureScope) {
arrangementSpacing.roundToPx()
}
var totalWeight = 0f
var fixedSpace = 0
var crossAxisSpace = 0
var weightChildrenCount = 0
var anyAlignBy = false
val subSize = endIndex - startIndex
// First measure children with zero weight.
var spaceAfterLastNoWeight = 0
for (i in startIndex until endIndex) {
val child = measurables[i]
val parentData = rowColumnParentData[i]
val weight = parentData.weight
if (weight > 0f) {
totalWeight += weight
++weightChildrenCount
} else {
val mainAxisMax = constraints.mainAxisMax
val placeable = placeables[i] ?: child.measure(
// Ask for preferred main axis size.
constraints.copy(
mainAxisMin = 0,
mainAxisMax = if (mainAxisMax == Constraints.Infinity) {
Constraints.Infinity
} else {
mainAxisMax - fixedSpace
},
crossAxisMin = 0
).toBoxConstraints(orientation)
)
spaceAfterLastNoWeight = min(
arrangementSpacingPx,
mainAxisMax - fixedSpace - placeable.mainAxisSize()
)
fixedSpace += placeable.mainAxisSize() + spaceAfterLastNoWeight
crossAxisSpace = max(crossAxisSpace, placeable.crossAxisSize())
anyAlignBy = anyAlignBy || parentData.isRelative
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 (totalWeight > 0f && constraints.mainAxisMax != Constraints.Infinity) {
constraints.mainAxisMax
} else {
constraints.mainAxisMin
}
val remainingToTarget =
targetSpace - fixedSpace - arrangementSpacingPx * (weightChildrenCount - 1)
val weightUnitSpace = if (totalWeight > 0) remainingToTarget / totalWeight else 0f
var remainder = remainingToTarget - (startIndex until endIndex).sumOf {
(weightUnitSpace * rowColumnParentData[it].weight).roundToInt()
}
for (i in startIndex until endIndex) {
if (placeables[i] == null) {
val child = measurables[i]
val parentData = rowColumnParentData[i]
val weight = parentData.weight
check(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 childMainAxisSize = max(
0,
(weightUnitSpace * weight).roundToInt() + remainderUnit
)
val placeable = child.measure(
OrientationIndependentConstraints(
if (parentData.fill && childMainAxisSize != Constraints.Infinity) {
childMainAxisSize
} else {
0
},
childMainAxisSize,
0,
constraints.crossAxisMax
).toBoxConstraints(orientation)
)
weightedSpace += placeable.mainAxisSize()
crossAxisSpace = max(crossAxisSpace, placeable.crossAxisSize())
anyAlignBy = anyAlignBy || parentData.isRelative
placeables[i] = placeable
}
}
weightedSpace = (weightedSpace + arrangementSpacingPx * (weightChildrenCount - 1))
.coerceAtMost(constraints.mainAxisMax - fixedSpace)
}
var beforeCrossAxisAlignmentLine = 0
var afterCrossAxisAlignmentLine = 0
if (anyAlignBy) {
for (i in startIndex until endIndex) {
val placeable = placeables[i]!!
val parentData = rowColumnParentData[i]
val alignmentLinePosition = parentData.crossAxisAlignment
?.calculateAlignmentLinePosition(placeable)
if (alignmentLinePosition != null) {
beforeCrossAxisAlignmentLine = max(
beforeCrossAxisAlignmentLine,
alignmentLinePosition.let {
if (it != AlignmentLine.Unspecified) it else 0
}
)
afterCrossAxisAlignmentLine = max(
afterCrossAxisAlignmentLine,
placeable.crossAxisSize() -
(
alignmentLinePosition.let {
if (it != AlignmentLine.Unspecified) {
it
} else {
placeable.crossAxisSize()
}
}
)
)
}
}
}
// Compute the Row or Column size and position the children.
val mainAxisLayoutSize = max(fixedSpace + weightedSpace, constraints.mainAxisMin)
val crossAxisLayoutSize = if (constraints.crossAxisMax != Constraints.Infinity &&
crossAxisSize == SizeMode.Expand
) {
constraints.crossAxisMax
} else {
max(
crossAxisSpace,
max(
constraints.crossAxisMin,
beforeCrossAxisAlignmentLine + afterCrossAxisAlignmentLine
)
)
}
val mainAxisPositions = IntArray(subSize) { 0 }
val childrenMainAxisSize = IntArray(subSize) { index ->
placeables[index + startIndex]!!.mainAxisSize()
}
return RowColumnMeasureHelperResult(
mainAxisSize = mainAxisLayoutSize,
crossAxisSize = crossAxisLayoutSize,
startIndex = startIndex,
endIndex = endIndex,
beforeCrossAxisAlignmentLine = beforeCrossAxisAlignmentLine,
mainAxisPositions = mainAxisPositions(
mainAxisLayoutSize,
childrenMainAxisSize,
mainAxisPositions,
measureScope
))
}
private fun mainAxisPositions(
mainAxisLayoutSize: Int,
childrenMainAxisSize: IntArray,
mainAxisPositions: IntArray,
measureScope: MeasureScope
): IntArray {
arrangement(
mainAxisLayoutSize,
childrenMainAxisSize,
measureScope.layoutDirection,
measureScope,
mainAxisPositions
)
return mainAxisPositions
}
private fun getCrossAxisPosition(
placeable: Placeable,
parentData: RowColumnParentData?,
crossAxisLayoutSize: Int,
layoutDirection: LayoutDirection,
beforeCrossAxisAlignmentLine: Int
): Int {
val childCrossAlignment = parentData?.crossAxisAlignment ?: crossAxisAlignment
return childCrossAlignment.align(
size = crossAxisLayoutSize - placeable.crossAxisSize(),
layoutDirection = if (orientation == LayoutOrientation.Horizontal) {
LayoutDirection.Ltr
} else {
layoutDirection
},
placeable = placeable,
beforeCrossAxisAlignmentLine = beforeCrossAxisAlignmentLine
)
}
fun placeHelper(
placeableScope: Placeable.PlacementScope,
measureResult: RowColumnMeasureHelperResult,
crossAxisOffset: Int,
layoutDirection: LayoutDirection,
) {
with(placeableScope) {
for (i in measureResult.startIndex until measureResult.endIndex) {
val placeable = placeables[i]
placeable!!
val mainAxisPositions = measureResult.mainAxisPositions
val crossAxisPosition = getCrossAxisPosition(
placeable,
(measurables[i].parentData as? RowColumnParentData),
measureResult.crossAxisSize,
layoutDirection,
measureResult.beforeCrossAxisAlignmentLine
) + crossAxisOffset
if (orientation == LayoutOrientation.Horizontal) {
placeable.place(
mainAxisPositions[i - measureResult.startIndex],
crossAxisPosition
)
} else {
placeable.place(
crossAxisPosition,
mainAxisPositions[i - measureResult.startIndex]
)
}
}
}
}
}