blob: a62f5bbeb511dbeb2dfe163a9a80f9d4d0652a2e [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.core
import androidx.compose.Children
import androidx.compose.Composable
import androidx.compose.Compose
import androidx.compose.ambient
import androidx.compose.composer
import androidx.compose.compositionReference
import androidx.compose.memo
import androidx.compose.onPreCommit
import androidx.compose.trace
import androidx.compose.unaryPlus
internal typealias LayoutBlock = LayoutBlockReceiver.(List<Measurable>, Constraints) -> Unit
internal typealias IntrinsicMeasurementBlock = IntrinsicMeasurementsReceiver
.(List<Measurable>, IntPx) -> IntPx
internal val LayoutBlockStub: LayoutBlock = { _, _ -> }
internal val IntrinsicMeasurementBlockStub: IntrinsicMeasurementBlock = { _, _ -> 0.ipx }
internal class ComplexLayoutState(
internal var layoutBlock: LayoutBlock = LayoutBlockStub,
internal var minIntrinsicWidthBlock: IntrinsicMeasurementBlock = IntrinsicMeasurementBlockStub,
internal var maxIntrinsicWidthBlock: IntrinsicMeasurementBlock = IntrinsicMeasurementBlockStub,
internal var minIntrinsicHeightBlock: IntrinsicMeasurementBlock = IntrinsicMeasurementBlockStub,
internal var maxIntrinsicHeightBlock: IntrinsicMeasurementBlock = IntrinsicMeasurementBlockStub,
internal val density: Density
) : Measurable, Placeable(), MeasurableLayout {
override val parentData: Any?
get() {
var node = layoutNode.parent
val parentLayoutNode = layoutNode.parentLayoutNode
while (node != null && node !== parentLayoutNode) {
if (node is DataNode<*> && node.key === ParentDataKey) {
return node.value
}
node = node.parent
}
return null
}
internal var block: ComplexLayoutReceiver.() -> Unit = {}
internal var positioningBlock: PositioningBlockReceiver.() -> Unit = {}
internal val layoutBlockReceiver = LayoutBlockReceiver(this)
internal val intrinsicMeasurementsReceiver =
IntrinsicMeasurementsReceiver(this)
internal val positioningBlockReceiver = PositioningBlockReceiver()
internal val layoutNodeRef = Ref<LayoutNode>()
internal val layoutNode: LayoutNode
get() = layoutNodeRef.value!!
internal val childrenMeasurables: List<Measurable> get() =
ComplexLayoutStateMeasurablesList(layoutNode.childrenLayouts().map { it as Measurable })
private var measureIteration = 0L
override fun callMeasure(constraints: Constraints) { measure(constraints) }
override fun callLayout() {
placeChildren()
}
fun measure(constraints: Constraints): Placeable {
val iteration = layoutNode.owner?.measureIteration ?: 0L
if (measureIteration == iteration) {
throw IllegalStateException("measure() may not be called multiple times " +
"on the same Measurable")
}
measureIteration = iteration
if (layoutNode.constraints == constraints && !layoutNode.needsRemeasure) {
layoutNode.resize(layoutNode.width, layoutNode.height)
return this // we're already measured to this size, don't do anything
}
layoutNode.startMeasure()
layoutNode.constraints = constraints
layoutBlockReceiver.layoutBlock(childrenMeasurables, constraints)
layoutNode.endMeasure()
return this
}
fun minIntrinsicWidth(h: IntPx) =
minIntrinsicWidthBlock(intrinsicMeasurementsReceiver, childrenMeasurables, h)
fun maxIntrinsicWidth(h: IntPx) =
maxIntrinsicWidthBlock(intrinsicMeasurementsReceiver, childrenMeasurables, h)
fun minIntrinsicHeight(w: IntPx) =
minIntrinsicHeightBlock(intrinsicMeasurementsReceiver, childrenMeasurables, w)
fun maxIntrinsicHeight(w: IntPx) =
maxIntrinsicHeightBlock(intrinsicMeasurementsReceiver, childrenMeasurables, w)
internal fun placeChildren() {
if (layoutNode.needsRelayout) {
layoutNode.startLayout()
positioningBlockReceiver.apply { positioningBlock() }
dispatchOnPositionedCallbacks()
layoutNode.endLayout()
}
}
private fun dispatchOnPositionedCallbacks() {
// There are two types of callbacks:
// a) when the Layout is positioned - `onPositioned`
// b) when the child of the Layout is positioned - `onChildPositioned`
// To create LayoutNodeCoordinates only once here we will call callbacks from
// both `onPositioned` and 'onChildPositioned'.
val coordinates = LayoutNodeCoordinates(layoutNode)
walkOnPosition(layoutNode, coordinates)
walkOnChildPositioned(layoutNode, coordinates)
}
internal fun resize(width: IntPx, height: IntPx) {
layoutNode.resize(width, height)
}
private fun moveTo(x: IntPx, y: IntPx) {
layoutNode.moveTo(x, y)
}
override val width: IntPx get() = layoutNode.width
override val height: IntPx get() = layoutNode.height
override fun place(x: IntPx, y: IntPx) {
moveTo(x, y)
placeChildren()
}
companion object {
@Suppress("UNCHECKED_CAST")
private fun walkOnPosition(node: ComponentNode, coordinates: LayoutCoordinates) {
node.visitChildren { child ->
if (child !is LayoutNode) {
if (child is DataNode<*> && child.key === OnPositionedKey) {
val method = child.value as (LayoutCoordinates) -> Unit
method(coordinates)
}
walkOnPosition(child, coordinates)
}
}
}
@Suppress("UNCHECKED_CAST")
private fun walkOnChildPositioned(layoutNode: LayoutNode, coordinates: LayoutCoordinates) {
var node = layoutNode.parent
while (node != null && node !is LayoutNode) {
if (node is DataNode<*> && node.key === OnChildPositionedKey) {
val method = node.value as (LayoutCoordinates) -> Unit
method(coordinates)
}
node = node.parent
}
}
}
}
internal class ComplexLayoutStateMeasurablesList(
internal val measurables: List<Measurable>
) : List<Measurable> by (measurables.filter { it.parentData !is ChildrenEndParentData })
/**
* Receiver scope for the [ComplexLayout] lambda.
*/
class ComplexLayoutReceiver internal constructor(private val layoutState: ComplexLayoutState) {
fun layout(layoutBlock: LayoutBlock) {
layoutState.layoutBlock = layoutBlock
}
fun minIntrinsicWidth(minIntrinsicWidthBlock: IntrinsicMeasurementBlock) {
layoutState.minIntrinsicWidthBlock = minIntrinsicWidthBlock
}
fun maxIntrinsicWidth(maxIntrinsicWidthBlock: IntrinsicMeasurementBlock) {
layoutState.maxIntrinsicWidthBlock = maxIntrinsicWidthBlock
}
fun minIntrinsicHeight(minIntrinsicHeightBlock: IntrinsicMeasurementBlock) {
layoutState.minIntrinsicHeightBlock = minIntrinsicHeightBlock
}
fun maxIntrinsicHeight(maxIntrinsicHeightBlock: IntrinsicMeasurementBlock) {
layoutState.maxIntrinsicHeightBlock = maxIntrinsicHeightBlock
}
internal fun runBlock(block: ComplexLayoutReceiver.() -> Unit) {
layoutState.layoutBlock = LayoutBlockStub
layoutState.minIntrinsicWidthBlock = IntrinsicMeasurementBlockStub
layoutState.maxIntrinsicWidthBlock = IntrinsicMeasurementBlockStub
layoutState.minIntrinsicHeightBlock = IntrinsicMeasurementBlockStub
layoutState.maxIntrinsicHeightBlock = IntrinsicMeasurementBlockStub
block()
val noLambdaMessage = { subject: String ->
{ "No $subject lambda provided in ComplexLayout" }
}
require(layoutState.layoutBlock != LayoutBlockStub, noLambdaMessage("layout"))
require(
layoutState.minIntrinsicWidthBlock != IntrinsicMeasurementBlockStub,
noLambdaMessage("minIntrinsicWidth")
)
require(
layoutState.maxIntrinsicWidthBlock != IntrinsicMeasurementBlockStub,
noLambdaMessage("maxIntrinsicWidth")
)
require(
layoutState.minIntrinsicHeightBlock != IntrinsicMeasurementBlockStub,
noLambdaMessage("minIntrinsicHeight")
)
require(
layoutState.maxIntrinsicHeightBlock != IntrinsicMeasurementBlockStub,
noLambdaMessage("maxIntrinsicHeight")
)
}
}
/**
* [ComplexLayout] is the main core component for layout. It can be used to measure and position
* zero or more children.
*
* For a simpler version of [ComplexLayout], unaware of intrinsic measurements, see [Layout].
* For a widget able to define its content according to the incoming constraints,
* see [WithConstraints].
* @see Layout
* @see WithConstraints
*/
@Composable
fun ComplexLayout(
children: @Composable() () -> Unit,
@Children(composable = false) block: ComplexLayoutReceiver.() -> Unit
) {
val density = +ambientDensity()
val layoutState = +memo { ComplexLayoutState(density = density) }
layoutState.block = block
+onPreCommit {
layoutState.layoutNode.requestRemeasure()
}
<LayoutNode ref = layoutState.layoutNodeRef layout = layoutState>
children()
</LayoutNode>
ComplexLayoutReceiver(layoutState).runBlock(block)
}
/**
* Receiver scope for [ComplexLayout]'s intrinsic measurements lambdas.
*/
class IntrinsicMeasurementsReceiver internal constructor(
internal val layoutState: ComplexLayoutState
) : DensityReceiver {
override val density: Density
get() = layoutState.density
fun Measurable.minIntrinsicWidth(h: IntPx) =
(this as ComplexLayoutState).minIntrinsicWidth(h)
fun Measurable.maxIntrinsicWidth(h: IntPx) =
(this as ComplexLayoutState).maxIntrinsicWidth(h)
fun Measurable.minIntrinsicHeight(w: IntPx) =
(this as ComplexLayoutState).minIntrinsicHeight(w)
fun Measurable.maxIntrinsicHeight(w: IntPx) =
(this as ComplexLayoutState).maxIntrinsicHeight(w)
}
/**
* Receiver scope for [ComplexLayout]'s layout lambda.
*/
class LayoutBlockReceiver internal constructor(
internal val layoutState: ComplexLayoutState
) : DensityReceiver {
override val density: Density
get() = layoutState.density
fun Measurable.measure(constraints: Constraints): Placeable {
this as ComplexLayoutState
return this.measure(constraints)
}
fun layoutResult(
width: IntPx,
height: IntPx,
block: PositioningBlockReceiver.() -> Unit
) {
layoutState.resize(width, height)
layoutState.positioningBlock = block
}
fun Measurable.minIntrinsicWidth(h: IntPx) =
(this as ComplexLayoutState).minIntrinsicWidth(h)
fun Measurable.maxIntrinsicWidth(h: IntPx) =
(this as ComplexLayoutState).maxIntrinsicWidth(h)
fun Measurable.minIntrinsicHeight(w: IntPx) =
(this as ComplexLayoutState).minIntrinsicHeight(w)
fun Measurable.maxIntrinsicHeight(w: IntPx) =
(this as ComplexLayoutState).maxIntrinsicHeight(w)
}
internal class DummyPlaceable(override val width: IntPx, override val height: IntPx) : Placeable() {
override fun place(x: IntPx, y: IntPx) { }
}
/**
* A simpler version of [ComplexLayout], intrinsic dimensions do not need to be defined.
* If a layout of this [Layout] queries the intrinsics, an exception will be thrown.
* This [Layout] is built using public API on top of [ComplexLayout].
*/
@Composable
fun Layout(
children: @Composable() () -> Unit,
@Children(composable = false) layoutBlock: LayoutReceiver
.(measurables: List<Measurable>, constraints: Constraints) -> Unit
) {
trace("UI:Layout") {
ComplexLayout(children = children, block = {
layout { measurables, constraints ->
val layoutReceiver = LayoutReceiver(
layoutState,
{ m, c -> m.measure(c) }, /* measure lambda */
this::layoutResult,
density
)
layoutReceiver.layoutBlock(measurables, constraints)
}
minIntrinsicWidth { measurables, h ->
var intrinsicWidth = IntPx.Zero
val measureBoxReceiver = LayoutReceiver(layoutState, { m, c ->
val width = m.minIntrinsicWidth(c.maxHeight)
DummyPlaceable(width, h)
}, { width, _, _ -> intrinsicWidth = width }, density)
val constraints = Constraints(maxHeight = h)
layoutBlock(measureBoxReceiver, measurables, constraints)
intrinsicWidth
}
maxIntrinsicWidth { measurables, h ->
var intrinsicWidth = IntPx.Zero
val layoutReceiver = LayoutReceiver(layoutState, { m, c ->
val width = m.maxIntrinsicWidth(c.maxHeight)
DummyPlaceable(width, h)
}, { width, _, _ -> intrinsicWidth = width }, density)
val constraints = Constraints(maxHeight = h)
layoutBlock(layoutReceiver, measurables, constraints)
intrinsicWidth
}
minIntrinsicHeight { measurables, w ->
var intrinsicHeight = IntPx.Zero
val layoutReceiver = LayoutReceiver(layoutState, { m, c ->
val height = m.minIntrinsicHeight(c.maxWidth)
DummyPlaceable(w, height)
}, { _, height, _ -> intrinsicHeight = height }, density)
val constraints = Constraints(maxWidth = w)
layoutBlock(layoutReceiver, measurables, constraints)
intrinsicHeight
}
maxIntrinsicHeight { measurables, w ->
var intrinsicHeight = IntPx.Zero
val layoutReceiver = LayoutReceiver(layoutState, { m, c ->
val height = m.maxIntrinsicHeight(c.maxWidth)
DummyPlaceable(w, height)
}, { _, height, _ -> intrinsicHeight = height }, density)
val constraints = Constraints(maxWidth = w)
layoutBlock(layoutReceiver, measurables, constraints)
intrinsicHeight
}
})
}
}
/**
* Used by multi child [Layout] as parent data for the dummy [Layout] instances that mark
* the end of the [Measurable]s sequence corresponding to a particular child.
*/
internal data class ChildrenEndParentData(val children: @Composable() () -> Unit)
/**
* Temporary component that allows composing and indexing measurables of multiple composables.
* The logic here will be moved back to Layout, which will accept vararg children argument.
* TODO(popam): remove this when the new syntax is available
* With the new syntax, the API should support both:
* Layout(children) { measurables, constraints ->
* val placeables = measurables.map { it.measure(...) }
* ...
* }
* and
* Layout(header, cardContent, footer) { measurables, constraints ->
* val headerPlaceables = measurables[header].map { it.measure(...) }
* val cardContentPlaceables = measurables[cardContent].map { ... }
* val footerPlaceables = measurables[footer].map { ... }
* ...
* }
*/
@Composable
fun Layout(
childrenArray: Array<@Composable() () -> Unit>,
@Children(composable = false) layoutBlock: LayoutReceiver
.(measurables: List<Measurable>, constraints: Constraints) -> Unit
) {
val ChildrenEndMarker = @Composable { children: @Composable() () -> Unit ->
ParentData(data = ChildrenEndParentData(children)) {
Layout(layoutBlock={_, _ -> layout(0.ipx, 0.ipx){}}, children = {})
}
}
val children = @Composable {
val addMarkers = childrenArray.size > 1
childrenArray.forEach { childrenComposable ->
childrenComposable()
if (addMarkers) ChildrenEndMarker(childrenComposable)
}
}
Layout(layoutBlock = layoutBlock, children = children)
}
/**
* Receiver scope for the lambda of [Layout].
* Used to mask away intrinsics inside [Layout].
*/
class LayoutReceiver internal constructor(
internal val layoutState: ComplexLayoutState,
private val complexMeasure: (Measurable, Constraints) -> Placeable,
private val complexLayoutResult: (IntPx, IntPx, PositioningBlockReceiver.() -> Unit) -> Unit,
override val density: Density
) : DensityReceiver {
/**
* Returns all the [Measurable]s emitted for a particular children lambda.
* TODO(popam): finding measurables for each individual composable is O(n^2), consider improving
*/
operator fun List<Measurable>.get(children: () -> Unit): List<Measurable> {
if (this !is ComplexLayoutStateMeasurablesList) error("Invalid list of measurables")
val childrenMeasurablesEnd = measurables.indexOfFirst {
it.parentData is ChildrenEndParentData &&
(it.parentData as ChildrenEndParentData).children == children
}
val childrenMeasurablesStart = measurables.take(childrenMeasurablesEnd).indexOfLast {
it.parentData is ChildrenEndParentData
} + 1
return measurables.subList(childrenMeasurablesStart, childrenMeasurablesEnd)
}
/**
* Measure the child [Measurable] with a specific set of [Constraints]. The result
* is a [Placeable], which can be used inside the [layout] method to position the child.
*/
fun Measurable.measure(constraints: Constraints): Placeable = complexMeasure(this, constraints)
/**
* Sets the width and height of the current layout. The lambda is used to perform the
* calls to [Placeable.place], defining the positions of the children relative to the current
* layout.
*/
fun layout(width: IntPx, height: IntPx, block: PositioningBlockReceiver.() -> Unit) {
complexLayoutResult(width, height, block)
}
}
/**
* A widget that defines its own content according to the available space, based on the incoming
* constraints. Example usage:
*
* WithConstraints { constraints ->
* if (constraints.maxWidth < 100.ipx) {
* Icon()
* } else {
* Row {
* Icon()
* IconDescription()
* }
* }
* }
*
* The widget will compose the given children, and will position the resulting layout widgets
* in a parent [Layout]. The widget will be as small as possible such that it can fit its
* children. If the composition yields multiple layout children, these will be all placed at the
* top left of the WithConstraints, so consider wrapping them in an additional common
* parent if different positioning is preferred.
*
* Please note that using this widget might be a performance hit, so please use with care.
*/
@Composable
fun WithConstraints(@Children children: @Composable() (Constraints) -> Unit) {
val ref = +compositionReference()
val context = +ambient(ContextAmbient)
Layout(
layoutBlock = { _, constraints ->
val root = layoutState.layoutNode
// Start subcomposition from the current node.
Compose.composeInto(
root,
context,
ref
) {
children(p1 = constraints)
}
// Measure the obtained children and compute our size.
val measurables = layoutState.childrenMeasurables
val placeables = measurables.map { it.measure(constraints) }
val layoutSize = constraints.constrain(IntPxSize(
placeables.map { it.width }.maxBy { it.value } ?: IntPx.Zero,
placeables.map { it.height }.maxBy { it.value } ?: IntPx.Zero
))
layout(layoutSize.width, layoutSize.height) {
placeables.forEach { placeable ->
placeable.place(IntPx.Zero, IntPx.Zero)
}
}
},
children={})
}
private val OnPositionedKey = DataNodeKey<(LayoutCoordinates) -> Unit>("Compose:OnPositioned")
private val OnChildPositionedKey =
DataNodeKey<(LayoutCoordinates) -> Unit>("Compose:OnChildPositioned")
/**
* [onPositioned] callback will be called with the final LayoutCoordinates of the parent
* MeasureBox after measuring.
* Note that it will be called after a composition when the coordinates are finalized.
*
* Usage example:
* Column {
* Item1()
* Item2()
* OnPositioned (onPositioned={ coordinates ->
* // This coordinates contain bounds of the Column within it's parent Layout.
* // Store it if you want to use it later f.e. when a touch happens.
* })
* }
*/
@Composable
fun OnPositioned(
onPositioned: (coordinates: LayoutCoordinates) -> Unit
) {
<DataNode key=OnPositionedKey value=onPositioned/>
}
/**
* [onPositioned] callback will be called with the final LayoutCoordinates of the children
* MeasureBox(es) after measuring.
* Note that it will be called after a composition when the coordinates are finalized.
*
* Usage example:
* OnChildPositioned (onPositioned={ coordinates ->
* // This coordinates contain bounds of the Item within it's parent Layout.
* // Store it if you want to use it later f.e. when a touch happens.
* }) {
* Item()
* }
* }
*/
@Composable
fun OnChildPositioned(
onPositioned: (coordinates: LayoutCoordinates) -> Unit,
@Children children: @Composable() () -> Unit
) {
<DataNode key=OnChildPositionedKey value=onPositioned>
<children/>
</DataNode>
}