/*
* Copyright 2020 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.compose.ui.node
import androidx.compose.runtime.collection.MutableVector
import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.node.LayoutNode.Companion.NotPlacedPlaceOrder
import androidx.compose.ui.node.LayoutNode.LayoutState
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastForEach
/**
* This class works as a layout delegate for [LayoutNode]. It delegates all the measure/layout
* requests to its [measurePassDelegate] and [lookaheadPassDelegate] depending on whether the
* request is specific to lookahead.
*/
internal class LayoutNodeLayoutDelegate(
private val layoutNode: LayoutNode,
) {
val outerCoordinator: NodeCoordinator
get() = layoutNode.nodes.outerCoordinator
val lastConstraints: Constraints?
get() = measurePassDelegate.lastConstraints
val lastLookaheadConstraints: Constraints?
get() = lookaheadPassDelegate?.lastConstraints
internal val height: Int
get() = measurePassDelegate.height
internal val width: Int
get() = measurePassDelegate.width
/**
* The layout state the node is currently in.
*
* The mutation of [layoutState] is confined to [LayoutNodeLayoutDelegate], and is therefore
* read-only outside this class. This makes the state machine easier to reason about.
*/
internal var layoutState = LayoutState.Idle
private set
/**
* Tracks whether another measure pass is needed for the LayoutNodeLayoutDelegate.
* Mutation to [measurePending] is confined to LayoutNodeLayoutDelegate. It can only be set true
* from outside of this class via [markMeasurePending]. It is cleared (i.e. set false) during
* the measure pass (i.e. in [performMeasure]).
*/
internal var measurePending: Boolean = false
private set
/**
* Tracks whether another layout pass is needed for the LayoutNodeLayoutDelegate.
* Mutation to [layoutPending] is confined to this class. It can only be set true from outside
* of this class via [markLayoutPending]. It is cleared (i.e. set false) during the layout pass
* (i.e. in [MeasurePassDelegate.layoutChildren]).
*/
internal var layoutPending: Boolean = false
private set
/**
* Tracks whether another layout pass is needed for the LayoutNodeLayoutDelegate
* for the purposes of calculating alignment lines. After calculating alignment lines, if
* the [Placeable.PlacementScope.coordinates] have been accessed, there is no need to
* rerun layout for further alignment lines checks, but [layoutPending] will indicate
* that the normal placement still needs to be run.
*/
private var layoutPendingForAlignment = false
/**
* Tracks whether another lookahead measure pass is needed for the LayoutNodeLayoutDelegate.
* Mutation to [lookaheadMeasurePending] is confined to LayoutNodeLayoutDelegate. It can only
* be set true from outside of this class via [markLookaheadMeasurePending]. It is cleared
* (i.e. set false) during the lookahead measure pass (i.e. in [performLookaheadMeasure]).
*/
internal var lookaheadMeasurePending: Boolean = false
private set
/**
* Tracks whether another lookahead layout pass is needed for the LayoutNodeLayoutDelegate.
* Mutation to [lookaheadLayoutPending] is confined to this class. It can only be set true
* from outside of this class via [markLookaheadLayoutPending]. It is cleared (i.e. set false)
* during the layout pass (i.e. in [LookaheadPassDelegate.layoutChildren]).
*/
internal var lookaheadLayoutPending: Boolean = false
private set
/**
* Tracks whether another lookahead layout pass is needed for the LayoutNodeLayoutDelegate
* for the purposes of calculating alignment lines. After calculating alignment lines, if
* the [Placeable.PlacementScope.coordinates] have been accessed, there is no need to
* rerun layout for further alignment lines checks, but [lookaheadLayoutPending] will indicate
* that the normal placement still needs to be run.
*/
private var lookaheadLayoutPendingForAlignment = false
/**
* The counter on a parent node which is used by its children to understand the order in
* which they were placed in the lookahead pass.
*/
private var nextChildLookaheadPlaceOrder: Int = 0
/**
* The counter on a parent node which is used by its children to understand the order in
* which they were placed in the main pass.
*/
private var nextChildPlaceOrder: Int = 0
/**
* Marks the layoutNode dirty for another layout pass.
*/
internal fun markLayoutPending() {
layoutPending = true
layoutPendingForAlignment = true
}
/**
* Marks the layoutNode dirty for another measure pass.
*/
internal fun markMeasurePending() {
measurePending = true
}
/**
* Marks the layoutNode dirty for another lookahead layout pass.
*/
internal fun markLookaheadLayoutPending() {
lookaheadLayoutPending = true
lookaheadLayoutPendingForAlignment = true
}
/**
* Marks the layoutNode dirty for another lookahead measure pass.
*/
internal fun markLookaheadMeasurePending() {
lookaheadMeasurePending = true
}
internal val alignmentLinesOwner: AlignmentLinesOwner
get() = measurePassDelegate
internal val lookaheadAlignmentLinesOwner: AlignmentLinesOwner?
get() = lookaheadPassDelegate
/**
* This is used to track when the [Placeable.PlacementScope.coordinates] have been
* accessed while placement is run. When the coordinates are accessed during an alignment
* line query, it indicates that the placement is not final and must be run again so that
* the correct positioning is done. If the coordinates are not accessed during an alignment
* lines query (and it isn't just a [LookaheadCapablePlaceable.isShallowPlacing]),
* then the placement can be considered final and doesn't have to be run again.
*
* Also, if coordinates are accessed during placement, then a change in parent coordinates
* requires placement to be run again.
*/
var coordinatesAccessedDuringPlacement = false
set(value) {
val oldValue = field
if (oldValue != value) {
field = value
if (value) {
childrenAccessingCoordinatesDuringPlacement++
} else {
childrenAccessingCoordinatesDuringPlacement--
}
}
}
/**
* The number of children with [coordinatesAccessedDuringPlacement] or have
* descendants with [coordinatesAccessedDuringPlacement]. This also includes
* this, if [coordinatesAccessedDuringPlacement] is `true`.
*/
var childrenAccessingCoordinatesDuringPlacement = 0
set(value) {
val oldValue = field
field = value
if ((oldValue == 0) != (value == 0)) {
// A child is either newly listening for coordinates or stopped listening
val parentLayoutDelegate = layoutNode.parent?.layoutDelegate
if (parentLayoutDelegate != null) {
if (value == 0) {
parentLayoutDelegate.childrenAccessingCoordinatesDuringPlacement--
} else {
parentLayoutDelegate.childrenAccessingCoordinatesDuringPlacement++
}
}
}
}
/**
* measurePassDelegate manages the measure/layout and alignmentLine related queries for the
* actual measure/layout pass.
*/
internal val measurePassDelegate = MeasurePassDelegate()
/**
* lookaheadPassDelegate manages the measure/layout and alignmentLine related queries for the
* lookahead pass.
*/
internal var lookaheadPassDelegate: LookaheadPassDelegate? = null
private set
/**
* [MeasurePassDelegate] manages the measure/layout and alignmentLine related queries for the
* actual measure/layout pass.
*/
inner class MeasurePassDelegate : Measurable, Placeable(), AlignmentLinesOwner {
/**
* Is true during [replace] invocation. Helps to differentiate between the cases when our
* parent is measuring us during the measure block, and when we are remeasured individually
* because of some change. This could be useful to know if we need to record the placing
* order.
*/
private var relayoutWithoutParentInProgress: Boolean = false
/**
* The value [placeOrder] had during the previous parent `layoutChildren`. Helps us to
* understand if the order did change.
*/
internal var previousPlaceOrder: Int = NotPlacedPlaceOrder
private set
/**
* The order in which this node was placed by its parent during the previous `layoutChildren`.
* Before the placement the order is set to [NotPlacedPlaceOrder] to all the children. Then
* every placed node assigns this variable to parent's LayoutNodeLayoutDelegate's
* nextChildPlaceOrder and increments this counter. Not placed items will still have
* [NotPlacedPlaceOrder] set.
*/
internal var placeOrder: Int = NotPlacedPlaceOrder
private set
private var measuredOnce = false
private var placedOnce = false
val lastConstraints: Constraints?
get() = if (measuredOnce) {
measurementConstraints
} else {
null
}
internal var measuredByParent: LayoutNode.UsageByParent = LayoutNode.UsageByParent.NotUsed
internal var duringAlignmentLinesQuery = false
private var lastPosition: IntOffset = IntOffset.Zero
private var lastLayerBlock: (GraphicsLayerScope.() -> Unit)? = null
private var lastZIndex: Float = 0f
private var parentDataDirty: Boolean = true
override var parentData: Any? = null
private set
/**
* Whether or not this [LayoutNode] and all of its parents have been placed in the hierarchy.
*/
override var isPlaced: Boolean = false
internal set
override val innerCoordinator: NodeCoordinator
get() = layoutNode.innerCoordinator
override val alignmentLines: AlignmentLines = LayoutNodeAlignmentLines(this)
private val _childDelegates = MutableVector<MeasurePassDelegate>()
internal var childDelegatesDirty: Boolean = true
internal val childDelegates: List<MeasurePassDelegate>
get() {
// Update the children list first so we know whether the cached list is
// reusable.
layoutNode.updateChildrenIfDirty()
if (!childDelegatesDirty) return _childDelegates.asMutableList()
layoutNode.updateChildMeasurables(_childDelegates) {
it.layoutDelegate.measurePassDelegate
}
childDelegatesDirty = false
return _childDelegates.asMutableList()
}
override fun layoutChildren() {
alignmentLines.recalculateQueryOwner()
if (layoutPending) {
onBeforeLayoutChildren()
}
// as a result of the previous operation we can figure out a child has been resized
// and we need to be remeasured, not relaid out
if (layoutPendingForAlignment ||
(!duringAlignmentLinesQuery && !innerCoordinator.isPlacingForAlignment &&
layoutPending)
) {
layoutPending = false
val oldLayoutState = layoutState
layoutState = LayoutState.LayingOut
with(layoutNode) {
val owner = requireOwner()
owner.snapshotObserver.observeLayoutSnapshotReads(
this,
affectsLookahead = false
) {
clearPlaceOrder()
forEachChildAlignmentLinesOwner {
it.alignmentLines.usedDuringParentLayout = false
}
innerCoordinator.measureResult.placeChildren()
checkChildrenPlaceOrderForUpdates()
forEachChildAlignmentLinesOwner {
it.alignmentLines.previousUsedDuringParentLayout =
it.alignmentLines.usedDuringParentLayout
}
}
}
layoutState = oldLayoutState
if (innerCoordinator.isPlacingForAlignment &&
coordinatesAccessedDuringPlacement
) {
requestLayout()
}
layoutPendingForAlignment = false
}
if (alignmentLines.usedDuringParentLayout) {
alignmentLines.previousUsedDuringParentLayout = true
}
if (alignmentLines.dirty && alignmentLines.required) alignmentLines.recalculate()
}
private fun checkChildrenPlaceOrderForUpdates() {
with(layoutNode) {
forEachChild { child ->
// we set `placeOrder` to NotPlacedPlaceOrder for all the children, then
// during the placeChildren() invocation the real order will be assigned for
// all the placed children.
if (child.measurePassDelegate.previousPlaceOrder != child.placeOrder) {
onZSortedChildrenInvalidated()
invalidateLayer()
if (child.placeOrder == NotPlacedPlaceOrder) {
child.measurePassDelegate.markSubtreeAsNotPlaced()
}
}
}
}
}
private fun markSubtreeAsNotPlaced() {
if (isPlaced) {
isPlaced = false
forEachChildDelegate {
it.markSubtreeAsNotPlaced()
}
}
}
private fun markNodeAndSubtreeAsPlaced() {
val wasPlaced = isPlaced
isPlaced = true
with(layoutNode) {
if (!wasPlaced) {
// if the node was not placed previous remeasure request could have been ignored
if (measurePending) {
requestRemeasure(forceRequest = true)
} else if (lookaheadMeasurePending) {
requestLookaheadRemeasure(forceRequest = true)
}
}
// invalidate all the nodes layers that were invalidated while the node was not placed
forEachCoordinatorIncludingInner {
if (it.lastLayerDrawingWasSkipped) {
it.invalidateLayer()
}
}
forEachChild {
// this child was placed during the previous parent's layoutChildren(). this
// means that before the parent became not placed this child was placed. we need
// to restore that
if (it.placeOrder != NotPlacedPlaceOrder) {
it.measurePassDelegate.markNodeAndSubtreeAsPlaced()
rescheduleRemeasureOrRelayout(it)
}
}
}
}
internal var zIndex: Float = 0f
private set
/**
* Invoked when the parent placed the node. It will trigger the layout.
*/
internal fun onNodePlaced() {
val parent = layoutNode.parent
var newZIndex = innerCoordinator.zIndex
layoutNode.forEachCoordinator {
newZIndex += it.zIndex
}
if (newZIndex != zIndex) {
zIndex = newZIndex
parent?.onZSortedChildrenInvalidated()
parent?.invalidateLayer()
}
if (!isPlaced) {
// when the visibility of a child has been changed we need to invalidate
// parents inner layer - the layer in which this child will be drawn
parent?.invalidateLayer()
markNodeAndSubtreeAsPlaced()
}
if (parent != null) {
if (!relayoutWithoutParentInProgress &&
parent.layoutState == LayoutState.LayingOut
) {
// the parent is currently placing its children
check(placeOrder == NotPlacedPlaceOrder) {
"Place was called on a node which was placed already"
}
placeOrder = parent.layoutDelegate.nextChildPlaceOrder
parent.layoutDelegate.nextChildPlaceOrder++
}
// if relayoutWithoutParentInProgress is true we were asked to be relaid out without
// affecting the parent. this means our placeOrder didn't change since the last time
// parent placed us.
} else {
// parent is null for the root node
placeOrder = 0
}
layoutChildren()
}
private fun clearPlaceOrder() {
// reset the place order counter which will be used by the children
nextChildPlaceOrder = 0
forEachChildDelegate { child ->
// and reset the place order for all the children before placing them
child.previousPlaceOrder = child.placeOrder
child.placeOrder = NotPlacedPlaceOrder
// before rerunning the user's layout block reset previous measuredByParent
// for children which we measured in the layout block during the last run.
if (child.measuredByParent == LayoutNode.UsageByParent.InLayoutBlock) {
child.measuredByParent = LayoutNode.UsageByParent.NotUsed
}
}
}
private inline fun forEachChildDelegate(block: (MeasurePassDelegate) -> Unit) {
layoutNode.children.fastForEach {
block(it.measurePassDelegate)
}
}
/**
* The function to be executed when the parent layout measures its children.
*/
override fun measure(constraints: Constraints): Placeable {
if (layoutNode.intrinsicsUsageByParent == LayoutNode.UsageByParent.NotUsed) {
// This LayoutNode may have asked children for intrinsics. If so, we should
// clear the intrinsics usage for everything that was requested previously.
layoutNode.clearSubtreeIntrinsicsUsage()
}
// If we are at the lookahead root of the tree, do both the lookahead measure and
// regular measure. Otherwise, we'll be consistent with parent's lookahead measure
// and regular measure stages. This avoids producing exponential amount of
// lookahead when LookaheadLayouts are nested.
if (layoutNode.isOutMostLookaheadRoot()) {
measuredOnce = true
measurementConstraints = constraints
lookaheadPassDelegate!!.run {
measuredByParent = LayoutNode.UsageByParent.NotUsed
measure(constraints)
}
}
trackMeasurementByParent(layoutNode)
remeasure(constraints)
return this
}
/**
* Return true if the measured size has been changed
*/
fun remeasure(constraints: Constraints): Boolean {
val owner = layoutNode.requireOwner()
val parent = layoutNode.parent
@Suppress("Deprecation")
layoutNode.canMultiMeasure = layoutNode.canMultiMeasure ||
(parent != null && parent.canMultiMeasure)
if (layoutNode.measurePending || measurementConstraints != constraints) {
alignmentLines.usedByModifierMeasurement = false
forEachChildAlignmentLinesOwner {
it.alignmentLines.usedDuringParentMeasurement = false
}
measuredOnce = true
val outerPreviousMeasuredSize = outerCoordinator.size
measurementConstraints = constraints
performMeasure(constraints)
val sizeChanged = outerCoordinator.size != outerPreviousMeasuredSize ||
outerCoordinator.width != width ||
outerCoordinator.height != height
// We are using the coerced coordinator size here to avoid double offset in layout coop.
measuredSize = IntSize(outerCoordinator.width, outerCoordinator.height)
return sizeChanged
} else {
// this node doesn't require being remeasured. however in order to make sure we have
// the final size we need to also make sure the whole subtree is remeasured as it can
// trigger extra remeasure request on our node. we do it now in order to report the
// final measured size to our parent without doing extra pass later.
owner.forceMeasureTheSubtree(layoutNode)
// Restore the intrinsics usage for the sub-tree
layoutNode.resetSubtreeIntrinsicsUsage()
}
return false
}
private fun trackMeasurementByParent(node: LayoutNode) {
val parent = node.parent
if (parent != null) {
check(
measuredByParent == LayoutNode.UsageByParent.NotUsed ||
@Suppress("DEPRECATION") node.canMultiMeasure
) { MeasuredTwiceErrorMessage }
measuredByParent = when (parent.layoutState) {
LayoutState.Measuring ->
LayoutNode.UsageByParent.InMeasureBlock
LayoutState.LayingOut ->
LayoutNode.UsageByParent.InLayoutBlock
else -> throw IllegalStateException(
"Measurable could be only measured from the parent's measure or layout" +
" block. Parents state is ${parent.layoutState}"
)
}
} else {
// when we measure the root it is like the virtual parent is currently laying out
measuredByParent = LayoutNode.UsageByParent.NotUsed
}
}
// We are setting our measuredSize to match the coerced outerCoordinator size, to prevent
// double offseting for layout cooperation. However, this means that here we need
// to override these getters to make the measured values correct in Measured.
// TODO(popam): clean this up
override val measuredWidth: Int get() = outerCoordinator.measuredWidth
override val measuredHeight: Int get() = outerCoordinator.measuredHeight
override fun get(alignmentLine: AlignmentLine): Int {
if (layoutNode.parent?.layoutState == LayoutState.Measuring) {
alignmentLines.usedDuringParentMeasurement = true
} else if (layoutNode.parent?.layoutState == LayoutState.LayingOut) {
alignmentLines.usedDuringParentLayout = true
}
duringAlignmentLinesQuery = true
val result = outerCoordinator[alignmentLine]
duringAlignmentLinesQuery = false
return result
}
override fun placeAt(
position: IntOffset,
zIndex: Float,
layerBlock: (GraphicsLayerScope.() -> Unit)?
) {
if (position != lastPosition) {
notifyChildrenUsingCoordinatesWhilePlacing()
}
// This can actually be called as soon as LookaheadMeasure is done, but devs may expect
// certain placement results (e.g. LayoutCoordinates) to be valid when lookahead placement
// takes place. If that's not the case, it will make sense to move this right after
// lookahead measure, before place.
if (layoutNode.isOutMostLookaheadRoot()) {
// Lookahead placement first
with(PlacementScope) {
lookaheadPassDelegate!!.let {
// Since this is the root of the lookahead delegate tree, no parent will
// reset the place order, therefore we have to do it manually.
layoutNode.parent?.run {
layoutDelegate.nextChildLookaheadPlaceOrder = 0
}
it.placeOrder = NotPlacedPlaceOrder
it.place(position.x, position.y)
}
}
}
// Post-lookahead (if any) placement
layoutState = LayoutState.LayingOut
placeOuterCoordinator(position, zIndex, layerBlock)
layoutState = LayoutState.Idle
}
private fun placeOuterCoordinator(
position: IntOffset,
zIndex: Float,
layerBlock: (GraphicsLayerScope.() -> Unit)?
) {
lastPosition = position
lastZIndex = zIndex
lastLayerBlock = layerBlock
placedOnce = true
alignmentLines.usedByModifierLayout = false
coordinatesAccessedDuringPlacement = false
val owner = layoutNode.requireOwner()
owner.snapshotObserver.observeLayoutModifierSnapshotReads(
layoutNode,
affectsLookahead = false
) {
with(PlacementScope) {
if (layerBlock == null) {
outerCoordinator.place(position, zIndex)
} else {
outerCoordinator.placeWithLayer(position, zIndex, layerBlock)
}
}
}
}
/**
* Calls [placeOuterCoordinator] with the same position used during the last
* [placeOuterCoordinator] call. [placeOuterCoordinator] only does the placement for
* post-lookahead pass.
*/
fun replace() {
try {
relayoutWithoutParentInProgress = true
check(placedOnce)
placeOuterCoordinator(lastPosition, lastZIndex, lastLayerBlock)
} finally {
relayoutWithoutParentInProgress = false
}
}
override fun minIntrinsicWidth(height: Int): Int {
onIntrinsicsQueried()
return outerCoordinator.minIntrinsicWidth(height)
}
override fun maxIntrinsicWidth(height: Int): Int {
onIntrinsicsQueried()
return outerCoordinator.maxIntrinsicWidth(height)
}
override fun minIntrinsicHeight(width: Int): Int {
onIntrinsicsQueried()
return outerCoordinator.minIntrinsicHeight(width)
}
override fun maxIntrinsicHeight(width: Int): Int {
onIntrinsicsQueried()
return outerCoordinator.maxIntrinsicHeight(width)
}
private fun onIntrinsicsQueried() {
// How intrinsics work when specific / custom intrinsics are not provided to the custom
// layout is we essentially run the measure block of a child with not-final constraints
// and fake measurables. It is possible that some measure blocks are not pure and have
// side effects, like save some state calculated during the measurement.
// In order to make it possible we always have to rerun the measure block with the real
// final constraints after the intrinsics run. Sometimes it will cause unnecessary
// remeasurements, but it makes sure such component states are using the correct final
// constraints/sizes.
layoutNode.requestRemeasure()
// Mark the intrinsics size has been used by the parent if it hasn't already been marked.
val parent = layoutNode.parent
if (parent != null &&
layoutNode.intrinsicsUsageByParent == LayoutNode.UsageByParent.NotUsed
) {
layoutNode.intrinsicsUsageByParent = when (parent.layoutState) {
LayoutState.Measuring -> LayoutNode.UsageByParent.InMeasureBlock
LayoutState.LayingOut -> LayoutNode.UsageByParent.InLayoutBlock
// Called from parent's intrinsic measurement
else -> parent.intrinsicsUsageByParent
}
}
}
fun invalidateParentData() {
parentDataDirty = true
}
fun updateParentData(): Boolean {
if (!parentDataDirty) return false
parentDataDirty = false
val changed = parentData != outerCoordinator.parentData
parentData = outerCoordinator.parentData
return changed
}
override fun calculateAlignmentLines(): Map<AlignmentLine, Int> {
if (!duringAlignmentLinesQuery) {
// Mark alignments used by modifier
if (layoutState == LayoutState.Measuring) {
alignmentLines.usedByModifierMeasurement = true
// We quickly transition to layoutPending as we need the alignment lines now.
// Later we will see that we also laid out as part of measurement and will skip layout.
if (alignmentLines.dirty) markLayoutPending()
} else {
// Note this can also happen for onGloballyPositioned queries.
alignmentLines.usedByModifierLayout = true
}
}
innerCoordinator.isPlacingForAlignment = true
layoutChildren()
innerCoordinator.isPlacingForAlignment = false
return alignmentLines.getLastCalculation()
}
override val parentAlignmentLinesOwner: AlignmentLinesOwner?
get() = layoutNode.parent?.layoutDelegate?.alignmentLinesOwner
override fun forEachChildAlignmentLinesOwner(block: (AlignmentLinesOwner) -> Unit) {
layoutNode.children.fastForEach {
block(it.layoutDelegate.alignmentLinesOwner)
}
}
override fun requestLayout() {
layoutNode.requestRelayout()
}
override fun requestMeasure() {
layoutNode.requestRemeasure()
}
/**
* This is called any time a placement has done that changes the position during the
* layout pass. If any child is looking at their own coordinates to know how to
* place children, it will be invalided.
*
* Note that this is called for every changed position. While not many layouts
* look at their coordinates, if there is one, it will cause all position changes
* from an ancestor to call down the hierarchy. If this becomes expensive (e.g. many
* parents change their position on the same frame), it might be worth using a flag
* so that this call becomes cheap after the first one.
*/
fun notifyChildrenUsingCoordinatesWhilePlacing() {
if (childrenAccessingCoordinatesDuringPlacement > 0) {
layoutNode.children.fastForEach { child ->
val childLayoutDelegate = child.layoutDelegate
if (childLayoutDelegate.coordinatesAccessedDuringPlacement &&
!childLayoutDelegate.layoutPending
) {
child.requestRelayout()
}
childLayoutDelegate.measurePassDelegate
.notifyChildrenUsingCoordinatesWhilePlacing()
}
}
}
/**
* The callback to be executed before running layoutChildren.
*
* There are possible cases when we run layoutChildren() on the parent node, but some of its
* children are not yet measured even if they are supposed to be measured in the measure
* block of our parent.
*
* Example:
* val child = Layout(...)
* Layout(child) { measurable, constraints ->
* val placeable = measurable.first().measure(constraints)
* layout(placeable.width, placeable.height) {
* placeable.place(0, 0)
* }
* }
* And now some set of changes scheduled remeasure for child and relayout for parent.
*
* During the [MeasureAndLayoutDelegate.measureAndLayout] we will start with the parent as it
* has lower depth. Inside the layout block we will call placeable.width which is currently
* dirty as the child was scheduled to remeasure. This callback will ensure it never happens
* and pre-remeasure everything required for this layoutChildren().
*/
private fun onBeforeLayoutChildren() {
layoutNode.forEachChild {
if (it.measurePending &&
it.measuredByParent == LayoutNode.UsageByParent.InMeasureBlock
) {
if (it.remeasure()) {
layoutNode.requestRemeasure()
}
}
}
}
/**
* If this was used in an intrinsics measurement, find the parent that used it and
* invalidate either the measure block or layout block.
*/
fun invalidateIntrinsicsParent(forceRequest: Boolean) {
val parent = layoutNode.parent
val intrinsicsUsageByParent = layoutNode.intrinsicsUsageByParent
if (parent != null && intrinsicsUsageByParent != LayoutNode.UsageByParent.NotUsed) {
// find measuring parent
var intrinsicsUsingParent: LayoutNode = parent
while (intrinsicsUsingParent.intrinsicsUsageByParent == intrinsicsUsageByParent) {
intrinsicsUsingParent = intrinsicsUsingParent.parent ?: break
}
when (intrinsicsUsageByParent) {
LayoutNode.UsageByParent.InMeasureBlock ->
intrinsicsUsingParent.requestRemeasure(forceRequest)
LayoutNode.UsageByParent.InLayoutBlock ->
intrinsicsUsingParent.requestRelayout(forceRequest)
else -> error("Intrinsics isn't used by the parent")
}
}
}
fun onNodeDetached() {
placeOrder = NotPlacedPlaceOrder
previousPlaceOrder = NotPlacedPlaceOrder
isPlaced = false
}
/**
* Measure the [MeasurePassDelegate] using the lookahead constraints.
*
* Note: [measure] will only be invoked if we are in the right block. That means if
* lookahead measurement was done in the measurement block, this function needs to be
* invoked in measurement block. Otherwise, no-op.
*/
fun measureBasedOnLookahead() {
val lookaheadDelegate = lookaheadPassDelegate
val parent = layoutNode.parent!!
requireNotNull(lookaheadDelegate)
if (lookaheadDelegate.measuredByParent == LayoutNode.UsageByParent.InMeasureBlock &&
parent.layoutState == LayoutState.Measuring
) {
measure(lookaheadDelegate.lastConstraints!!)
} else if (
lookaheadDelegate.measuredByParent == LayoutNode.UsageByParent.InLayoutBlock &&
parent.layoutState == LayoutState.LayingOut
) {
measure(lookaheadDelegate.lastConstraints!!)
}
}
/**
* Places the [MeasurePassDelegate] at the same position with the same zIndex and
* layerBlock as lookahead.
*/
fun placeBasedOnLookahead() {
val lookaheadDelegate = requireNotNull(lookaheadPassDelegate)
placeAt(
lookaheadDelegate.lastPosition,
lookaheadDelegate.lastZIndex,
lookaheadDelegate.lastLayerBlock
)
}
}
/**
* [LookaheadPassDelegate] manages the measure/layout and alignmentLine related queries for
* the lookahead pass.
*/
inner class LookaheadPassDelegate : Placeable(), Measurable, AlignmentLinesOwner {
/**
* Is true during [replace] invocation. Helps to differentiate between the cases when our
* parent is measuring us during the measure block, and when we are remeasured individually
* because of some change. This could be useful to know if we need to record the placing
* order.
*/
private var relayoutWithoutParentInProgress: Boolean = false
/**
* The value [placeOrder] had during the previous parent `layoutChildren`. Helps us to
* understand if the order did change.
*/
private var previousPlaceOrder: Int = NotPlacedPlaceOrder
private set
/**
* The order in which this node was placed by its parent during the previous
* `layoutChildren`. Before the placement the order is set to [NotPlacedPlaceOrder] to all
* the children. Then every placed node assigns this variable to parent's
* LayoutNodeLayoutDelegate's [nextChildLookaheadPlaceOrder] and increments this counter.
* Not placed items will still have [NotPlacedPlaceOrder] set.
*/
internal var placeOrder: Int = NotPlacedPlaceOrder
internal var measuredByParent = LayoutNode.UsageByParent.NotUsed
internal val measurePassDelegate: MeasurePassDelegate
get() = this@LayoutNodeLayoutDelegate.measurePassDelegate
internal var duringAlignmentLinesQuery: Boolean = false
private var placedOnce: Boolean = false
private var measuredOnce: Boolean = false
val lastConstraints: Constraints?
get() = lookaheadConstraints
private var lookaheadConstraints: Constraints? = null
internal var lastPosition: IntOffset = IntOffset.Zero
private set
internal var lastZIndex: Float = 0f
private set
internal var lastLayerBlock: (GraphicsLayerScope.() -> Unit)? = null
private set
override var isPlaced: Boolean = false
override val innerCoordinator: NodeCoordinator
get() = layoutNode.innerCoordinator
override val alignmentLines: AlignmentLines = LookaheadAlignmentLines(this)
private val _childDelegates = MutableVector<LookaheadPassDelegate>()
internal var childDelegatesDirty: Boolean = true
internal val childDelegates: List<LookaheadPassDelegate>
get() {
layoutNode.children.let {
// Invoke children to get children updated before checking dirty
if (!childDelegatesDirty) return _childDelegates.asMutableList()
}
layoutNode.updateChildMeasurables(_childDelegates) {
it.layoutDelegate.lookaheadPassDelegate!!
}
childDelegatesDirty = false
return _childDelegates.asMutableList()
}
private inline fun forEachChildDelegate(block: (LookaheadPassDelegate) -> Unit) =
layoutNode.forEachChild {
block(it.layoutDelegate.lookaheadPassDelegate!!)
}
override fun layoutChildren() {
alignmentLines.recalculateQueryOwner()
if (lookaheadLayoutPending) {
onBeforeLayoutChildren()
}
val lookaheadDelegate = innerCoordinator.lookaheadDelegate!!
// as a result of the previous operation we can figure out a child has been resized
// and we need to be remeasured, not relaid out
if (lookaheadLayoutPendingForAlignment ||
(!duringAlignmentLinesQuery && !lookaheadDelegate.isPlacingForAlignment &&
lookaheadLayoutPending)
) {
lookaheadLayoutPending = false
val oldLayoutState = layoutState
layoutState = LayoutState.LookaheadLayingOut
val owner = layoutNode.requireOwner()
owner.snapshotObserver.observeLayoutSnapshotReads(layoutNode) {
clearPlaceOrder()
forEachChildAlignmentLinesOwner { child ->
child.alignmentLines.usedDuringParentLayout = false
}
lookaheadDelegate.measureResult.placeChildren()
checkChildrenPlaceOrderForUpdates()
forEachChildAlignmentLinesOwner { child ->
child.alignmentLines.previousUsedDuringParentLayout =
child.alignmentLines.usedDuringParentLayout
}
}
layoutState = oldLayoutState
if (coordinatesAccessedDuringPlacement &&
lookaheadDelegate.isPlacingForAlignment
) {
requestLayout()
}
lookaheadLayoutPendingForAlignment = false
}
if (alignmentLines.usedDuringParentLayout) {
alignmentLines.previousUsedDuringParentLayout = true
}
if (alignmentLines.dirty && alignmentLines.required) alignmentLines.recalculate()
}
private fun checkChildrenPlaceOrderForUpdates() {
forEachChildDelegate { child ->
// we set `placeOrder` to NotPlacedPlaceOrder for all the children, then
// during the placeChildren() invocation the real order will be assigned for
// all the placed children.
if (child.previousPlaceOrder != child.placeOrder) {
if (child.placeOrder == NotPlacedPlaceOrder) {
child.markSubtreeAsNotPlaced()
}
}
}
}
private fun markSubtreeAsNotPlaced() {
if (isPlaced) {
isPlaced = false
forEachChildDelegate { it.markSubtreeAsNotPlaced() }
}
}
override fun calculateAlignmentLines(): Map<AlignmentLine, Int> {
if (!duringAlignmentLinesQuery) {
if (layoutState == LayoutState.LookaheadMeasuring) {
// Mark alignments used by modifier
alignmentLines.usedByModifierMeasurement = true
// We quickly transition to layoutPending as we need the alignment lines now.
// Later we will see that we also laid out as part of measurement and will skip layout.
if (alignmentLines.dirty) markLookaheadLayoutPending()
} else {
// Note this can also happen for onGloballyPositioned queries.
alignmentLines.usedByModifierLayout = true
}
}
innerCoordinator.lookaheadDelegate?.isPlacingForAlignment = true
layoutChildren()
innerCoordinator.lookaheadDelegate?.isPlacingForAlignment = false
return alignmentLines.getLastCalculation()
}
override val parentAlignmentLinesOwner: AlignmentLinesOwner?
get() = layoutNode.parent?.layoutDelegate?.lookaheadAlignmentLinesOwner
override fun forEachChildAlignmentLinesOwner(block: (AlignmentLinesOwner) -> Unit) {
layoutNode.children.fastForEach {
block(it.layoutDelegate.lookaheadAlignmentLinesOwner!!)
}
}
override fun requestLayout() {
layoutNode.requestLookaheadRelayout()
}
override fun requestMeasure() {
layoutNode.requestLookaheadRemeasure()
}
/**
* This is called any time a placement has done that changes the position during the
* lookahead layout pass. If any child is looking at their own coordinates to know how to
* place children, it will be invalided.
*
* Note that this is called for every changed position. While not many layouts
* look at their coordinates, if there is one, it will cause all position changes
* from an ancestor to call down the hierarchy. If this becomes expensive (e.g. many
* parents change their position on the same frame), it might be worth using a flag
* so that this call becomes cheap after the first one.
*/
fun notifyChildrenUsingCoordinatesWhilePlacing() {
if (childrenAccessingCoordinatesDuringPlacement > 0) {
layoutNode.children.fastForEach { child ->
val childLayoutDelegate = child.layoutDelegate
if (childLayoutDelegate.coordinatesAccessedDuringPlacement &&
!childLayoutDelegate.layoutPending
) {
child.requestLookaheadRelayout()
}
childLayoutDelegate.lookaheadPassDelegate
?.notifyChildrenUsingCoordinatesWhilePlacing()
}
}
}
override fun measure(constraints: Constraints): Placeable {
trackLookaheadMeasurementByParent(layoutNode)
if (layoutNode.intrinsicsUsageByParent == LayoutNode.UsageByParent.NotUsed) {
// This LayoutNode may have asked children for intrinsics. If so, we should
// clear the intrinsics usage for everything that was requested previously.
layoutNode.clearSubtreeIntrinsicsUsage()
}
// Since this a measure request coming from the parent. We'd be starting lookahead
// only if the current layoutNode is the top-level lookahead root.
// This is an optimization to avoid redundant Snapshot.enter when creating new snapshots
// for lookahead, in order to reduce the size of the call stack.
remeasure(constraints)
return this
}
// Track lookahead measurement
private fun trackLookaheadMeasurementByParent(node: LayoutNode) {
// when we measure the root it is like the virtual parent is currently laying out
val parent = node.parent
if (parent != null) {
check(
measuredByParent == LayoutNode.UsageByParent.NotUsed ||
@Suppress("DEPRECATION") node.canMultiMeasure
) { MeasuredTwiceErrorMessage }
measuredByParent = when (parent.layoutState) {
LayoutState.LookaheadMeasuring, LayoutState.Measuring ->
LayoutNode.UsageByParent.InMeasureBlock
LayoutState.LayingOut, LayoutState.LookaheadLayingOut ->
LayoutNode.UsageByParent.InLayoutBlock
else -> throw IllegalStateException(
"Measurable could be only measured from the parent's measure or layout" +
" block. Parents state is ${parent.layoutState}"
)
}
} else {
measuredByParent = LayoutNode.UsageByParent.NotUsed
}
}
private var parentDataDirty: Boolean = true
override var parentData: Any? = measurePassDelegate.parentData
private set
// Lookahead remeasurement with the given constraints.
fun remeasure(constraints: Constraints): Boolean {
val parent = layoutNode.parent
@Suppress("Deprecation")
layoutNode.canMultiMeasure = layoutNode.canMultiMeasure ||
(parent != null && parent.canMultiMeasure)
if (layoutNode.lookaheadMeasurePending || lookaheadConstraints != constraints) {
lookaheadConstraints = constraints
alignmentLines.usedByModifierMeasurement = false
forEachChildAlignmentLinesOwner {
it.alignmentLines.usedDuringParentMeasurement = false
}
measuredOnce = true
val lookaheadDelegate = outerCoordinator.lookaheadDelegate
check(lookaheadDelegate != null) {
"Lookahead result from lookaheadRemeasure cannot be null"
}
// Copy out the previous size before perform lookahead measure
val lastLookaheadSize = IntSize(lookaheadDelegate.width, lookaheadDelegate.height)
performLookaheadMeasure(constraints)
measuredSize = IntSize(lookaheadDelegate.width, lookaheadDelegate.height)
val sizeChanged = lastLookaheadSize.width != lookaheadDelegate.width ||
lastLookaheadSize.height != lookaheadDelegate.height
return sizeChanged
} else {
// this node doesn't require being remeasured. however in order to make sure we have
// the final size we need to also make sure the whole subtree is remeasured as it
// can trigger extra remeasure request on our node. we do it now in order to report
// the final measured size to our parent without doing extra pass later.
layoutNode.owner?.forceMeasureTheSubtree(layoutNode, affectsLookahead = true)
// Restore the intrinsics usage for the sub-tree
layoutNode.resetSubtreeIntrinsicsUsage()
}
return false
}
override fun placeAt(
position: IntOffset,
zIndex: Float,
layerBlock: (GraphicsLayerScope.() -> Unit)?
) {
layoutState = LayoutState.LookaheadLayingOut
placedOnce = true
if (position != lastPosition) {
notifyChildrenUsingCoordinatesWhilePlacing()
}
alignmentLines.usedByModifierLayout = false
val owner = layoutNode.requireOwner()
coordinatesAccessedDuringPlacement = false
owner.snapshotObserver.observeLayoutModifierSnapshotReads(layoutNode) {
with(PlacementScope) {
outerCoordinator.lookaheadDelegate!!.place(position)
}
}
lastPosition = position
lastZIndex = zIndex
lastLayerBlock = layerBlock
layoutState = LayoutState.Idle
}
// We are setting our measuredSize to match the coerced outerCoordinator size, to prevent
// double offseting for layout cooperation. However, this means that here we need
// to override these getters to make the measured values correct in Measured.
// TODO(popam): clean this up
override val measuredWidth: Int get() = outerCoordinator.lookaheadDelegate!!.measuredWidth
override val measuredHeight: Int get() = outerCoordinator.lookaheadDelegate!!.measuredHeight
override fun get(alignmentLine: AlignmentLine): Int {
if (layoutNode.parent?.layoutState == LayoutState.LookaheadMeasuring) {
alignmentLines.usedDuringParentMeasurement = true
} else if (layoutNode.parent?.layoutState == LayoutState.LookaheadLayingOut) {
alignmentLines.usedDuringParentLayout = true
}
duringAlignmentLinesQuery = true
val result = outerCoordinator.lookaheadDelegate!![alignmentLine]
duringAlignmentLinesQuery = false
return result
}
override fun minIntrinsicWidth(height: Int): Int {
onIntrinsicsQueried()
return outerCoordinator.lookaheadDelegate!!.minIntrinsicWidth(height)
}
override fun maxIntrinsicWidth(height: Int): Int {
onIntrinsicsQueried()
return outerCoordinator.lookaheadDelegate!!.maxIntrinsicWidth(height)
}
override fun minIntrinsicHeight(width: Int): Int {
onIntrinsicsQueried()
return outerCoordinator.lookaheadDelegate!!.minIntrinsicHeight(width)
}
override fun maxIntrinsicHeight(width: Int): Int {
onIntrinsicsQueried()
return outerCoordinator.lookaheadDelegate!!.maxIntrinsicHeight(width)
}
private fun onIntrinsicsQueried() {
// How intrinsics work when specific / custom intrinsics are not provided to the custom
// layout is we essentially run the measure block of a child with not-final constraints
// and fake measurables. It is possible that some measure blocks are not pure and have
// side effects, like save some state calculated during the measurement.
// In order to make it possible we always have to rerun the measure block with the real
// final constraints after the intrinsics run. Sometimes it will cause unnecessary
// remeasurements, but it makes sure such component states are using the correct final
// constraints/sizes.
layoutNode.requestLookaheadRemeasure()
// Mark the intrinsics size has been used by the parent if it hasn't already been marked.
val parent = layoutNode.parent
if (parent != null &&
layoutNode.intrinsicsUsageByParent == LayoutNode.UsageByParent.NotUsed
) {
layoutNode.intrinsicsUsageByParent = when (parent.layoutState) {
LayoutState.Measuring -> LayoutNode.UsageByParent.InMeasureBlock
LayoutState.LayingOut -> LayoutNode.UsageByParent.InLayoutBlock
// Called from parent's intrinsic measurement
else -> parent.intrinsicsUsageByParent
}
}
}
/**
* If this was used in an intrinsics measurement, find the parent that used it and
* invalidate either the measure block or layout block.
*/
fun invalidateIntrinsicsParent(forceRequest: Boolean) {
val parent = layoutNode.parent
val intrinsicsUsageByParent = layoutNode.intrinsicsUsageByParent
if (parent != null && intrinsicsUsageByParent != LayoutNode.UsageByParent.NotUsed) {
// find measuring parent
var intrinsicsUsingParent: LayoutNode = parent
while (intrinsicsUsingParent.intrinsicsUsageByParent == intrinsicsUsageByParent) {
intrinsicsUsingParent = intrinsicsUsingParent.parent ?: break
}
when (intrinsicsUsageByParent) {
LayoutNode.UsageByParent.InMeasureBlock ->
if (intrinsicsUsingParent.lookaheadRoot != null) {
intrinsicsUsingParent.requestLookaheadRemeasure(forceRequest)
} else {
intrinsicsUsingParent.requestRemeasure(forceRequest)
}
LayoutNode.UsageByParent.InLayoutBlock ->
if (intrinsicsUsingParent.lookaheadRoot != null) {
intrinsicsUsingParent.requestLookaheadRelayout(forceRequest)
} else {
intrinsicsUsingParent.requestRelayout(forceRequest)
}
else -> error("Intrinsics isn't used by the parent")
}
}
}
fun invalidateParentData() {
parentDataDirty = true
}
fun updateParentData(): Boolean {
if (!parentDataDirty) return false
parentDataDirty = false
val changed = parentData != outerCoordinator.lookaheadDelegate!!.parentData
parentData = outerCoordinator.lookaheadDelegate!!.parentData
return changed
}
internal fun onNodePlaced() {
val parent = layoutNode.parent
if (!isPlaced) {
markNodeAndSubtreeAsPlaced()
}
if (parent != null) {
if (!relayoutWithoutParentInProgress &&
parent.layoutState == LayoutState.LayingOut ||
parent.layoutState == LayoutState.LookaheadLayingOut
) {
// the parent is currently placing its children
check(placeOrder == NotPlacedPlaceOrder) {
"Place was called on a node which was placed already"
}
placeOrder = parent.layoutDelegate.nextChildLookaheadPlaceOrder
parent.layoutDelegate.nextChildLookaheadPlaceOrder++
}
// if relayoutWithoutParentInProgress is true we were asked to be relaid out without
// affecting the parent. this means our placeOrder didn't change since the last time
// parent placed us.
} else {
// parent is null for the root node
placeOrder = 0
}
layoutChildren()
}
private fun clearPlaceOrder() {
// reset the place order counter which will be used by the children
this@LayoutNodeLayoutDelegate.nextChildLookaheadPlaceOrder = 0
forEachChildDelegate { child ->
// and reset the place order for all the children before placing them
child.previousPlaceOrder = child.placeOrder
child.placeOrder = NotPlacedPlaceOrder
// before rerunning the user's layout block reset previous measuredByParent
// for children which we measured in the layout block during the last run.
if (child.measuredByParent == LayoutNode.UsageByParent.InLayoutBlock) {
child.measuredByParent = LayoutNode.UsageByParent.NotUsed
}
}
}
private fun markNodeAndSubtreeAsPlaced() {
val wasPlaced = isPlaced
isPlaced = true
if (!wasPlaced) {
if (lookaheadMeasurePending) {
// if the node was not placed previous remeasure request could have been ignored
layoutNode.requestLookaheadRemeasure(forceRequest = true)
}
}
layoutNode.forEachChild {
// this child was placed during the previous parent's layoutChildren(). this means that
// before the parent became not placed this child was placed. we need to restore that
if (it.placeOrder != NotPlacedPlaceOrder) {
it.lookaheadPassDelegate!!.markNodeAndSubtreeAsPlaced()
it.rescheduleRemeasureOrRelayout(it)
}
}
}
/**
* The callback to be executed before running layoutChildren.
*
* There are possible cases when we run layoutChildren() on the parent node, but some of its
* children are not yet measured even if they are supposed to be measured in the measure
* block of our parent.
*
* Example:
* val child = Layout(...)
* Layout(child) { measurable, constraints ->
* val placeable = measurable.first().measure(constraints)
* layout(placeable.width, placeable.height) {
* placeable.place(0, 0)
* }
* }
* And now some set of changes scheduled remeasure for child and relayout for parent.
*
* During the [MeasureAndLayoutDelegate.measureAndLayout] we will start with the parent as it
* has lower depth. Inside the layout block we will call placeable.width which is currently
* dirty as the child was scheduled to remeasure. This callback will ensure it never happens
* and pre-remeasure everything required for this layoutChildren().
*/
private fun onBeforeLayoutChildren() {
layoutNode.forEachChild {
if (it.lookaheadMeasurePending &&
it.measuredByParentInLookahead == LayoutNode.UsageByParent.InMeasureBlock
) {
if (it.layoutDelegate.lookaheadPassDelegate!!.remeasure(
lastConstraints!!
)
) {
layoutNode.requestLookaheadRemeasure()
}
}
}
}
fun replace() {
try {
relayoutWithoutParentInProgress = true
check(placedOnce)
placeAt(lastPosition, 0f, null)
} finally {
relayoutWithoutParentInProgress = false
}
}
fun onNodeDetached() {
placeOrder = NotPlacedPlaceOrder
previousPlaceOrder = NotPlacedPlaceOrder
isPlaced = false
}
}
/**
* Returns if the we are at the lookahead root of the tree, by checking if the parent is
* has a lookahead root.
*/
private fun LayoutNode.isOutMostLookaheadRoot(): Boolean =
lookaheadRoot != null && parent?.lookaheadRoot == null
/**
* Performs measure with the given constraints and perform necessary state mutations before
* and after the measurement.
*/
private fun performMeasure(constraints: Constraints) {
check(layoutState == LayoutState.Idle) {
"layout state is not idle before measure starts"
}
layoutState = LayoutState.Measuring
measurePending = false
layoutNode.requireOwner().snapshotObserver.observeMeasureSnapshotReads(
layoutNode,
affectsLookahead = false
) {
outerCoordinator.measure(constraints)
}
// The resulting layout state might be Ready. This can happen when the layout node's
// own modifier is querying an alignment line during measurement, therefore we
// need to also layout the layout node.
if (layoutState == LayoutState.Measuring) {
markLayoutPending()
layoutState = LayoutState.Idle
}
}
private fun performLookaheadMeasure(
constraints: Constraints
) {
layoutState = LayoutState.LookaheadMeasuring
lookaheadMeasurePending = false
layoutNode.requireOwner().snapshotObserver.observeMeasureSnapshotReads(layoutNode) {
outerCoordinator.lookaheadDelegate!!.measure(constraints)
}
markLookaheadLayoutPending()
if (layoutNode.isOutMostLookaheadRoot()) {
// If layoutNode is the root of the lookahead, measure is redirected to lookahead
// measure, and layout pass will begin lookahead placement, measure & layout.
markLayoutPending()
} else {
// If layoutNode is not the root of the lookahead, measure needs to follow the
// lookahead measure.
markMeasurePending()
}
layoutState = LayoutState.Idle
}
internal fun ensureLookaheadDelegateCreated() {
if (lookaheadPassDelegate == null) {
lookaheadPassDelegate = LookaheadPassDelegate()
}
}
fun updateParentData() {
if (measurePassDelegate.updateParentData()) {
layoutNode.parent?.requestRemeasure()
}
if (lookaheadPassDelegate?.updateParentData() == true) {
if (layoutNode.isOutMostLookaheadRoot()) {
layoutNode.parent?.requestRemeasure()
} else {
layoutNode.parent?.requestLookaheadRemeasure()
}
}
}
fun invalidateParentData() {
measurePassDelegate.invalidateParentData()
lookaheadPassDelegate?.invalidateParentData()
}
fun resetAlignmentLines() {
measurePassDelegate.alignmentLines.reset()
lookaheadPassDelegate?.alignmentLines?.reset()
}
fun markChildrenDirty() {
measurePassDelegate.childDelegatesDirty = true
lookaheadPassDelegate?.let { it.childDelegatesDirty = true }
}
}
private inline fun <T : Measurable> LayoutNode.updateChildMeasurables(
destination: MutableVector<T>,
transform: (LayoutNode) -> T
) {
forEachChildIndexed { i, layoutNode ->
if (destination.size <= i) {
destination.add(transform(layoutNode))
} else {
destination[i] = transform(layoutNode)
}
}
destination.removeRange(
children.size,
destination.size
)
}
private const val MeasuredTwiceErrorMessage: String =
"measure() may not be called multiple times on the same Measurable. If you want to " +
"get the content size of the Measurable before calculating the final constraints, " +
"please use methods like minIntrinsicWidth()/maxIntrinsicWidth() and " +
"minIntrinsicHeight()/maxIntrinsicHeight()"
/**
* AlignmentLinesOwner defines APIs that are needed to respond to alignment line changes, and to
* query alignment line related info.
*
* [LayoutNodeLayoutDelegate.LookaheadPassDelegate] and
* [LayoutNodeLayoutDelegate.MeasurePassDelegate] both implement this interface, and they
* encapsulate the difference in alignment lines handling for lookahead pass vs. actual
* measure/layout pass.
*/
internal interface AlignmentLinesOwner : Measurable {
/**
* Whether the AlignmentLinesOwner has been placed.
*/
val isPlaced: Boolean
/**
* InnerNodeCoordinator of the LayoutNode that the AlignmentLinesOwner operates on.
*/
val innerCoordinator: NodeCoordinator
/**
* Alignment lines for either lookahead pass or post-lookahead pass, depending on the
* AlignmentLineOwner.
*/
val alignmentLines: AlignmentLines
/**
* The implementation for laying out children. Different types of AlignmentLinesOwner will
* layout children for either the lookahead pass, or the layout pass post-lookahead.
*/
fun layoutChildren()
/**
* Recalculate the alignment lines if dirty, and layout children as needed.
*/
fun calculateAlignmentLines(): Map<AlignmentLine, Int>
/**
* Parent [AlignmentLinesOwner]. This will be the AlignmentLinesOwner for the same pass but for
* the parent [LayoutNode].
*/
val parentAlignmentLinesOwner: AlignmentLinesOwner?
/**
* This allows iterating all the AlignmentOwners for the same pass for each of the child
* LayoutNodes
*/
fun forEachChildAlignmentLinesOwner(block: (AlignmentLinesOwner) -> Unit)
/**
* Depending on which pass the [AlignmentLinesOwner] is created for, this could mean
* requestLookaheadLayout() for the lookahead pass, or requestLayout() for post-
* lookahead pass.
*/
fun requestLayout()
/**
* Depending on which pass the [AlignmentLinesOwner] is created for, this could mean
* requestLookaheadMeasure() for the lookahead pass, or requestMeasure() for post-
* lookahead pass.
*/
fun requestMeasure()
}