blob: 9321ebe38a26c875e8284b2387d12be0bf2293a6 [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.ui.layout
import androidx.annotation.FloatRange
import androidx.compose.Composable
import androidx.compose.composer
import androidx.ui.core.Constraints
import androidx.ui.core.IntPx
import androidx.ui.core.IntPxSize
import androidx.ui.core.Measurable
import androidx.ui.core.Placeable
import androidx.ui.core.ipx
import androidx.ui.core.max
import androidx.ui.core.ComplexLayout
import androidx.ui.core.ParentData
import androidx.ui.core.isFinite
import androidx.ui.core.px
import androidx.ui.core.round
import androidx.ui.core.toPx
/**
* Parent data associated with children to assign flex and fit values for them.
*/
private data class FlexInfo(val flex: Float, val fit: FlexFit)
/**
* Collects information about the children of a [FlexColumn] or [FlexColumn]
* when its body is executed with a [FlexChildren] instance as argument.
*/
class FlexChildren internal constructor() {
internal val childrenList = mutableListOf<@Composable() () -> Unit>()
fun expanded(@FloatRange(from = 0.0) flex: Float, children: @Composable() () -> Unit) {
if (flex < 0) {
throw IllegalArgumentException("flex must be >= 0")
}
childrenList += {
ParentData(data = FlexInfo(flex = flex, fit = FlexFit.Tight), children = children)
}
}
fun flexible(@FloatRange(from = 0.0) flex: Float, children: @Composable() () -> Unit) {
if (flex < 0) {
throw IllegalArgumentException("flex must be >= 0")
}
childrenList += {
ParentData(data = FlexInfo(flex = flex, fit = FlexFit.Loose), children = children)
}
}
fun inflexible(children: @Composable() () -> Unit) {
childrenList += {
ParentData(data = FlexInfo(flex = 0f, fit = FlexFit.Loose), children = children)
}
}
}
/**
* A widget that places its children in a horizontal sequence, assigning children widths
* according to their flex weights.
*
* [FlexRow] children can be:
* - [inflexible] meaning that the child is not flex, and it should be measured with loose
* constraints to determine its preferred width
* - [expanded] meaning that the child is flexible, and it should be assigned a width according
* to its flex weight relative to its flexible children. The child is forced to occupy the
* entire width assigned by the parent
* - [flexible] similar to [expanded], but the child can leave unoccupied width.
*
* Example usage:
*
* @sample androidx.ui.layout.samples.SimpleFlexRow
*
* @param mainAxisAlignment The alignment of the layout's children in main axis direction.
* Default is [MainAxisAlignment.Start].
* @param mainAxisSize The size of the layout in the main axis dimension.
* Default is [FlexSize.Max].
* @param crossAxisAlignment The alignment of the layout's children in cross axis direction.
* Default is [CrossAxisAlignment.Center].
* @param crossAxisSize The size of the layout in the cross axis dimension.
* Default is [FlexSize.Min].
*/
@Composable
fun FlexRow(
mainAxisAlignment: MainAxisAlignment = MainAxisAlignment.Start,
mainAxisSize: FlexSize = FlexSize.Max,
crossAxisAlignment: CrossAxisAlignment = CrossAxisAlignment.Center,
crossAxisSize: FlexSize = FlexSize.Min,
block: FlexChildren.() -> Unit
) {
Flex(
orientation = FlexOrientation.Horizontal,
mainAxisAlignment = mainAxisAlignment,
mainAxisSize = mainAxisSize,
crossAxisAlignment = crossAxisAlignment,
crossAxisSize = crossAxisSize,
block = block
)
}
/**
* A widget that places its children in a vertical sequence, assigning children heights
* according to their flex weights.
*
* [FlexRow] children can be:
* - [inflexible] meaning that the child is not flex, and it should be measured with
* loose constraints to determine its preferred height
* - [expanded] meaning that the child is flexible, and it should be assigned a
* height according to its flex weight relative to its flexible children. The child is forced
* to occupy the entire height assigned by the parent
* - [flexible] similar to [expanded], but the child can leave unoccupied height.
*
* Example usage:
*
* @sample androidx.ui.layout.samples.SimpleFlexColumn
*
* @param mainAxisAlignment The alignment of the layout's children in main axis direction.
* Default is [MainAxisAlignment.Start].
* @param mainAxisSize The size of the layout in the main axis dimension.
* Default is [FlexSize.Max].
* @param crossAxisAlignment The alignment of the layout's children in cross axis direction.
* Default is [CrossAxisAlignment.Center].
* @param crossAxisSize The size of the layout in the cross axis dimension.
* Default is [FlexSize.Min].
*/
@Composable
fun FlexColumn(
mainAxisAlignment: MainAxisAlignment = MainAxisAlignment.Start,
mainAxisSize: FlexSize = FlexSize.Max,
crossAxisAlignment: CrossAxisAlignment = CrossAxisAlignment.Center,
crossAxisSize: FlexSize = FlexSize.Min,
block: FlexChildren.() -> Unit
) {
Flex(
orientation = FlexOrientation.Vertical,
mainAxisAlignment = mainAxisAlignment,
mainAxisSize = mainAxisSize,
crossAxisAlignment = crossAxisAlignment,
crossAxisSize = crossAxisSize,
block = block
)
}
/**
* A widget that places its children in a horizontal sequence.
*
* Example usage:
*
* @sample androidx.ui.layout.samples.SimpleRow
*
* @param mainAxisAlignment The alignment of the layout's children in main axis direction.
* Default is [MainAxisAlignment.Start].
* @param mainAxisSize The size of the layout in the main axis dimension.
* Default is [FlexSize.Max].
* @param crossAxisAlignment The alignment of the layout's children in cross axis direction.
* Default is [CrossAxisAlignment.Center].
* @param crossAxisSize The size of the layout in the cross axis dimension.
* Default is [FlexSize.Min].
*/
@Composable
fun Row(
mainAxisAlignment: MainAxisAlignment = MainAxisAlignment.Start,
mainAxisSize: FlexSize = FlexSize.Max,
crossAxisAlignment: CrossAxisAlignment = CrossAxisAlignment.Center,
crossAxisSize: FlexSize = FlexSize.Min,
block: @Composable() () -> Unit
) {
FlexRow(
mainAxisAlignment = mainAxisAlignment,
mainAxisSize = mainAxisSize,
crossAxisAlignment = crossAxisAlignment,
crossAxisSize = crossAxisSize
) {
inflexible {
block()
}
}
}
/**
* A widget that places its children in a vertical sequence.
*
* Example usage:
*
* @sample androidx.ui.layout.samples.SimpleColumn
*
* @param mainAxisAlignment The alignment of the layout's children in main axis direction.
* Default is [MainAxisAlignment.Start].
* @param mainAxisSize The size of the layout in the main axis dimension.
* Default is [FlexSize.Max].
* @param crossAxisAlignment The alignment of the layout's children in cross axis direction.
* Default is [CrossAxisAlignment.Center].
* @param crossAxisSize The size of the layout in the cross axis dimension.
* Default is [FlexSize.Min].
*/
@Composable
fun Column(
mainAxisAlignment: MainAxisAlignment = MainAxisAlignment.Start,
mainAxisSize: FlexSize = FlexSize.Max,
crossAxisAlignment: CrossAxisAlignment = CrossAxisAlignment.Center,
crossAxisSize: FlexSize = FlexSize.Min,
block: @Composable() () -> Unit
) {
FlexColumn(
mainAxisAlignment = mainAxisAlignment,
mainAxisSize = mainAxisSize,
crossAxisAlignment = crossAxisAlignment,
crossAxisSize = crossAxisSize
) {
inflexible {
block()
}
}
}
internal enum class FlexFit {
Tight,
Loose
}
internal enum class FlexOrientation {
Horizontal,
Vertical
}
/**
* Used to specify how a layout chooses its own size when multiple behaviors are possible.
*/
enum class FlexSize {
/**
* Minimize the amount of free space, subject to the incoming layout constraints.
*/
Min,
/**
* Maximize the amount of free space, subject to the incoming layout constraints.
*/
Max
}
/**
* Used to specify the alignment of a layout's children, in main axis direction.
*/
enum class MainAxisAlignment(internal val aligner: Aligner) {
/**
* Place children such that they are as close as possible to the middle of the main axis.
*/
Center(MainAxisCenterAligner()),
/**
* Place children such that they are as close as possible to the start of the main axis.
* TODO(popam): Consider rtl directionality.
*/
Start(MainAxisStartAligner()),
/**
* Place children such that they are as close as possible to the end of the main axis.
*/
End(MainAxisEndAligner()),
/**
* Place children such that they are spaced evenly across the main axis, including free
* space before the first child and after the last child.
*/
SpaceEvenly(MainAxisSpaceEvenlyAligner()),
/**
* Place children such that they are spaced evenly across the main axis, without free
* space before the first child or after the last child.
*/
SpaceBetween(MainAxisSpaceBetweenAligner()),
/**
* 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.
*/
SpaceAround(MainAxisSpaceAroundAligner());
internal interface Aligner {
fun align(totalSize: IntPx, size: List<IntPx>): List<IntPx>
}
private class MainAxisCenterAligner : Aligner {
override fun align(totalSize: IntPx, size: List<IntPx>): List<IntPx> {
val consumedSize = size.fold(0.ipx) { a, b -> a + b }
val positions = mutableListOf<IntPx>()
var current = (totalSize - consumedSize).toPx() / 2
size.forEach {
positions.add(current.round())
current += it
}
return positions
}
}
private class MainAxisStartAligner : Aligner {
override fun align(totalSize: IntPx, size: List<IntPx>): List<IntPx> {
val positions = mutableListOf<IntPx>()
var current = 0.ipx
size.forEach {
positions.add(current)
current += it
}
return positions
}
}
private class MainAxisEndAligner : Aligner {
override fun align(totalSize: IntPx, size: List<IntPx>): List<IntPx> {
val consumedSize = size.fold(0.ipx) { a, b -> a + b }
val positions = mutableListOf<IntPx>()
var current = totalSize - consumedSize
size.forEach {
positions.add(current)
current += it
}
return positions
}
}
private class MainAxisSpaceEvenlyAligner : Aligner {
override fun align(totalSize: IntPx, size: List<IntPx>): List<IntPx> {
val consumedSize = size.fold(0.ipx) { a, b -> a + b }
val gapSize = (totalSize - consumedSize).toPx() / (size.size + 1)
val positions = mutableListOf<IntPx>()
var current = gapSize
size.forEach {
positions.add(current.round())
current += it.toPx() + gapSize
}
return positions
}
}
private class MainAxisSpaceBetweenAligner : Aligner {
override fun align(totalSize: IntPx, size: List<IntPx>): List<IntPx> {
val consumedSize = size.fold(0.ipx) { a, b -> a + b }
val gapSize = if (size.size > 1) {
(totalSize - consumedSize).toPx() / (size.size - 1)
} else {
0.px
}
val positions = mutableListOf<IntPx>()
var current = 0.px
size.forEach {
positions.add(current.round())
current += it.toPx() + gapSize
}
return positions
}
}
private class MainAxisSpaceAroundAligner : Aligner {
override fun align(totalSize: IntPx, size: List<IntPx>): List<IntPx> {
val consumedSize = size.fold(0.ipx) { a, b -> a + b }
val gapSize = if (size.isNotEmpty()) {
(totalSize - consumedSize).toPx() / size.size
} else {
0.px
}
val positions = mutableListOf<IntPx>()
var current = gapSize / 2
size.forEach {
positions.add(current.round())
current += it.toPx() + gapSize
}
return positions
}
}
}
/**
* Used to specify the alignment of a layout's children, in cross axis direction.
*/
enum class CrossAxisAlignment {
/**
* Place children such that their center is in the middle of the cross axis.
*/
Center,
/**
* Place children such that their start edge is aligned to the start edge of the cross
* axis. TODO(popam): Consider rtl directionality.
*/
Start,
/**
* Place children such that their end edge is aligned to the end edge of the cross
* axis. TODO(popam): Consider rtl directionality.
*/
End,
/**
* Force children to occupy the entire cross axis space.
*/
Stretch,
/**
* Align children by their baseline. TODO(popam): support this when baseline support is
* added in ComplexMeasureBox.
*/
Baseline
}
/**
* Box [Constraints], but which abstract away width and height in favor of main axis and cross axis.
*/
private data class OrientationIndependentConstraints(
val mainAxisMin: IntPx,
val mainAxisMax: IntPx,
val crossAxisMin: IntPx,
val crossAxisMax: IntPx
) {
constructor(c: Constraints, orientation: FlexOrientation) : this(
if (orientation === FlexOrientation.Horizontal) c.minWidth else c.minHeight,
if (orientation === FlexOrientation.Horizontal) c.maxWidth else c.maxHeight,
if (orientation === FlexOrientation.Horizontal) c.minHeight else c.minWidth,
if (orientation === FlexOrientation.Horizontal) c.maxHeight else c.maxWidth
)
// Creates a new instance with the same cross axis constraints and unbounded main axis.
fun looseMainAxis() = OrientationIndependentConstraints(
IntPx.Zero, IntPx.Infinity, crossAxisMin, crossAxisMax
)
// Creates a new instance with the same main axis constraints and maximum tight cross axis.
fun stretchCrossAxis() = OrientationIndependentConstraints(
mainAxisMin,
mainAxisMax,
if (crossAxisMax.isFinite()) crossAxisMax else crossAxisMin,
crossAxisMax
)
// Given an orientation, resolves the current instance to traditional constraints.
fun toBoxConstraints(orientation: FlexOrientation) =
if (orientation === FlexOrientation.Horizontal) {
Constraints(mainAxisMin, mainAxisMax, crossAxisMin, crossAxisMax)
} else {
Constraints(crossAxisMin, crossAxisMax, mainAxisMin, mainAxisMax)
}
// Given an orientation, resolves the max width constraint this instance represents.
fun maxWidth(orientation: FlexOrientation) =
if (orientation === FlexOrientation.Horizontal) {
mainAxisMax
} else {
crossAxisMax
}
// Given an orientation, resolves the max height constraint this instance represents.
fun maxHeight(orientation: FlexOrientation) =
if (orientation === FlexOrientation.Horizontal) {
crossAxisMax
} else {
mainAxisMax
}
}
private val Measurable.flex: Float get() = (parentData as FlexInfo).flex
private val Measurable.fit: FlexFit get() = (parentData as FlexInfo).fit
/**
* Layout model that places its children in a horizontal or vertical sequence, according to the
* specified orientation, while also looking at the flex weights of the children.
*/
@Composable
private fun Flex(
orientation: FlexOrientation,
mainAxisSize: FlexSize = FlexSize.Max,
mainAxisAlignment: MainAxisAlignment = MainAxisAlignment.Start,
crossAxisSize: FlexSize = FlexSize.Min,
crossAxisAlignment: CrossAxisAlignment = CrossAxisAlignment.Center,
block: FlexChildren.() -> Unit
) {
fun Placeable.mainAxisSize() = if (orientation == FlexOrientation.Horizontal) width else height
fun Placeable.crossAxisSize() = if (orientation == FlexOrientation.Horizontal) height else width
val flexChildren: @Composable() () -> Unit = with(FlexChildren()) {
block()
val composable = @Composable {
childrenList.forEach { it() }
}
composable
}
ComplexLayout(flexChildren) {
measure { children, outerConstraints ->
val constraints = OrientationIndependentConstraints(outerConstraints, orientation)
var totalFlex = 0f
var inflexibleSpace = IntPx.Zero
var crossAxisSpace = IntPx.Zero
val placeables = arrayOfNulls<Placeable>(children.size)
// First measure children with zero flex.
for (i in 0 until children.size) {
val child = children[i]
val flex = child.flex
if (flex > 0f) {
totalFlex += child.flex
} else {
val placeable = child.measure(
// Ask for preferred main axis size.
constraints.looseMainAxis().let {
if (crossAxisAlignment == CrossAxisAlignment.Stretch) {
it.stretchCrossAxis()
} else {
it.copy(crossAxisMin = IntPx.Zero)
}
}.toBoxConstraints(orientation)
)
inflexibleSpace += placeable.mainAxisSize()
crossAxisSpace = max(crossAxisSpace, placeable.crossAxisSize())
placeables[i] = placeable
}
}
// Then measure the rest according to their flexes in the remaining main axis space.
val targetSpace = if (mainAxisSize == FlexSize.Max) {
constraints.mainAxisMax
} else {
constraints.mainAxisMin
}
var flexibleSpace = IntPx.Zero
for (i in 0 until children.size) {
val child = children[i]
val flex = child.flex
if (flex > 0f) {
val childMainAxisSize = max(
IntPx.Zero,
(targetSpace - inflexibleSpace) * child.flex / totalFlex
)
val placeable = child.measure(
OrientationIndependentConstraints(
if (child.fit == FlexFit.Tight && childMainAxisSize.isFinite()) {
childMainAxisSize
} else {
IntPx.Zero
},
childMainAxisSize,
if (crossAxisAlignment == CrossAxisAlignment.Stretch) {
constraints.crossAxisMax
} else {
IntPx.Zero
},
constraints.crossAxisMax
).toBoxConstraints(orientation)
)
flexibleSpace += placeable.mainAxisSize()
crossAxisSpace = max(crossAxisSpace, placeable.crossAxisSize())
placeables[i] = placeable
}
}
// Compute the Flex size and position the children.
val mainAxisLayoutSize = if (constraints.mainAxisMax.isFinite() &&
mainAxisSize == FlexSize.Max
) {
constraints.mainAxisMax
} else {
max(inflexibleSpace + flexibleSpace, constraints.mainAxisMin)
}
val crossAxisLayoutSize = if (constraints.crossAxisMax.isFinite() &&
crossAxisSize == FlexSize.Max
) {
constraints.crossAxisMax
} else {
max(crossAxisSpace, constraints.crossAxisMin)
}
val layoutWidth = if (orientation == FlexOrientation.Horizontal) {
mainAxisLayoutSize
} else {
crossAxisLayoutSize
}
val layoutHeight = if (orientation == FlexOrientation.Horizontal) {
crossAxisLayoutSize
} else {
mainAxisLayoutSize
}
layout(layoutWidth, layoutHeight) {
val childrenMainAxisSize = placeables.map { it!!.mainAxisSize() }
val mainAxisPositions = mainAxisAlignment.aligner
.align(mainAxisLayoutSize, childrenMainAxisSize)
placeables.forEachIndexed { index, placeable ->
placeable!!
val crossAxis = when (crossAxisAlignment) {
CrossAxisAlignment.Start -> IntPx.Zero
CrossAxisAlignment.Stretch -> IntPx.Zero
CrossAxisAlignment.End -> {
crossAxisLayoutSize - placeable.crossAxisSize()
}
CrossAxisAlignment.Center -> {
Alignment.Center.align(
IntPxSize(
mainAxisLayoutSize - placeable.mainAxisSize(),
crossAxisLayoutSize - placeable.crossAxisSize()
)
).y
}
else -> {
IntPx.Zero /* TODO(popam): support baseline */
}
}
if (orientation == FlexOrientation.Horizontal) {
placeable.place(mainAxisPositions[index], crossAxis)
} else {
placeable.place(crossAxis, mainAxisPositions[index])
}
}
}
}
minIntrinsicWidth { children, availableHeight ->
intrinsicSize(
children,
{ h -> minIntrinsicWidth(h) },
{ w -> maxIntrinsicHeight(w) },
availableHeight,
orientation,
FlexOrientation.Horizontal
)
}
minIntrinsicHeight { children, availableWidth ->
intrinsicSize(
children,
{ w -> minIntrinsicHeight(w) },
{ h -> maxIntrinsicWidth(h) },
availableWidth,
orientation,
FlexOrientation.Vertical
)
}
maxIntrinsicWidth { children, availableHeight ->
intrinsicSize(
children,
{ h -> maxIntrinsicWidth(h) },
{ w -> maxIntrinsicHeight(w) },
availableHeight,
orientation,
FlexOrientation.Horizontal
)
}
maxIntrinsicHeight { children, availableWidth ->
intrinsicSize(
children,
{ w -> maxIntrinsicHeight(w) },
{ h -> maxIntrinsicWidth(h) },
availableWidth,
orientation,
FlexOrientation.Vertical
)
}
}
}
private fun intrinsicSize(
children: List<Measurable>,
intrinsicMainSize: Measurable.(IntPx) -> IntPx,
intrinsicCrossSize: Measurable.(IntPx) -> IntPx,
crossAxisAvailable: IntPx,
flexOrientation: FlexOrientation,
intrinsicOrientation: FlexOrientation
) = if (flexOrientation == intrinsicOrientation) {
intrinsicMainAxisSize(children, intrinsicMainSize, crossAxisAvailable)
} else {
intrinsicCrossAxisSize(children, intrinsicCrossSize, intrinsicMainSize, crossAxisAvailable)
}
private fun intrinsicMainAxisSize(
children: List<Measurable>,
mainAxisSize: Measurable.(IntPx) -> IntPx,
crossAxisAvailable: IntPx
): IntPx {
var maxFlexibleSpace = 0.ipx
var inflexibleSpace = 0.ipx
var totalFlex = 0f
children.forEach { child ->
val flex = child.flex
val size = child.mainAxisSize(crossAxisAvailable)
if (flex == 0f) {
inflexibleSpace += size
} else if (flex > 0f) {
totalFlex += flex
maxFlexibleSpace = max(maxFlexibleSpace, size / flex)
}
}
return maxFlexibleSpace * totalFlex + inflexibleSpace
}
private fun intrinsicCrossAxisSize(
children: List<Measurable>,
mainAxisSize: Measurable.(IntPx) -> IntPx,
crossAxisSize: Measurable.(IntPx) -> IntPx,
mainAxisAvailable: IntPx
): IntPx {
var inflexibleSpace = 0.ipx
var crossAxisMax = 0.ipx
var totalFlex = 0f
children.forEach { child ->
val flex = child.flex
if (flex == 0f) {
val mainAxisSpace = child.mainAxisSize(IntPx.Infinity)
inflexibleSpace += mainAxisSpace
crossAxisMax = max(crossAxisMax, child.crossAxisSize(mainAxisSpace))
} else if (flex > 0f) {
totalFlex += flex
}
}
val flexSection = if (totalFlex == 0f) {
IntPx.Zero
} else {
max(mainAxisAvailable - inflexibleSpace, IntPx.Zero) / totalFlex
}
children.forEach { child ->
if (child.flex > 0f) {
crossAxisMax = max(crossAxisMax, child.crossAxisSize(flexSection * child.flex))
}
}
return crossAxisMax
}