LayoutModifierNodeCoordinator.kt
/*
* Copyright 2022 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.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.PaintingStyle
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.HorizontalAlignmentLine
import androidx.compose.ui.layout.IntermediateLayoutModifier
import androidx.compose.ui.layout.LayoutModifier
import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
@OptIn(ExperimentalComposeUiApi::class)
internal class LayoutModifierNodeCoordinator(
layoutNode: LayoutNode,
measureNode: LayoutModifierNode,
) : NodeCoordinator(layoutNode) {
var layoutModifierNode: LayoutModifierNode = measureNode
internal set
override val tail: Modifier.Node
get() = layoutModifierNode.node
val wrappedNonNull: NodeCoordinator get() = wrapped!!
private var lookAheadTransientMeasureNode: IntermediateLayoutModifierNode? = measureNode.run {
if (node.isKind(Nodes.IntermediateMeasure) && this is IntermediateLayoutModifierNode) this
else null
}
/**
* LookaheadDelegate impl for when the modifier is any [LayoutModifier] except
* [IntermediateLayoutModifier]. This impl will invoke [LayoutModifier.measure] for
* the lookahead measurement.
*/
private inner class LookaheadDelegateForLayoutModifierNode(
scope: LookaheadScope
) : LookaheadDelegate(this, scope) {
// LookaheadMeasure
override fun measure(constraints: Constraints): Placeable =
performingMeasure(constraints) {
with(layoutModifierNode) {
measure(
// This allows `measure` calls in the modifier to be redirected to
// calling lookaheadMeasure in wrapped.
wrappedNonNull.lookaheadDelegate!!, constraints
)
}
}
override fun calculateAlignmentLine(alignmentLine: AlignmentLine): Int {
return calculateAlignmentAndPlaceChildAsNeeded(alignmentLine).also {
cachedAlignmentLinesMap[alignmentLine] = it
}
}
override fun minIntrinsicWidth(height: Int): Int =
with(layoutModifierNode) {
minIntrinsicWidth(wrappedNonNull.lookaheadDelegate!!, height)
}
override fun maxIntrinsicWidth(height: Int): Int =
with(layoutModifierNode) {
maxIntrinsicWidth(wrappedNonNull.lookaheadDelegate!!, height)
}
override fun minIntrinsicHeight(width: Int): Int =
with(layoutModifierNode) {
minIntrinsicHeight(wrappedNonNull.lookaheadDelegate!!, width)
}
override fun maxIntrinsicHeight(width: Int): Int =
with(layoutModifierNode) {
maxIntrinsicHeight(wrappedNonNull.lookaheadDelegate!!, width)
}
}
/**
* LookaheadDelegate impl for when the [layoutModifierNode] is an
* [IntermediateLayoutModifierNode]. This impl will redirect the measure call to the next
* lookahead delegate in the chain, without invoking the measure lambda defined in the modifier.
* This is necessary because [IntermediateLayoutModifierNode] does not participate in lookahead.
*/
private inner class LookaheadDelegateForIntermediateLayoutModifier(
scope: LookaheadScope,
val intermediateMeasureNode: IntermediateLayoutModifierNode
) : LookaheadDelegate(this, scope) {
private inner class PassThroughMeasureResult : MeasureResult {
override val width: Int
get() = wrappedNonNull.lookaheadDelegate!!.measureResult.width
override val height: Int
get() = wrappedNonNull.lookaheadDelegate!!.measureResult.height
override val alignmentLines: Map<AlignmentLine, Int> = emptyMap()
override fun placeChildren() {
with(PlacementScope) {
wrappedNonNull.lookaheadDelegate!!.place(0, 0)
}
}
}
private val passThroughMeasureResult = PassThroughMeasureResult()
// LookaheadMeasure
override fun measure(constraints: Constraints): Placeable =
with(intermediateMeasureNode) {
performingMeasure(constraints) {
wrappedNonNull.lookaheadDelegate!!.run {
measure(constraints)
targetSize = IntSize(measureResult.width, measureResult.height)
}
passThroughMeasureResult
}
}
override fun calculateAlignmentLine(alignmentLine: AlignmentLine): Int {
return calculateAlignmentAndPlaceChildAsNeeded(alignmentLine).also {
cachedAlignmentLinesMap[alignmentLine] = it
}
}
}
override fun createLookaheadDelegate(scope: LookaheadScope): LookaheadDelegate {
return lookAheadTransientMeasureNode?.let {
LookaheadDelegateForIntermediateLayoutModifier(scope, it)
} ?: LookaheadDelegateForLayoutModifierNode(scope)
}
override fun measure(constraints: Constraints): Placeable {
performingMeasure(constraints) {
with(layoutModifierNode) {
measureResult = measure(wrappedNonNull, constraints)
this@LayoutModifierNodeCoordinator
}
}
onMeasured()
return this
}
override fun minIntrinsicWidth(height: Int): Int {
return with(layoutModifierNode) {
minIntrinsicWidth(wrappedNonNull, height)
}
}
override fun maxIntrinsicWidth(height: Int): Int =
with(layoutModifierNode) {
maxIntrinsicWidth(wrappedNonNull, height)
}
override fun minIntrinsicHeight(width: Int): Int =
with(layoutModifierNode) {
minIntrinsicHeight(wrappedNonNull, width)
}
override fun maxIntrinsicHeight(width: Int): Int =
with(layoutModifierNode) {
maxIntrinsicHeight(wrappedNonNull, width)
}
override fun placeAt(
position: IntOffset,
zIndex: Float,
layerBlock: (GraphicsLayerScope.() -> Unit)?
) {
super.placeAt(position, zIndex, layerBlock)
// The coordinator only runs their placement block to obtain our position, which allows them
// to calculate the offset of an alignment line we have already provided a position for.
// No need to place our wrapped as well (we might have actually done this already in
// get(line), to obtain the position of the alignment line the coordinator currently needs
// our position in order ot know how to offset the value we provided).
if (isShallowPlacing) return
onPlaced()
PlacementScope.executeWithRtlMirroringValues(
measuredSize.width,
layoutDirection,
this
) {
measureResult.placeChildren()
}
}
override fun onLayoutModifierNodeChanged() {
super.onLayoutModifierNodeChanged()
layoutModifierNode.let { node ->
// Creates different [LookaheadDelegate]s based on the type of the modifier.
if (node.node.isKind(Nodes.IntermediateMeasure) &&
node is IntermediateLayoutModifierNode
) {
lookAheadTransientMeasureNode = node
lookaheadDelegate?.let {
updateLookaheadDelegate(
LookaheadDelegateForIntermediateLayoutModifier(it.lookaheadScope, node)
)
}
} else {
lookAheadTransientMeasureNode = null
lookaheadDelegate?.let {
updateLookaheadDelegate(
LookaheadDelegateForLayoutModifierNode(it.lookaheadScope)
)
}
}
}
}
override fun calculateAlignmentLine(alignmentLine: AlignmentLine): Int {
return lookaheadDelegate?.getCachedAlignmentLine(alignmentLine)
?: calculateAlignmentAndPlaceChildAsNeeded(alignmentLine)
}
override fun performDraw(canvas: Canvas) {
wrappedNonNull.draw(canvas)
if (layoutNode.requireOwner().showLayoutBounds) {
drawBorder(canvas, modifierBoundsPaint)
}
}
internal companion object {
val modifierBoundsPaint = Paint().also { paint ->
paint.color = Color.Blue
paint.strokeWidth = 1f
paint.style = PaintingStyle.Stroke
}
}
}
private fun LookaheadCapablePlaceable.calculateAlignmentAndPlaceChildAsNeeded(
alignmentLine: AlignmentLine
): Int {
val child = child
check(child != null) {
"Child of $this cannot be null when calculating alignment line"
}
if (measureResult.alignmentLines.containsKey(alignmentLine)) {
return measureResult.alignmentLines[alignmentLine] ?: AlignmentLine.Unspecified
}
val positionInWrapped = child[alignmentLine]
if (positionInWrapped == AlignmentLine.Unspecified) {
return AlignmentLine.Unspecified
}
// Place our wrapped to obtain their position inside ourselves.
child.isShallowPlacing = true
isPlacingForAlignment = true
replace()
child.isShallowPlacing = false
isPlacingForAlignment = false
return if (alignmentLine is HorizontalAlignmentLine) {
positionInWrapped + child.position.y
} else {
positionInWrapped + child.position.x
}
}