[go: nahoru, domu]

blob: 3cb26c8e132df041ba21f4a01b3c93ba5093228b [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.state
import androidx.ui.core.Alignment
import androidx.ui.core.Constraints
import androidx.ui.core.IntrinsicMeasurable
import androidx.ui.core.IntrinsicMeasureBlock
import androidx.ui.core.Layout
import androidx.ui.core.ParentData
import androidx.ui.core.Placeable
import androidx.ui.core.constrain
import androidx.ui.unit.Density
import androidx.ui.unit.DensityScope
import androidx.ui.unit.Dp
import androidx.ui.unit.IntPx
import androidx.ui.unit.IntPxSize
import androidx.ui.unit.isFinite
import androidx.ui.unit.max
import androidx.ui.unit.min
import androidx.ui.unit.withDensity
/**
* Collects information about the children of a [Table] when
* its body is executed with a [TableChildren] as argument.
*/
@LayoutScopeMarker
class TableChildren internal constructor() {
internal val tableChildren = mutableListOf<@Composable() () -> Unit>()
internal val tableDecorationsOverlay = mutableListOf<TableDecoration>()
internal val tableDecorationsUnderlay = mutableListOf<TableDecoration>()
/**
* Creates a new row in the [Table] with the specified content.
*/
fun tableRow(children: @Composable() () -> Unit) {
val rowIndex = tableChildren.size
tableChildren += {
ParentData(data = TableChildData(rowIndex), children = children)
}
}
/**
* Adds a decoration which will be placed either above or below the content of the [Table].
* This can be either a component, such as Layout or SizedRectangle, or a Draw composable.
* Note that decorations are measured with tight constraints to fill the size of the [Table],
* and the offsets of each row and column of the [Table] are available inside the body of this.
*
* Example usage:
*
* @sample androidx.ui.layout.samples.TableWithDecorations
*
* @param overlay Whether the decoration is placed above (true) or below (false) the content.
*/
fun tableDecoration(overlay: Boolean, decoration: TableDecoration) {
if (overlay) {
tableDecorationsOverlay += decoration
} else {
tableDecorationsUnderlay += decoration
}
}
}
/**
* Collects the vertical/horizontal offsets of each row/column of a [Table] that are available
* to a [TableDecoration] when its body is executed on a [TableDecorationChildren] instance.
*/
data class TableDecorationChildren internal constructor(
val verticalOffsets: List<IntPx>,
val horizontalOffsets: List<IntPx>
)
typealias TableDecoration = @Composable TableDecorationChildren.() -> Unit
/**
* Parent data associated with children to assign a row group.
*/
private data class TableChildData(val rowIndex: Int)
private val IntrinsicMeasurable.rowIndex get() = (parentData as? TableChildData)?.rowIndex
/**
* Used to specify the size of a [Table]'s column.
*/
abstract class TableColumnWidth private constructor(internal val flexValue: Float) {
/**
* Returns the ideal width of the column.
*
* Note that the column might be wider than this if it is flexible.
*/
abstract fun preferredWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density
): IntPx
/**
* Returns the minimum intrinsic width of the column for the given height.
*
* This is used for computing the table's intrinsic dimensions. Defaults to [preferredWidth].
*/
open fun minIntrinsicWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density,
availableHeight: IntPx
): IntPx {
return preferredWidth(cells, containerWidth, density)
}
/**
* Returns the minimum intrinsic width of the column for the given height.
*
* This is used for computing the table's intrinsic dimensions. Defaults to [preferredWidth].
*/
open fun maxIntrinsicWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density,
availableHeight: IntPx
): IntPx {
return preferredWidth(cells, containerWidth, density)
}
/**
* An inflexible column has a fixed size which is computed by [preferredWidth].
*/
abstract class Inflexible : TableColumnWidth(flexValue = 0f) {
/**
* Creates a column width specification which defaults to the width specified by the
* [preferredWidth] of the receiver, but may also grow by taking a part of the remaining
* space according to the given [flex] once all the inflexible columns have been measured.
*/
fun flexible(flex: Float): TableColumnWidth = Flexible(flex, this)
}
private data class Flexible(val flex: Float, val other: Inflexible) : TableColumnWidth(flex) {
override fun preferredWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density
): IntPx {
return other.preferredWidth(cells, containerWidth, density)
}
override fun minIntrinsicWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density,
availableHeight: IntPx
): IntPx {
return other.minIntrinsicWidth(cells, containerWidth, density, availableHeight)
}
override fun maxIntrinsicWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density,
availableHeight: IntPx
): IntPx {
return other.maxIntrinsicWidth(cells, containerWidth, density, availableHeight)
}
}
/**
* Sizes the column by taking a part of the remaining space according to [flex] once all the
* inflexible columns have been measured. Note that this defaults to 0 if no space is available.
*/
data class Flex(@FloatRange(from = 0.0) private val flex: Float) : TableColumnWidth(flex) {
override fun preferredWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density
): IntPx {
return IntPx.Zero
}
}
/**
* Sizes the column to the width of the widest child in that column.
*
* Note that in order to compute their preferred widths, the children will be measured with
* infinite width constraints, which means that some of them may have infinite width. For a
* wrap content behaviour that avoids this, use [MinIntrinsicWidth] or [MaxIntrinsicWidth].
*/
object Wrap : Inflexible() {
override fun preferredWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density
): IntPx {
return cells.fold(IntPx.Zero) { acc, cell ->
max(acc, cell.preferredWidth())
}
}
override fun minIntrinsicWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density,
availableHeight: IntPx
): IntPx {
return cells.fold(IntPx.Zero) { acc, cell ->
max(acc, cell.minIntrinsicWidth(availableHeight / cells.size))
}
}
override fun maxIntrinsicWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density,
availableHeight: IntPx
): IntPx {
return cells.fold(IntPx.Zero) { acc, cell ->
max(acc, cell.maxIntrinsicWidth(availableHeight / cells.size))
}
}
}
/**
* Sizes the column to the largest of the minimum intrinsic widths of the children in that
* column (i.e. the minimum width such that children can layout/paint themselves correctly).
*
* Note that this is a very expensive way to size a column. For a wrap content behaviour that
* skips the intrinsic measurements which compute the column width before measuring, use [Wrap].
*/
object MinIntrinsic : Inflexible() {
override fun preferredWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density
): IntPx {
return cells.fold(IntPx.Zero) { acc, cell ->
max(acc, cell.minIntrinsicWidth(IntPx.Infinity))
}
}
override fun minIntrinsicWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density,
availableHeight: IntPx
): IntPx {
return cells.fold(IntPx.Zero) { acc, cell ->
max(acc, cell.minIntrinsicWidth(availableHeight / cells.size))
}
}
override fun maxIntrinsicWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density,
availableHeight: IntPx
): IntPx {
return cells.fold(IntPx.Zero) { acc, cell ->
max(acc, cell.maxIntrinsicWidth(availableHeight / cells.size))
}
}
}
/**
* Sizes the column to the largest of the maximum intrinsic widths of the children in that
* column (i.e. the maximum width such that children can occupy the entire space without waste).
*
* Note that this is a very expensive way to size a column. For a wrap content behaviour that
* skips the intrinsic measurements which compute the column width before measuring, use [Wrap].
*/
object MaxIntrinsic : Inflexible() {
override fun preferredWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density
): IntPx {
return cells.fold(IntPx.Zero) { acc, cell ->
max(acc, cell.maxIntrinsicWidth(IntPx.Infinity))
}
}
override fun minIntrinsicWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density,
availableHeight: IntPx
): IntPx {
return cells.fold(IntPx.Zero) { acc, cell ->
max(acc, cell.minIntrinsicWidth(availableHeight / cells.size))
}
}
override fun maxIntrinsicWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density,
availableHeight: IntPx
): IntPx {
return cells.fold(IntPx.Zero) { acc, cell ->
max(acc, cell.maxIntrinsicWidth(availableHeight / cells.size))
}
}
}
/**
* Sizes the column to a specific width.
*/
data class Fixed(private val width: Dp) : Inflexible() {
override fun preferredWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density
): IntPx {
return withDensity(density) { width.toIntPx() }
}
}
/**
* Sizes the column to a fraction of the table's maximum width constraint.
*
* Note that this defaults to 0 if the maximum width constraints is infinite.
*/
data class Fraction(
@FloatRange(from = 0.0, to = 1.0) private val fraction: Float
) : Inflexible() {
override fun preferredWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density
): IntPx {
return if (containerWidth.isFinite()) {
containerWidth * fraction
} else {
IntPx.Zero
}
}
}
/**
* Sizes the column to the size that is the minimum of two column width specifications.
*
* Both specifications are evaluated, so if either specification is expensive, so is this.
*/
data class Min(private val a: Inflexible, private val b: Inflexible) : Inflexible() {
override fun preferredWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density
): IntPx {
return min(
a.preferredWidth(cells, containerWidth, density),
b.preferredWidth(cells, containerWidth, density)
)
}
override fun minIntrinsicWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density,
availableHeight: IntPx
): IntPx {
return min(
a.minIntrinsicWidth(cells, containerWidth, density, availableHeight),
b.minIntrinsicWidth(cells, containerWidth, density, availableHeight)
)
}
override fun maxIntrinsicWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density,
availableHeight: IntPx
): IntPx {
return min(
a.maxIntrinsicWidth(cells, containerWidth, density, availableHeight),
b.maxIntrinsicWidth(cells, containerWidth, density, availableHeight)
)
}
}
/**
* Sizes the column to the size that is the maximum of two column width specifications.
*
* Both specifications are evaluated, so if either specification is expensive, so is this.
*/
data class Max(private val a: Inflexible, private val b: Inflexible) : Inflexible() {
override fun preferredWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density
): IntPx {
return max(
a.preferredWidth(cells, containerWidth, density),
b.preferredWidth(cells, containerWidth, density)
)
}
override fun minIntrinsicWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density,
availableHeight: IntPx
): IntPx {
return max(
a.minIntrinsicWidth(cells, containerWidth, density, availableHeight),
b.minIntrinsicWidth(cells, containerWidth, density, availableHeight)
)
}
override fun maxIntrinsicWidth(
cells: List<TableMeasurable>,
containerWidth: IntPx,
density: Density,
availableHeight: IntPx
): IntPx {
return max(
a.maxIntrinsicWidth(cells, containerWidth, density, availableHeight),
b.maxIntrinsicWidth(cells, containerWidth, density, availableHeight)
)
}
}
}
/**
* Collects measurements for the children of a column that
* are available to implementations of [TableColumnWidth].
*/
data class TableMeasurable internal constructor(
/**
* Computes the preferred width of the child by measuring with infinite constraints.
*/
val preferredWidth: () -> IntPx,
/**
* Computes the minimum intrinsic width of the child for the given available height.
*/
val minIntrinsicWidth: (IntPx) -> IntPx,
/**
* Computes the maximum intrinsic width of the child for the given available height.
*/
val maxIntrinsicWidth: (IntPx) -> IntPx
)
/**
* Layout model that arranges its children into rows and columns.
*
* Example usage:
*
* @sample androidx.ui.layout.samples.SimpleTable
*
* @sample androidx.ui.layout.samples.TableWithDifferentColumnWidths
*/
@Composable
fun Table(
columns: Int,
alignment: (columnIndex: Int) -> Alignment = { Alignment.TopLeft },
columnWidth: (columnIndex: Int) -> TableColumnWidth = { TableColumnWidth.Flex(1f) },
children: TableChildren.() -> Unit
) {
var verticalOffsets by state { emptyArray<IntPx>() }
var horizontalOffsets by state { emptyArray<IntPx>() }
val tableChildren: @Composable() () -> Unit = with(TableChildren()) {
apply(children)
val composable = @Composable {
val needDecorations = tableDecorationsUnderlay.isNotEmpty() ||
tableDecorationsOverlay.isNotEmpty()
val hasOffsets = verticalOffsets.isNotEmpty() && horizontalOffsets.isNotEmpty()
val decorationsScope = if (needDecorations && hasOffsets) {
TableDecorationChildren(
verticalOffsets = verticalOffsets.toList(),
horizontalOffsets = horizontalOffsets.toList()
)
} else {
null
}
if (decorationsScope != null) {
tableDecorationsUnderlay.forEach { decorationsScope.it() }
}
tableChildren.forEach { it() }
if (decorationsScope != null) {
tableDecorationsOverlay.forEach { decorationsScope.it() }
}
}
composable
}
Layout(
tableChildren,
minIntrinsicWidthMeasureBlock = MinIntrinsicWidthMeasureBlock(columns, columnWidth),
minIntrinsicHeightMeasureBlock = MinIntrinsicHeightMeasureBlock(columns, columnWidth),
maxIntrinsicWidthMeasureBlock = MaxIntrinsicWidthMeasureBlock(columns, columnWidth),
maxIntrinsicHeightMeasureBlock = MaxIntrinsicHeightMeasureBlock(columns, columnWidth)
) { measurables, constraints ->
val rowMeasurables = measurables.filter { it.rowIndex != null }.groupBy { it.rowIndex }
val rows = rowMeasurables.size
fun measurableAt(row: Int, column: Int) = rowMeasurables[row]?.getOrNull(column)
val placeables = Array(rows) { arrayOfNulls<Placeable>(columns) }
// Compute column widths and collect flex information.
var totalFlex = 0f
var availableSpace =
if (constraints.maxWidth.isFinite()) constraints.maxWidth else constraints.minWidth
val columnWidths = Array(columns) { IntPx.Zero }
for (column in 0 until columns) {
val spec = columnWidth(column)
val cells = List(rows) { row ->
TableMeasurable(
preferredWidth = {
placeables[row][column]?.width
?: measurableAt(row, column)?.measure(Constraints())
?.also { placeables[row][column] = it }?.width ?: IntPx.Zero
},
minIntrinsicWidth = {
measurableAt(row, column)?.minIntrinsicWidth(it) ?: IntPx.Zero
},
maxIntrinsicWidth = {
measurableAt(row, column)?.maxIntrinsicWidth(it) ?: IntPx.Zero
}
)
}
columnWidths[column] = spec.preferredWidth(cells, constraints.maxWidth, density)
availableSpace -= columnWidths[column]
totalFlex += spec.flexValue
}
// Grow flexible columns to fill available horizontal space.
if (totalFlex > 0 && availableSpace > IntPx.Zero) {
for (column in 0 until columns) {
val spec = columnWidth(column)
if (spec.flexValue > 0) {
columnWidths[column] += availableSpace * (spec.flexValue / totalFlex)
}
}
}
// Measure the remaining children and calculate row heights.
val rowHeights = Array(rows) { IntPx.Zero }
for (row in 0 until rows) {
for (column in 0 until columns) {
if (placeables[row][column] == null) {
placeables[row][column] = measurableAt(row, column)?.measure(
Constraints(minWidth = IntPx.Zero, maxWidth = columnWidths[column])
)
}
rowHeights[row] =
max(rowHeights[row], placeables[row][column]?.height ?: IntPx.Zero)
}
}
// Compute row/column offsets.
val rowOffsets = Array(rows + 1) { IntPx.Zero }
val columnOffsets = Array(columns + 1) { IntPx.Zero }
for (row in 0 until rows) {
rowOffsets[row + 1] = rowOffsets[row] + rowHeights[row]
}
for (column in 0 until columns) {
columnOffsets[column + 1] = columnOffsets[column] + columnWidths[column]
}
if (!verticalOffsets.contentEquals(rowOffsets)) {
verticalOffsets = rowOffsets
}
if (!horizontalOffsets.contentEquals(columnOffsets)) {
horizontalOffsets = columnOffsets
}
// TODO(calintat): Do something when these do not satisfy constraints.
val tableSize =
constraints.constrain(IntPxSize(columnOffsets[columns], rowOffsets[rows]))
layout(tableSize.width, tableSize.height) {
for (row in 0 until rows) {
for (column in 0 until columns) {
placeables[row][column]?.let {
val position = alignment(column).align(
IntPxSize(
width = columnWidths[column] - it.width,
height = rowHeights[row] - it.height
)
)
it.place(
x = columnOffsets[column] + position.x,
y = rowOffsets[row] + position.y
)
}
}
}
val decorationConstraints =
Constraints.fixed(tableSize.width, tableSize.height)
measurables.filter { it.rowIndex == null }.forEach {
it.measure(decorationConstraints).place(IntPx.Zero, IntPx.Zero)
}
}
}
}
private val MinIntrinsicWidthMeasureBlock:
(Int, (Int) -> TableColumnWidth) -> IntrinsicMeasureBlock =
{ columns, columnWidth ->
{ measurables, availableHeight ->
intrinsicWidth(
columns = columns,
columnWidth = columnWidth,
children = measurables,
availableHeight = availableHeight,
minimise = true
)
}
}
private val MinIntrinsicHeightMeasureBlock:
(Int, (Int) -> TableColumnWidth) -> IntrinsicMeasureBlock =
{ columns, columnWidth ->
{ measurables, availableWidth ->
intrinsicHeight(
columns = columns,
columnWidth = columnWidth,
children = measurables,
availableWidth = availableWidth,
intrinsicHeight = { w -> minIntrinsicHeight(w) }
)
}
}
private val MaxIntrinsicWidthMeasureBlock:
(Int, (Int) -> TableColumnWidth) -> IntrinsicMeasureBlock =
{ columns, columnWidth ->
{ measurables, availableHeight ->
intrinsicWidth(
columns = columns,
columnWidth = columnWidth,
children = measurables,
availableHeight = availableHeight,
minimise = false
)
}
}
private val MaxIntrinsicHeightMeasureBlock:
(Int, (Int) -> TableColumnWidth) -> IntrinsicMeasureBlock =
{ columns, columnWidth ->
{ measurables, availableWidth ->
intrinsicHeight(
columns = columns,
columnWidth = columnWidth,
children = measurables,
availableWidth = availableWidth,
intrinsicHeight = { w -> maxIntrinsicHeight(w) }
)
}
}
private fun DensityScope.intrinsicWidth(
columns: Int,
columnWidth: (columnIndex: Int) -> TableColumnWidth,
children: List<IntrinsicMeasurable>,
availableHeight: IntPx,
minimise: Boolean
): IntPx {
val measurables = children.filter { it.rowIndex != null }.groupBy { it.rowIndex }
val rows = measurables.size
fun measurableAt(row: Int, column: Int) = measurables[row]?.getOrNull(column)
var totalFlex = 0f
var flexibleSpace = IntPx.Zero
var inflexibleSpace = IntPx.Zero
for (column in 0 until columns) {
val spec = columnWidth(column)
val cells = List(rows) { row ->
TableMeasurable(
preferredWidth = { IntPx.Zero },
minIntrinsicWidth = {
measurableAt(row, column)?.minIntrinsicWidth(it) ?: IntPx.Zero
},
maxIntrinsicWidth = {
measurableAt(row, column)?.maxIntrinsicWidth(it) ?: IntPx.Zero
}
)
}
val width = if (minimise) {
spec.minIntrinsicWidth(cells, IntPx.Infinity, density, availableHeight)
} else {
spec.maxIntrinsicWidth(cells, IntPx.Infinity, density, availableHeight)
}
if (spec.flexValue <= 0) {
inflexibleSpace += width
} else {
totalFlex += spec.flexValue
flexibleSpace = max(flexibleSpace, width / spec.flexValue)
}
}
return flexibleSpace * totalFlex + inflexibleSpace
}
private fun DensityScope.intrinsicHeight(
columns: Int,
columnWidth: (columnIndex: Int) -> TableColumnWidth,
children: List<IntrinsicMeasurable>,
availableWidth: IntPx,
intrinsicHeight: IntrinsicMeasurable.(IntPx) -> IntPx
): IntPx {
val measurables = children.filter { it.rowIndex != null }.groupBy { it.rowIndex }
val rows = measurables.size
fun measurableAt(row: Int, column: Int) = measurables[row]?.getOrNull(column)
// Compute column widths and collect flex information.
var totalFlex = 0f
var availableSpace = availableWidth
val columnWidths = Array(columns) { IntPx.Zero }
for (column in 0 until columns) {
val spec = columnWidth(column)
val cells = List(rows) { row ->
TableMeasurable(
preferredWidth = { IntPx.Zero },
minIntrinsicWidth = {
measurableAt(row, column)?.minIntrinsicWidth(it) ?: IntPx.Zero
},
maxIntrinsicWidth = {
measurableAt(row, column)?.maxIntrinsicWidth(it) ?: IntPx.Zero
}
)
}
columnWidths[column] =
spec.maxIntrinsicWidth(cells, availableWidth, density, IntPx.Infinity)
availableSpace -= columnWidths[column]
totalFlex += spec.flexValue
}
// Grow flexible columns to fill available horizontal space.
if (totalFlex > 0 && availableSpace > IntPx.Zero) {
for (column in 0 until columns) {
val spec = columnWidth(column)
if (spec.flexValue > 0) {
columnWidths[column] += availableSpace * (spec.flexValue / totalFlex)
}
}
}
// Calculate row heights and table height.
return (0 until rows).fold(IntPx.Zero) { tableHeight, row ->
val rowHeight = (0 until columns).fold(IntPx.Zero) { rowHeight, column ->
max(
rowHeight,
measurableAt(row, column)?.intrinsicHeight(columnWidths[column])
?: IntPx.Zero
)
}
tableHeight + rowHeight
}
}