Rewrite Row and Column (#251)

Added Arrangement to Row and Column.
Changed the implementation of Row and Column to one common with Constraints support.
Modifier.weight has been added to RowScope and ColumnScope.
This commit is contained in:
EpicDima
2023-11-19 06:44:51 +03:00
committed by GitHub
parent d491a59894
commit e49763c074
6 changed files with 1489 additions and 103 deletions

View File

@ -0,0 +1,581 @@
package com.jakewharton.mosaic.ui
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import kotlin.math.min
import kotlin.math.roundToInt
/**
* Used to specify the arrangement of the layout's children in layouts like [Row] or [Column] in
* the main axis direction (horizontal and vertical, respectively).
*
* Below is an illustration of different horizontal arrangements in [Row]s:
* ![Row arrangements](https://developer.android.com/images/reference/androidx/compose/foundation/layout/row_arrangement_visualization.gif)
*
* Different vertical arrangements in [Column]s:
* ![Column arrangements](https://developer.android.com/images/reference/androidx/compose/foundation/layout/column_arrangement_visualization.gif)
*/
@Immutable
public object Arrangement {
/**
* Used to specify the horizontal arrangement of the layout's children in layouts like [Row].
*/
@Stable
public interface Horizontal {
/**
* Spacing that should be added between any two adjacent layout children.
*/
public val spacing: Int get() = 0
/**
* Horizontally places the layout children.
*
* @param totalSize Available space that can be occupied by the children, in pixels.
* @param sizes An array of sizes of all children, in pixels.
* @param outPositions An array of the size of [sizes] that returns the calculated
* positions relative to the left, in pixels.
*/
public fun arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
)
}
/**
* Used to specify the vertical arrangement of the layout's children in layouts like [Column].
*/
@Stable
public interface Vertical {
/**
* Spacing that should be added between any two adjacent layout children.
*/
public val spacing: Int get() = 0
/**
* Vertically places the layout children.
*
* @param totalSize Available space that can be occupied by the children, in pixels.
* @param sizes An array of sizes of all children, in pixels.
* @param outPositions An array of the size of [sizes] that returns the calculated
* positions relative to the top, in pixels.
*/
public fun arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
)
}
/**
* Used to specify the horizontal arrangement of the layout's children in horizontal layouts
* like [Row], or the vertical arrangement of the layout's children in vertical layouts like
* [Column].
*/
@Stable
public interface HorizontalOrVertical : Horizontal, Vertical {
/**
* Spacing that should be added between any two adjacent layout children.
*/
override val spacing: Int get() = 0
}
/**
* Place children horizontally such that they are as close as possible to the beginning of the
* horizontal axis (left).
* Visually: 123####.
*/
@Stable
public val Start: Horizontal = object : Horizontal {
override fun arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
) = placeLeftOrTop(sizes, outPositions, reverseInput = false)
override fun toString() = "Arrangement#Start"
}
/**
* Place children horizontally such that they are as close as possible to the end of the main
* axis.
* Visually: ####123.
*/
@Stable
public val End: Horizontal = object : Horizontal {
override fun arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
) = placeRightOrBottom(totalSize, sizes, outPositions, reverseInput = false)
override fun toString() = "Arrangement#End"
}
/**
* Place children vertically such that they are as close as possible to the top of the main
* axis.
* Visually: (top) 123#### (bottom)
*/
@Stable
public val Top: Vertical = object : Vertical {
override fun arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
) = placeLeftOrTop(sizes, outPositions, reverseInput = false)
override fun toString() = "Arrangement#Top"
}
/**
* Place children vertically such that they are as close as possible to the bottom of the main
* axis.
* Visually: (top) ####123 (bottom)
*/
@Stable
public val Bottom: Vertical = object : Vertical {
override fun arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
) = placeRightOrBottom(totalSize, sizes, outPositions, reverseInput = false)
override fun toString() = "Arrangement#Bottom"
}
/**
* Place children such that they are as close as possible to the middle of the main axis.
* Visually: ##123## for LTR.
*/
@Stable
public val Center: HorizontalOrVertical = object : HorizontalOrVertical {
override val spacing = 0
override fun arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
) = placeCenter(totalSize, sizes, outPositions, reverseInput = false)
override fun toString() = "Arrangement#Center"
}
/**
* Place children such that they are spaced evenly across the main axis, including free
* space before the first child and after the last child.
* Visually: #1#2#3# for LTR.
*/
@Stable
public val SpaceEvenly: HorizontalOrVertical = object : HorizontalOrVertical {
override val spacing = 0
override fun arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
) = placeSpaceEvenly(totalSize, sizes, outPositions, reverseInput = false)
override fun toString() = "Arrangement#SpaceEvenly"
}
/**
* Place children such that they are spaced evenly across the main axis, without free
* space before the first child or after the last child.
* Visually: 1##2##3 for LTR.
*/
@Stable
public val SpaceBetween: HorizontalOrVertical = object : HorizontalOrVertical {
override val spacing = 0
override fun arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
) = placeSpaceBetween(totalSize, sizes, outPositions, reverseInput = false)
override fun toString() = "Arrangement#SpaceBetween"
}
/**
* Place children such that they are spaced evenly across the main axis, including free
* space before the first child and after the last child, but half the amount of space
* existing otherwise between two consecutive children.
* Visually: #1##2##3# for LTR.
*/
@Stable
public val SpaceAround: HorizontalOrVertical = object : HorizontalOrVertical {
override val spacing = 0
override fun arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
) = placeSpaceAround(totalSize, sizes, outPositions, reverseInput = false)
override fun toString() = "Arrangement#SpaceAround"
}
/**
* Place children such that each two adjacent ones are spaced by a fixed [space] distance across
* the main axis. The spacing will be subtracted from the available space that the children
* can occupy. The [space] can be negative, in which case children will overlap.
*
* To change alignment of the spaced children horizontally or vertically, use [spacedBy]
* overloads with `alignment` parameter.
*
* @param space The space between adjacent children.
*/
@Stable
public fun spacedBy(space: Int): HorizontalOrVertical =
SpacedAligned(space, true) { size -> Alignment.Start.align(0, size) }
/**
* Place children horizontally such that each two adjacent ones are spaced by a fixed [space]
* distance. The spacing will be subtracted from the available width that the children
* can occupy. An [alignment] can be specified to align the spaced children horizontally
* inside the parent, in case there is empty width remaining. The [space] can be negative,
* in which case children will overlap.
*
* @param space The space between adjacent children.
* @param alignment The alignment of the spaced children inside the parent.
*/
@Stable
public fun spacedBy(space: Int, alignment: Alignment.Horizontal): Horizontal =
SpacedAligned(space, true) { size -> alignment.align(0, size) }
/**
* Place children vertically such that each two adjacent ones are spaced by a fixed [space]
* distance. The spacing will be subtracted from the available height that the children
* can occupy. An [alignment] can be specified to align the spaced children vertically
* inside the parent, in case there is empty height remaining. The [space] can be negative,
* in which case children will overlap.
*
* @param space The space between adjacent children.
* @param alignment The alignment of the spaced children inside the parent.
*/
@Stable
public fun spacedBy(space: Int, alignment: Alignment.Vertical): Vertical =
SpacedAligned(space, false) { size -> alignment.align(0, size) }
/**
* Place children horizontally one next to the other and align the obtained group
* according to an [alignment].
*
* @param alignment The alignment of the children inside the parent.
*/
@Stable
public fun aligned(alignment: Alignment.Horizontal): Horizontal =
SpacedAligned(0, true) { size -> alignment.align(0, size) }
/**
* Place children vertically one next to the other and align the obtained group
* according to an [alignment].
*
* @param alignment The alignment of the children inside the parent.
*/
@Stable
public fun aligned(alignment: Alignment.Vertical): Vertical =
SpacedAligned(0, false) { size -> alignment.align(0, size) }
@Immutable
public object Absolute {
/**
* Place children horizontally such that they are as close as possible to the left edge of
* the [Row].
*
* Unlike [Arrangement.Start], when the layout direction is RTL, the children will not be
* mirrored and as such children will appear in the order they are composed inside the [Row].
*
* Visually: 123####
*/
@Stable
public val Left: Horizontal = object : Horizontal {
override fun arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
) = placeLeftOrTop(sizes, outPositions, reverseInput = false)
override fun toString() = "AbsoluteArrangement#Left"
}
/**
* Place children such that they are as close as possible to the middle of the [Row].
*
* Visually: ##123##
*/
@Stable
public val Center: Horizontal = object : Horizontal {
override fun arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
) = placeCenter(totalSize, sizes, outPositions, reverseInput = false)
override fun toString() = "AbsoluteArrangement#Center"
}
/**
* Place children horizontally such that they are as close as possible to the right edge of
* the [Row].
*
* Visually: ####123
*/
@Stable
public val Right: Horizontal = object : Horizontal {
override fun arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
) = placeRightOrBottom(totalSize, sizes, outPositions, reverseInput = false)
override fun toString() = "AbsoluteArrangement#Right"
}
/**
* Place children such that they are spaced evenly across the main axis, without free
* space before the first child or after the last child.
*
* Visually: 1##2##3
*/
@Stable
public val SpaceBetween: Horizontal = object : Horizontal {
override fun arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
) = placeSpaceBetween(totalSize, sizes, outPositions, reverseInput = false)
override fun toString() = "AbsoluteArrangement#SpaceBetween"
}
/**
* Place children such that they are spaced evenly across the main axis, including free
* space before the first child and after the last child.
*
* Visually: #1#2#3#
*/
@Stable
public val SpaceEvenly: Horizontal = object : Horizontal {
override fun arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
) = placeSpaceEvenly(totalSize, sizes, outPositions, reverseInput = false)
override fun toString() = "AbsoluteArrangement#SpaceEvenly"
}
/**
* Place children such that they are spaced evenly horizontally, including free
* space before the first child and after the last child, but half the amount of space
* existing otherwise between two consecutive children.
*
* Visually: #1##2##3##4#
*/
@Stable
public val SpaceAround: Horizontal = object : Horizontal {
override fun arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
) = placeSpaceAround(totalSize, sizes, outPositions, reverseInput = false)
override fun toString() = "AbsoluteArrangement#SpaceAround"
}
/**
* Place children such that each two adjacent ones are spaced by a fixed [space] distance across
* the main axis. The spacing will be subtracted from the available space that the children
* can occupy.
*
* @param space The space between adjacent children.
*/
@Stable
public fun spacedBy(space: Int): HorizontalOrVertical =
SpacedAligned(space, false, null)
/**
* Place children horizontally such that each two adjacent ones are spaced by a fixed [space]
* distance. The spacing will be subtracted from the available width that the children
* can occupy. An [alignment] can be specified to align the spaced children horizontally
* inside the parent, in case there is empty width remaining.
*
* @param space The space between adjacent children.
* @param alignment The alignment of the spaced children inside the parent.
*/
@Stable
public fun spacedBy(space: Int, alignment: Alignment.Horizontal): Horizontal =
SpacedAligned(space, false) { size -> alignment.align(0, size) }
/**
* Place children vertically such that each two adjacent ones are spaced by a fixed [space]
* distance. The spacing will be subtracted from the available height that the children
* can occupy. An [alignment] can be specified to align the spaced children vertically
* inside the parent, in case there is empty height remaining.
*
* @param space The space between adjacent children.
* @param alignment The alignment of the spaced children inside the parent.
*/
@Stable
public fun spacedBy(space: Int, alignment: Alignment.Vertical): Vertical =
SpacedAligned(space, false) { size -> alignment.align(0, size) }
/**
* Place children horizontally one next to the other and align the obtained group
* according to an [alignment].
*
* @param alignment The alignment of the children inside the parent.
*/
@Stable
public fun aligned(alignment: Alignment.Horizontal): Horizontal =
SpacedAligned(0, false) { size -> alignment.align(0, size) }
}
/**
* Arrangement with spacing between adjacent children and alignment for the spaced group.
* Should not be instantiated directly, use [spacedBy] instead.
*/
@Immutable
internal data class SpacedAligned(
val space: Int,
val rtlMirror: Boolean,
val alignment: ((Int) -> Int)?,
) : HorizontalOrVertical {
override val spacing = space
override fun arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
) {
if (sizes.isEmpty()) return
var occupied = 0
var lastSpace = 0
val reversed = rtlMirror
sizes.forEachIndexed(reversed) { index, it ->
outPositions[index] = min(occupied, totalSize - it)
lastSpace = min(space, totalSize - outPositions[index] - it)
occupied = outPositions[index] + it + lastSpace
}
occupied -= lastSpace
if (alignment != null && occupied < totalSize) {
val groupPosition = alignment.invoke(totalSize - occupied)
for (index in outPositions.indices) {
outPositions[index] += groupPosition
}
}
}
override fun toString() =
"${if (rtlMirror) "" else "Absolute"}Arrangement#spacedAligned($space, $alignment)"
}
internal fun placeRightOrBottom(
totalSize: Int,
size: IntArray,
outPosition: IntArray,
reverseInput: Boolean,
) {
val consumedSize = size.fold(0) { a, b -> a + b }
var current = totalSize - consumedSize
size.forEachIndexed(reverseInput) { index, it ->
outPosition[index] = current
current += it
}
}
internal fun placeLeftOrTop(size: IntArray, outPosition: IntArray, reverseInput: Boolean) {
var current = 0
size.forEachIndexed(reverseInput) { index, it ->
outPosition[index] = current
current += it
}
}
internal fun placeCenter(
totalSize: Int,
size: IntArray,
outPosition: IntArray,
reverseInput: Boolean,
) {
val consumedSize = size.fold(0) { a, b -> a + b }
var current = (totalSize - consumedSize).toFloat() / 2
size.forEachIndexed(reverseInput) { index, it ->
outPosition[index] = current.roundToInt()
current += it.toFloat()
}
}
internal fun placeSpaceEvenly(
totalSize: Int,
size: IntArray,
outPosition: IntArray,
reverseInput: Boolean,
) {
val consumedSize = size.fold(0) { a, b -> a + b }
val gapSize = (totalSize - consumedSize).toFloat() / (size.size + 1)
var current = gapSize
size.forEachIndexed(reverseInput) { index, it ->
outPosition[index] = current.roundToInt()
current += it.toFloat() + gapSize
}
}
internal fun placeSpaceBetween(
totalSize: Int,
size: IntArray,
outPosition: IntArray,
reverseInput: Boolean,
) {
if (size.isEmpty()) return
val consumedSize = size.fold(0) { a, b -> a + b }
val noOfGaps = maxOf(size.lastIndex, 1)
val gapSize = (totalSize - consumedSize).toFloat() / noOfGaps
var current = 0f
if (reverseInput && size.size == 1) {
// If the layout direction is right-to-left and there is only one gap,
// we start current with the gap size. That forces the single item to be right-aligned.
current = gapSize
}
size.forEachIndexed(reverseInput) { index, it ->
outPosition[index] = current.roundToInt()
current += it.toFloat() + gapSize
}
}
internal fun placeSpaceAround(
totalSize: Int,
size: IntArray,
outPosition: IntArray,
reverseInput: Boolean,
) {
val consumedSize = size.fold(0) { a, b -> a + b }
val gapSize = if (size.isNotEmpty()) {
(totalSize - consumedSize).toFloat() / size.size
} else {
0f
}
var current = gapSize / 2
size.forEachIndexed(reverseInput) { index, it ->
outPosition[index] = current.roundToInt()
current += it.toFloat() + gapSize
}
}
private inline fun IntArray.forEachIndexed(reversed: Boolean, action: (Int, Int) -> Unit) {
if (!reversed) {
forEachIndexed(action)
} else {
for (i in (size - 1) downTo 0) {
action(i, get(i))
}
}
}
}

View File

@ -5,55 +5,55 @@ package com.jakewharton.mosaic.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import com.jakewharton.mosaic.layout.Measurable
import androidx.compose.runtime.remember
import com.jakewharton.mosaic.layout.MeasurePolicy
import com.jakewharton.mosaic.layout.MeasureResult
import com.jakewharton.mosaic.layout.MeasureScope
import com.jakewharton.mosaic.layout.ParentDataModifier
import com.jakewharton.mosaic.modifier.Modifier
import com.jakewharton.mosaic.ui.unit.Constraints
import kotlin.jvm.JvmName
@Composable
public fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable ColumnScope.() -> Unit,
) {
val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
Layout(
content = { ColumnScopeInstance.content() },
modifiers = modifier,
debugInfo = { "Column(alignment=$horizontalAlignment)" },
measurePolicy = ColumnMeasurePolicy(horizontalAlignment),
debugInfo = { "Column(arrangement=$verticalArrangement, alignment=$horizontalAlignment)" },
measurePolicy = measurePolicy,
)
}
private class ColumnMeasurePolicy(
private val horizontalAlignment: Alignment.Horizontal,
) : MeasurePolicy {
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints,
): MeasureResult {
var width = 0
var height = 0
val placeables = measurables.map { measurable ->
measurable.measure(constraints).also { placeable ->
width = maxOf(width, placeable.width)
height += placeable.height
}
}
return layout(width, height) {
var y = 0
placeables.forEachIndexed { index, placeable ->
val alignment = measurables[index].columnParentData?.alignment ?: horizontalAlignment
val x = alignment.align(placeable.width, width)
placeable.place(x, y)
y += placeable.height
}
internal val DefaultColumnMeasurePolicy: MeasurePolicy = RowColumnMeasurePolicy(
orientation = LayoutOrientation.Vertical,
verticalArrangement = Arrangement.Top,
horizontalArrangement = null,
arrangementSpacing = Arrangement.Top.spacing,
crossAxisAlignment = CrossAxisAlignment.horizontal(Alignment.Start),
crossAxisSize = SizeMode.Wrap,
)
@Composable
internal fun columnMeasurePolicy(
verticalArrangement: Arrangement.Vertical,
horizontalAlignment: Alignment.Horizontal,
): MeasurePolicy =
if (verticalArrangement == Arrangement.Top && horizontalAlignment == Alignment.Start) {
DefaultColumnMeasurePolicy
} else {
remember(verticalArrangement, horizontalAlignment) {
RowColumnMeasurePolicy(
orientation = LayoutOrientation.Vertical,
verticalArrangement = verticalArrangement,
horizontalArrangement = null,
arrangementSpacing = verticalArrangement.spacing,
crossAxisAlignment = CrossAxisAlignment.horizontal(horizontalAlignment),
crossAxisSize = SizeMode.Wrap,
)
}
}
}
/**
* Scope for the children of [Column].
@ -62,6 +62,24 @@ private class ColumnMeasurePolicy(
@Immutable
public interface ColumnScope {
/**
* Size the element's height proportional to its [weight] relative to other weighted sibling
* elements in the [Column]. The parent will divide the vertical space remaining after measuring
* unweighted child elements and distribute it according to this weight.
* When [fill] is true, the element will be forced to occupy the whole height allocated to it.
* Otherwise, the element is allowed to be smaller - this will result in [Column] being smaller,
* as the unused allocated height will not be redistributed to other siblings.
*
* @param weight The proportional height to give to this element, as related to the total of
* all weighted siblings. Must be positive.
* @param fill When `true`, the element will occupy the whole height allocated.
*/
@Stable
public fun Modifier.weight(
weight: Float,
fill: Boolean = true,
): Modifier
/**
* Align the element horizontally within the [Column]. This alignment will have priority over
* the [Column]'s `horizontalAlignment` parameter.
@ -72,28 +90,20 @@ public interface ColumnScope {
private object ColumnScopeInstance : ColumnScope {
@Stable
override fun Modifier.weight(weight: Float, fill: Boolean): Modifier {
require(weight > 0.0) { "invalid weight $weight; must be greater than zero" }
return this.then(
LayoutWeightModifier(
// Coerce Float.POSITIVE_INFINITY to Float.MAX_VALUE to avoid errors
weight = weight.coerceAtMost(Float.MAX_VALUE),
fill = fill,
),
)
}
@Stable
override fun Modifier.align(alignment: Alignment.Horizontal) = this.then(
HorizontalAlignModifier(horizontal = alignment),
)
}
private class HorizontalAlignModifier(
private val horizontal: Alignment.Horizontal,
) : ParentDataModifier {
override fun modifyParentData(parentData: Any?): Any {
return ((parentData as? ColumnParentData) ?: ColumnParentData()).also {
it.alignment = horizontal
}
}
override fun toString(): String = "HorizontalAlign($horizontal)"
}
private data class ColumnParentData(
var alignment: Alignment.Horizontal = Alignment.Start,
)
private val Measurable.columnParentData: ColumnParentData?
get() = this.parentData as? ColumnParentData

View File

@ -5,56 +5,55 @@ package com.jakewharton.mosaic.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import com.jakewharton.mosaic.layout.Measurable
import androidx.compose.runtime.remember
import com.jakewharton.mosaic.layout.MeasurePolicy
import com.jakewharton.mosaic.layout.MeasureResult
import com.jakewharton.mosaic.layout.MeasureScope
import com.jakewharton.mosaic.layout.ParentDataModifier
import com.jakewharton.mosaic.modifier.Modifier
import com.jakewharton.mosaic.ui.unit.Constraints
import kotlin.jvm.JvmName
@Composable
public fun Row(
modifier: Modifier = Modifier,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
verticalAlignment: Alignment.Vertical = Alignment.Top,
content: @Composable RowScope.() -> Unit,
) {
val measurePolicy = rowMeasurePolicy(horizontalArrangement, verticalAlignment)
Layout(
content = { RowScopeInstance.content() },
modifiers = modifier,
debugInfo = { "Row(alignment=$verticalAlignment)" },
measurePolicy = RowMeasurePolicy(verticalAlignment),
debugInfo = { "Row(arrangement=$horizontalArrangement, alignment=$verticalAlignment)" },
measurePolicy = measurePolicy,
)
}
private class RowMeasurePolicy(
private val verticalAlignment: Alignment.Vertical,
) : MeasurePolicy {
internal val DefaultRowMeasurePolicy: MeasurePolicy = RowColumnMeasurePolicy(
orientation = LayoutOrientation.Horizontal,
horizontalArrangement = Arrangement.Start,
verticalArrangement = null,
arrangementSpacing = Arrangement.Start.spacing,
crossAxisAlignment = CrossAxisAlignment.vertical(Alignment.Top),
crossAxisSize = SizeMode.Wrap,
)
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints,
): MeasureResult {
var width = 0
var height = 0
val placeables = measurables.map { measurable ->
measurable.measure(constraints).also { placeable ->
width += placeable.width
height = maxOf(height, placeable.height)
}
}
return layout(width, height) {
var x = 0
placeables.forEachIndexed { index, placeable ->
val alignment = measurables[index].rowParentData?.alignment ?: verticalAlignment
val y = alignment.align(placeable.height, height)
placeable.place(x, y)
x += placeable.width
}
@Composable
internal fun rowMeasurePolicy(
horizontalArrangement: Arrangement.Horizontal,
verticalAlignment: Alignment.Vertical,
): MeasurePolicy =
if (horizontalArrangement == Arrangement.Start && verticalAlignment == Alignment.Top) {
DefaultRowMeasurePolicy
} else {
remember(horizontalArrangement, verticalAlignment) {
RowColumnMeasurePolicy(
orientation = LayoutOrientation.Horizontal,
horizontalArrangement = horizontalArrangement,
verticalArrangement = null,
arrangementSpacing = horizontalArrangement.spacing,
crossAxisAlignment = CrossAxisAlignment.vertical(verticalAlignment),
crossAxisSize = SizeMode.Wrap,
)
}
}
}
/**
* Scope for the children of [Row].
@ -63,6 +62,24 @@ private class RowMeasurePolicy(
@Immutable
public interface RowScope {
/**
* Size the element's width proportional to its [weight] relative to other weighted sibling
* elements in the [Row]. The parent will divide the horizontal space remaining after measuring
* unweighted child elements and distribute it according to this weight.
* When [fill] is true, the element will be forced to occupy the whole width allocated to it.
* Otherwise, the element is allowed to be smaller - this will result in [Row] being smaller,
* as the unused allocated width will not be redistributed to other siblings.
*
* @param weight The proportional width to give to this element, as related to the total of
* all weighted siblings. Must be positive.
* @param fill When `true`, the element will occupy the whole width allocated.
*/
@Stable
public fun Modifier.weight(
weight: Float,
fill: Boolean = true,
): Modifier
/**
* Align the element vertically within the [Row]. This alignment will have priority over
* the [Row]'s `verticalAlignment` parameter.
@ -73,28 +90,20 @@ public interface RowScope {
private object RowScopeInstance : RowScope {
@Stable
override fun Modifier.weight(weight: Float, fill: Boolean): Modifier {
require(weight > 0.0) { "invalid weight $weight; must be greater than zero" }
return this.then(
LayoutWeightModifier(
// Coerce Float.POSITIVE_INFINITY to Float.MAX_VALUE to avoid errors
weight = weight.coerceAtMost(Float.MAX_VALUE),
fill = fill,
),
)
}
@Stable
override fun Modifier.align(alignment: Alignment.Vertical) = this.then(
VerticalAlignModifier(vertical = alignment),
)
}
private class VerticalAlignModifier(
private val vertical: Alignment.Vertical,
) : ParentDataModifier {
override fun modifyParentData(parentData: Any?): Any {
return ((parentData as? RowParentData) ?: RowParentData()).also {
it.alignment = vertical
}
}
override fun toString(): String = "VerticalAlign($vertical)"
}
private data class RowParentData(
var alignment: Alignment.Vertical = Alignment.Top,
)
private val Measurable.rowParentData: RowParentData?
get() = this.parentData as? RowParentData

View File

@ -0,0 +1,524 @@
package com.jakewharton.mosaic.ui
import androidx.compose.runtime.Immutable
import com.jakewharton.mosaic.layout.IntrinsicMeasurable
import com.jakewharton.mosaic.layout.Measurable
import com.jakewharton.mosaic.layout.MeasurePolicy
import com.jakewharton.mosaic.layout.MeasureResult
import com.jakewharton.mosaic.layout.MeasureScope
import com.jakewharton.mosaic.layout.ParentDataModifier
import com.jakewharton.mosaic.layout.Placeable
import com.jakewharton.mosaic.ui.LayoutOrientation.Horizontal
import com.jakewharton.mosaic.ui.LayoutOrientation.Vertical
import com.jakewharton.mosaic.ui.unit.Constraints
import kotlin.jvm.JvmInline
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
internal data class RowColumnMeasurePolicy(
private val orientation: LayoutOrientation,
private val horizontalArrangement: Arrangement.Horizontal?,
private val verticalArrangement: Arrangement.Vertical?,
private val arrangementSpacing: Int,
private val crossAxisSize: SizeMode,
private val crossAxisAlignment: CrossAxisAlignment,
) : MeasurePolicy {
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints,
): MeasureResult {
val placeables = arrayOfNulls<Placeable?>(measurables.size)
val rowColumnMeasureHelper =
RowColumnMeasurementHelper(
orientation,
horizontalArrangement,
verticalArrangement,
arrangementSpacing,
crossAxisSize,
crossAxisAlignment,
measurables,
placeables,
)
val measureResult = rowColumnMeasureHelper
.measureWithoutPlacing(constraints, 0, measurables.size)
val layoutWidth: Int
val layoutHeight: Int
if (orientation == Horizontal) {
layoutWidth = measureResult.mainAxisSize
layoutHeight = measureResult.crossAxisSize
} else {
layoutWidth = measureResult.crossAxisSize
layoutHeight = measureResult.mainAxisSize
}
return layout(layoutWidth, layoutHeight) {
rowColumnMeasureHelper.placeHelper(
this,
measureResult,
0,
)
}
}
override fun minIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int,
) = minIntrinsicWidthMeasureBlock(orientation)(
measurables,
height,
arrangementSpacing,
)
override fun minIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int,
) = minIntrinsicHeightMeasureBlock(orientation)(
measurables,
width,
arrangementSpacing,
)
override fun maxIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int,
) = maxIntrinsicWidthMeasureBlock(orientation)(
measurables,
height,
arrangementSpacing,
)
override fun maxIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int,
) = maxIntrinsicHeightMeasureBlock(orientation)(
measurables,
width,
arrangementSpacing,
)
}
/**
* [Row] will be [Horizontal], [Column] is [Vertical].
*/
internal enum class LayoutOrientation {
Horizontal,
Vertical,
}
/**
* Used to specify the alignment of a layout's children, in cross axis direction.
*/
@Immutable
internal sealed class CrossAxisAlignment {
/**
* Aligns to [size].
*
* @param size The remaining space (total size - content size) in the container.
* @param placeable The item being aligned.
*/
internal abstract fun align(
size: Int,
placeable: Placeable,
): Int
companion object {
/**
* Align children with vertical alignment.
*/
internal fun vertical(vertical: Alignment.Vertical): CrossAxisAlignment =
VerticalCrossAxisAlignment(vertical)
/**
* Align children with horizontal alignment.
*/
internal fun horizontal(horizontal: Alignment.Horizontal): CrossAxisAlignment =
HorizontalCrossAxisAlignment(horizontal)
}
private data class VerticalCrossAxisAlignment(
val vertical: Alignment.Vertical,
) : CrossAxisAlignment() {
override fun align(
size: Int,
placeable: Placeable,
): Int {
return vertical.align(0, size)
}
}
private data class HorizontalCrossAxisAlignment(
val horizontal: Alignment.Horizontal,
) : CrossAxisAlignment() {
override fun align(
size: Int,
placeable: Placeable,
): Int {
return horizontal.align(0, size)
}
}
}
/**
* Box [Constraints], but which abstract away width and height in favor of main axis and cross axis.
*/
@JvmInline
internal value class OrientationIndependentConstraints private constructor(
private val value: Constraints,
) {
inline val mainAxisMin: Int get() = value.minWidth
inline val mainAxisMax: Int get() = value.maxWidth
inline val crossAxisMin: Int get() = value.minHeight
inline val crossAxisMax: Int get() = value.maxHeight
constructor(
mainAxisMin: Int,
mainAxisMax: Int,
crossAxisMin: Int,
crossAxisMax: Int,
) : this(
Constraints(
minWidth = mainAxisMin,
maxWidth = mainAxisMax,
minHeight = crossAxisMin,
maxHeight = crossAxisMax,
),
)
constructor(c: Constraints, orientation: LayoutOrientation) : this(
if (orientation === Horizontal) c.minWidth else c.minHeight,
if (orientation === Horizontal) c.maxWidth else c.maxHeight,
if (orientation === Horizontal) c.minHeight else c.minWidth,
if (orientation === Horizontal) c.maxHeight else c.maxWidth,
)
// Given an orientation, resolves the current instance to traditional constraints.
fun toBoxConstraints(orientation: LayoutOrientation) =
if (orientation === Horizontal) {
Constraints(mainAxisMin, mainAxisMax, crossAxisMin, crossAxisMax)
} else {
Constraints(crossAxisMin, crossAxisMax, mainAxisMin, mainAxisMax)
}
fun copy(
mainAxisMin: Int = this.mainAxisMin,
mainAxisMax: Int = this.mainAxisMax,
crossAxisMin: Int = this.crossAxisMin,
crossAxisMax: Int = this.crossAxisMax,
): OrientationIndependentConstraints =
OrientationIndependentConstraints(
mainAxisMin,
mainAxisMax,
crossAxisMin,
crossAxisMax,
)
}
internal val IntrinsicMeasurable.rowColumnParentData: RowColumnParentData?
get() = parentData as? RowColumnParentData
internal val RowColumnParentData?.weight: Float
get() = this?.weight ?: 0f
internal val RowColumnParentData?.fill: Boolean
get() = this?.fill ?: true
private fun minIntrinsicWidthMeasureBlock(orientation: LayoutOrientation) =
if (orientation == Horizontal) {
IntrinsicMeasureBlocks.HorizontalMinWidth
} else {
IntrinsicMeasureBlocks.VerticalMinWidth
}
private fun minIntrinsicHeightMeasureBlock(orientation: LayoutOrientation) =
if (orientation == Horizontal) {
IntrinsicMeasureBlocks.HorizontalMinHeight
} else {
IntrinsicMeasureBlocks.VerticalMinHeight
}
private fun maxIntrinsicWidthMeasureBlock(orientation: LayoutOrientation) =
if (orientation == Horizontal) {
IntrinsicMeasureBlocks.HorizontalMaxWidth
} else {
IntrinsicMeasureBlocks.VerticalMaxWidth
}
private fun maxIntrinsicHeightMeasureBlock(orientation: LayoutOrientation) =
if (orientation == Horizontal) {
IntrinsicMeasureBlocks.HorizontalMaxHeight
} else {
IntrinsicMeasureBlocks.VerticalMaxHeight
}
private object IntrinsicMeasureBlocks {
val HorizontalMinWidth: (List<IntrinsicMeasurable>, Int, Int) -> Int =
{ measurables, availableHeight, mainAxisSpacing ->
intrinsicSize(
measurables,
{ h -> minIntrinsicWidth(h) },
{ w -> maxIntrinsicHeight(w) },
availableHeight,
mainAxisSpacing,
Horizontal,
Horizontal,
)
}
val VerticalMinWidth: (List<IntrinsicMeasurable>, Int, Int) -> Int =
{ measurables, availableHeight, mainAxisSpacing ->
intrinsicSize(
measurables,
{ h -> minIntrinsicWidth(h) },
{ w -> maxIntrinsicHeight(w) },
availableHeight,
mainAxisSpacing,
Vertical,
Horizontal,
)
}
val HorizontalMinHeight: (List<IntrinsicMeasurable>, Int, Int) -> Int =
{ measurables, availableWidth, mainAxisSpacing ->
intrinsicSize(
measurables,
{ w -> minIntrinsicHeight(w) },
{ h -> maxIntrinsicWidth(h) },
availableWidth,
mainAxisSpacing,
Horizontal,
Vertical,
)
}
val VerticalMinHeight: (List<IntrinsicMeasurable>, Int, Int) -> Int =
{ measurables, availableWidth, mainAxisSpacing ->
intrinsicSize(
measurables,
{ w -> minIntrinsicHeight(w) },
{ h -> maxIntrinsicWidth(h) },
availableWidth,
mainAxisSpacing,
Vertical,
Vertical,
)
}
val HorizontalMaxWidth: (List<IntrinsicMeasurable>, Int, Int) -> Int =
{ measurables, availableHeight, mainAxisSpacing ->
intrinsicSize(
measurables,
{ h -> maxIntrinsicWidth(h) },
{ w -> maxIntrinsicHeight(w) },
availableHeight,
mainAxisSpacing,
Horizontal,
Horizontal,
)
}
val VerticalMaxWidth: (List<IntrinsicMeasurable>, Int, Int) -> Int =
{ measurables, availableHeight, mainAxisSpacing ->
intrinsicSize(
measurables,
{ h -> maxIntrinsicWidth(h) },
{ w -> maxIntrinsicHeight(w) },
availableHeight,
mainAxisSpacing,
Vertical,
Horizontal,
)
}
val HorizontalMaxHeight: (List<IntrinsicMeasurable>, Int, Int) -> Int =
{ measurables, availableWidth, mainAxisSpacing ->
intrinsicSize(
measurables,
{ w -> maxIntrinsicHeight(w) },
{ h -> maxIntrinsicWidth(h) },
availableWidth,
mainAxisSpacing,
Horizontal,
Vertical,
)
}
val VerticalMaxHeight: (List<IntrinsicMeasurable>, Int, Int) -> Int =
{ measurables, availableWidth, mainAxisSpacing ->
intrinsicSize(
measurables,
{ w -> maxIntrinsicHeight(w) },
{ h -> maxIntrinsicWidth(h) },
availableWidth,
mainAxisSpacing,
Vertical,
Vertical,
)
}
}
private fun intrinsicSize(
children: List<IntrinsicMeasurable>,
intrinsicMainSize: IntrinsicMeasurable.(Int) -> Int,
intrinsicCrossSize: IntrinsicMeasurable.(Int) -> Int,
crossAxisAvailable: Int,
mainAxisSpacing: Int,
layoutOrientation: LayoutOrientation,
intrinsicOrientation: LayoutOrientation,
) = if (layoutOrientation == intrinsicOrientation) {
intrinsicMainAxisSize(children, intrinsicMainSize, crossAxisAvailable, mainAxisSpacing)
} else {
intrinsicCrossAxisSize(
children,
intrinsicCrossSize,
intrinsicMainSize,
crossAxisAvailable,
mainAxisSpacing,
)
}
private fun intrinsicMainAxisSize(
children: List<IntrinsicMeasurable>,
mainAxisSize: IntrinsicMeasurable.(Int) -> Int,
crossAxisAvailable: Int,
mainAxisSpacing: Int,
): Int {
if (children.isEmpty()) return 0
var weightUnitSpace = 0
var fixedSpace = 0
var totalWeight = 0f
children.forEach { child ->
val weight = child.rowColumnParentData.weight
val size = child.mainAxisSize(crossAxisAvailable)
if (weight == 0f) {
fixedSpace += size
} else if (weight > 0f) {
totalWeight += weight
weightUnitSpace = max(weightUnitSpace, (size / weight).roundToInt())
}
}
return (weightUnitSpace * totalWeight).roundToInt() + fixedSpace +
(children.size - 1) * mainAxisSpacing
}
private fun intrinsicCrossAxisSize(
children: List<IntrinsicMeasurable>,
mainAxisSize: IntrinsicMeasurable.(Int) -> Int,
crossAxisSize: IntrinsicMeasurable.(Int) -> Int,
mainAxisAvailable: Int,
mainAxisSpacing: Int,
): Int {
if (children.isEmpty()) return 0
var fixedSpace = min((children.size - 1) * mainAxisSpacing, mainAxisAvailable)
var crossAxisMax = 0
var totalWeight = 0f
children.forEach { child ->
val weight = child.rowColumnParentData.weight
if (weight == 0f) {
// Ask the child how much main axis space it wants to occupy. This cannot be more
// than the remaining available space.
val remaining = if (mainAxisAvailable == Constraints.Infinity) {
Constraints.Infinity
} else {
mainAxisAvailable - fixedSpace
}
val mainAxisSpace = min(
child.mainAxisSize(Constraints.Infinity),
remaining,
)
fixedSpace += mainAxisSpace
// Now that the assigned main axis space is known, ask about the cross axis space.
crossAxisMax = max(crossAxisMax, child.crossAxisSize(mainAxisSpace))
} else if (weight > 0f) {
totalWeight += weight
}
}
// For weighted children, calculate how much main axis space weight=1 would represent.
val weightUnitSpace = if (totalWeight == 0f) {
0
} else if (mainAxisAvailable == Constraints.Infinity) {
Constraints.Infinity
} else {
(max(mainAxisAvailable - fixedSpace, 0) / totalWeight).roundToInt()
}
children.forEach { child ->
val weight = child.rowColumnParentData.weight
// Now the main axis for weighted children is known, so ask about the cross axis space.
if (weight > 0f) {
crossAxisMax = max(
crossAxisMax,
child.crossAxisSize(
if (weightUnitSpace != Constraints.Infinity) {
(weightUnitSpace * weight).roundToInt()
} else {
Constraints.Infinity
},
),
)
}
}
return crossAxisMax
}
internal class LayoutWeightModifier(
private val weight: Float,
private val fill: Boolean,
) : ParentDataModifier {
override fun modifyParentData(parentData: Any?): Any {
return ((parentData as? RowColumnParentData) ?: RowColumnParentData()).also {
it.weight = weight
it.fill = fill
}
}
override fun toString(): String = "LayoutWeight(weight=$weight, fill=$fill)"
}
internal class HorizontalAlignModifier(
private val horizontal: Alignment.Horizontal,
) : ParentDataModifier {
override fun modifyParentData(parentData: Any?): Any {
return ((parentData as? RowColumnParentData) ?: RowColumnParentData()).also {
it.crossAxisAlignment = CrossAxisAlignment.horizontal(horizontal)
}
}
override fun toString(): String = "HorizontalAlign($horizontal)"
}
internal class VerticalAlignModifier(
private val vertical: Alignment.Vertical,
) : ParentDataModifier {
override fun modifyParentData(parentData: Any?): Any {
return ((parentData as? RowColumnParentData) ?: RowColumnParentData()).also {
it.crossAxisAlignment = CrossAxisAlignment.vertical(vertical)
}
}
override fun toString(): String = "VerticalAlign($vertical)"
}
/**
* Parent data associated with children.
*/
internal data class RowColumnParentData(
var weight: Float = 0f,
var fill: Boolean = true,
var crossAxisAlignment: CrossAxisAlignment? = null,
)
/**
* Used to specify how a layout chooses its own size when multiple behaviors are possible.
*/
internal enum class SizeMode {
/**
* Minimize the amount of free space by wrapping the children,
* subject to the incoming layout constraints.
*/
Wrap,
/**
* Maximize the amount of free space by expanding to fill the available space,
* subject to the incoming layout constraints.
*/
Expand,
}

View File

@ -0,0 +1,262 @@
package com.jakewharton.mosaic.ui
import com.jakewharton.mosaic.layout.Measurable
import com.jakewharton.mosaic.layout.Placeable
import com.jakewharton.mosaic.ui.unit.Constraints
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 mainAxisPositions: IntArray,
)
/**
* RowColumnMeasurementHelper
* Measures the row and column without placing, useful for reusing row/column logic
*/
internal class RowColumnMeasurementHelper(
private val orientation: LayoutOrientation,
private val horizontalArrangement: Arrangement.Horizontal?,
private val verticalArrangement: Arrangement.Vertical?,
private val arrangementSpacing: Int,
private val crossAxisSize: SizeMode,
private val crossAxisAlignment: CrossAxisAlignment,
private val measurables: List<Measurable>,
private val placeables: Array<Placeable?>,
) {
private val rowColumnParentData = Array(measurables.size) {
measurables[it].rowColumnParentData
}
private fun Placeable.mainAxisSize() =
if (orientation == LayoutOrientation.Horizontal) width else height
private fun Placeable.crossAxisSize() =
if (orientation == LayoutOrientation.Horizontal) height else width
/**
* Measures the row and column without placing, useful for reusing row/column logic
*
* @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(
constraints: Constraints,
startIndex: Int,
endIndex: Int,
): RowColumnMeasureHelperResult {
@Suppress("NAME_SHADOWING")
val constraints = OrientationIndependentConstraints(constraints, orientation)
var totalWeight = 0f
var fixedSpace = 0L
var crossAxisSpace = 0
var weightChildrenCount = 0
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).coerceAtLeast(0).toInt()
},
crossAxisMin = 0,
).toBoxConstraints(orientation),
)
spaceAfterLastNoWeight = min(
arrangementSpacing,
(mainAxisMax - fixedSpace - placeable.mainAxisSize())
.coerceAtLeast(0).toInt(),
)
fixedSpace += placeable.mainAxisSize() + spaceAfterLastNoWeight
crossAxisSpace = max(crossAxisSpace, placeable.crossAxisSize())
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 arrangementSpacingTotal = arrangementSpacing.toLong() * (weightChildrenCount - 1)
val remainingToTarget =
(targetSpace - fixedSpace - arrangementSpacingTotal).coerceAtLeast(0)
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())
placeables[i] = placeable
}
}
weightedSpace = (weightedSpace + arrangementSpacingTotal)
.coerceIn(0, constraints.mainAxisMax - fixedSpace)
.toInt()
}
// Compute the Row or Column size and position the children.
val mainAxisLayoutSize = max(
(fixedSpace + weightedSpace).coerceAtLeast(0).toInt(),
constraints.mainAxisMin,
)
val crossAxisLayoutSize = if (constraints.crossAxisMax != Constraints.Infinity &&
crossAxisSize == SizeMode.Expand
) {
constraints.crossAxisMax
} else {
max(crossAxisSpace, constraints.crossAxisMin)
}
val mainAxisPositions = IntArray(subSize) { 0 }
val childrenMainAxisSize = IntArray(subSize) { index ->
placeables[index + startIndex]!!.mainAxisSize()
}
return RowColumnMeasureHelperResult(
mainAxisSize = mainAxisLayoutSize,
crossAxisSize = crossAxisLayoutSize,
startIndex = startIndex,
endIndex = endIndex,
mainAxisPositions = mainAxisPositions(
mainAxisLayoutSize,
childrenMainAxisSize,
mainAxisPositions,
),
)
}
private fun mainAxisPositions(
mainAxisLayoutSize: Int,
childrenMainAxisSize: IntArray,
mainAxisPositions: IntArray,
): IntArray {
if (orientation == LayoutOrientation.Vertical) {
with(requireNotNull(verticalArrangement) { "null verticalArrangement in Column" }) {
arrange(
mainAxisLayoutSize,
childrenMainAxisSize,
mainAxisPositions,
)
}
} else {
with(requireNotNull(horizontalArrangement) { "null horizontalArrangement in Row" }) {
arrange(
mainAxisLayoutSize,
childrenMainAxisSize,
mainAxisPositions,
)
}
}
return mainAxisPositions
}
private fun getCrossAxisPosition(
placeable: Placeable,
parentData: RowColumnParentData?,
crossAxisLayoutSize: Int,
): Int {
val childCrossAlignment = parentData?.crossAxisAlignment ?: crossAxisAlignment
return childCrossAlignment.align(
size = crossAxisLayoutSize - placeable.crossAxisSize(),
placeable = placeable,
)
}
fun placeHelper(
placeableScope: Placeable.PlacementScope,
measureResult: RowColumnMeasureHelperResult,
crossAxisOffset: Int,
) {
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,
) + crossAxisOffset
if (orientation == LayoutOrientation.Horizontal) {
placeable.place(
mainAxisPositions[i - measureResult.startIndex],
crossAxisPosition,
)
} else {
placeable.place(
crossAxisPosition,
mainAxisPositions[i - measureResult.startIndex],
)
}
}
}
}
}

View File

@ -43,7 +43,7 @@ class DebugRenderingTest {
|Failed
|
|NODES:
|Row\(alignment=Vertical\(bias=-1\)\) x=0 y=0 w=11 h=1
|Row\(arrangement=Arrangement#Start, alignment=Vertical\(bias=-1\)\) x=0 y=0 w=11 h=1
| Text\("Hello "\) x=0 y=0 w=6 h=1 DrawBehind
| Layout\(\) x=6 y=0 w=5 h=1 DrawBehind
|