mirror of
https://github.com/JakeWharton/mosaic.git
synced 2025-11-02 13:00:09 +08:00
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:
@ -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:
|
||||
* 
|
||||
*
|
||||
* Different vertical arrangements in [Column]s:
|
||||
* 
|
||||
*/
|
||||
@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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
}
|
||||
@ -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],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
|
||||
|
||||
Reference in New Issue
Block a user