All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.androidx.compose.ui.node.LayoutModifierNodeCoordinator.kt Maven / Gradle / Ivy

Go to download

Compose UI primitives. This library contains the primitives that form the Compose UI Toolkit, such as drawing, measurement and layout.

There is a newer version: 1.8.0-alpha01
Show newest version
/*
 * 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.graphics.layer.GraphicsLayer
import androidx.compose.ui.internal.checkPrecondition
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.ApproachLayoutModifierNode
import androidx.compose.ui.layout.ApproachMeasureScopeImpl
import androidx.compose.ui.layout.HorizontalAlignmentLine
import androidx.compose.ui.layout.LayoutModifier
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset

@OptIn(ExperimentalComposeUiApi::class)
internal class LayoutModifierNodeCoordinator(
    layoutNode: LayoutNode,
    measureNode: LayoutModifierNode,
) : NodeCoordinator(layoutNode) {

    var layoutModifierNode: LayoutModifierNode = measureNode
        internal set(value) {
            if (value != field) {
                // Opt for a cheaper type check (via bit operation) before casting, as we anticipate
                // the node to not be ApproachLayoutModifierNode in most cases.
                if (value.node.isKind(Nodes.ApproachMeasure)) {
                    value as ApproachLayoutModifierNode
                    approachMeasureScope = approachMeasureScope?.also {
                        it.approachNode = value
                    } ?: ApproachMeasureScopeImpl(this, value)
                } else {
                    approachMeasureScope = null
                }
            }
            field = value
        }

    override val tail: Modifier.Node
        get() = layoutModifierNode.node

    val wrappedNonNull: NodeCoordinator get() = wrapped!!

    internal var lookaheadConstraints: Constraints? = null

    override var lookaheadDelegate: LookaheadDelegate? =
        if (layoutNode.lookaheadRoot != null) LookaheadDelegateForLayoutModifierNode() else null

    /**
     * Lazily initialized IntermediateMeasureScope. This is only initialized when the
     * current modifier is an ApproachLayoutModifierNode.
     */
    private var approachMeasureScope: ApproachMeasureScopeImpl? =
    // Opt for a cheaper type check (via bit operation) before casting, as we anticipate
        // the node to not be ApproachLayoutModifierNode in most cases.
        if (measureNode.node.isKind(Nodes.ApproachMeasure)) {
            ApproachMeasureScopeImpl(this, measureNode as ApproachLayoutModifierNode)
        } 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 :
        LookaheadDelegate(this@LayoutModifierNodeCoordinator) {
        // LookaheadMeasure
        override fun measure(constraints: Constraints): Placeable =
            performingMeasure(constraints) {
                [email protected] = constraints
                with([email protected]) {
                    measure(
                        // This allows `measure` calls in the modifier to be redirected to
                        // calling lookaheadMeasure in wrapped.
                        this@LayoutModifierNodeCoordinator.wrappedNonNull.lookaheadDelegate!!,
                        constraints
                    )
                }
            }

        override fun calculateAlignmentLine(alignmentLine: AlignmentLine): Int {
            return calculateAlignmentAndPlaceChildAsNeeded(alignmentLine).also {
                cachedAlignmentLinesMap[alignmentLine] = it
            }
        }

        override fun minIntrinsicWidth(height: Int): Int =
            with([email protected]) {
                minIntrinsicWidth(
                    this@LayoutModifierNodeCoordinator.wrappedNonNull.lookaheadDelegate!!,
                    height
                )
            }

        override fun maxIntrinsicWidth(height: Int): Int =
            with([email protected]) {
                maxIntrinsicWidth(
                    this@LayoutModifierNodeCoordinator.wrappedNonNull.lookaheadDelegate!!,
                    height
                )
            }

        override fun minIntrinsicHeight(width: Int): Int =
            with([email protected]) {
                minIntrinsicHeight(
                    this@LayoutModifierNodeCoordinator.wrappedNonNull.lookaheadDelegate!!,
                    width
                )
            }

        override fun maxIntrinsicHeight(width: Int): Int =
            with([email protected]) {
                maxIntrinsicHeight(
                    this@LayoutModifierNodeCoordinator.wrappedNonNull.lookaheadDelegate!!,
                    width
                )
            }
    }

    override fun ensureLookaheadDelegateCreated() {
        if (lookaheadDelegate == null) {
            lookaheadDelegate = LookaheadDelegateForLayoutModifierNode()
        }
    }

    override fun measure(constraints: Constraints): Placeable {
        @Suppress("NAME_SHADOWING")
        val constraints = if (forceMeasureWithLookaheadConstraints) {
            requireNotNull(lookaheadConstraints) {
                "Lookahead constraints cannot be null in approach pass."
            }
        } else {
            constraints
        }
        performingMeasure(constraints) {
            measureResult = approachMeasureScope?.let { scope ->
                // approachMeasureScope is created/updated when layoutModifierNode is set. An
                // ApproachLayoutModifierNode will lead to a non-null approachMeasureScope.
                with(scope.approachNode) {
                    scope.approachMeasureRequired = isMeasurementApproachInProgress(
                        scope.lookaheadSize
                    ) || constraints != lookaheadConstraints
                    if (!scope.approachMeasureRequired) {
                        // In the future we'll skip the invocation of this measure block when
                        // no approach is needed. For now, we'll ignore the constraints change
                        // in the measure block when it's declared approach complete.
                        wrappedNonNull.forceMeasureWithLookaheadConstraints = true
                    }
                    val result = scope.approachMeasure(wrappedNonNull, constraints)
                    wrappedNonNull.forceMeasureWithLookaheadConstraints = false
                    val reachedLookaheadSize = result.width == lookaheadDelegate!!.width &&
                        result.height == lookaheadDelegate!!.height
                    if (!scope.approachMeasureRequired &&
                        wrappedNonNull.size == wrappedNonNull.lookaheadDelegate?.size &&
                        !reachedLookaheadSize
                    ) {
                        object : MeasureResult by result {
                            override val width = lookaheadDelegate!!.width
                            override val height = lookaheadDelegate!!.height
                        }
                    } else {
                        result
                    }
                }
            } ?: with(layoutModifierNode) {
                measure(wrappedNonNull, constraints)
            }
            this@LayoutModifierNodeCoordinator
        }
        onMeasured()
        return this
    }

    override fun minIntrinsicWidth(height: Int): Int =
        approachMeasureScope?.run {
            with(approachNode) {
                minApproachIntrinsicWidth([email protected], height)
            }
        } ?: with(layoutModifierNode) {
            minIntrinsicWidth(wrappedNonNull, height)
        }

    override fun maxIntrinsicWidth(height: Int): Int =
        approachMeasureScope?.run {
            with(approachNode) {
                maxApproachIntrinsicWidth([email protected], height)
            }
        } ?: with(layoutModifierNode) {
            maxIntrinsicWidth(wrappedNonNull, height)
        }

    override fun minIntrinsicHeight(width: Int): Int =
        approachMeasureScope?.run {
            with(approachNode) {
                minApproachIntrinsicHeight([email protected], width)
            }
        } ?: with(layoutModifierNode) {
            minIntrinsicHeight(wrappedNonNull, width)
        }

    override fun maxIntrinsicHeight(width: Int): Int =
        approachMeasureScope?.run {
            with(approachNode) {
                maxApproachIntrinsicHeight([email protected], width)
            }
        } ?: with(layoutModifierNode) {
            maxIntrinsicHeight(wrappedNonNull, width)
        }

    override fun placeAt(
        position: IntOffset,
        zIndex: Float,
        layer: GraphicsLayer
    ) {
        super.placeAt(position, zIndex, layer)
        onAfterPlaceAt()
    }

    override fun placeAt(
        position: IntOffset,
        zIndex: Float,
        layerBlock: (GraphicsLayerScope.() -> Unit)?
    ) {
        super.placeAt(position, zIndex, layerBlock)
        onAfterPlaceAt()
    }

    private fun onAfterPlaceAt() {
        // 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()
        approachMeasureScope?.let {
            with(it.approachNode) {
                val approachComplete = with(placementScope) {
                    !isPlacementApproachInProgress(
                        lookaheadDelegate!!.lookaheadLayoutCoordinates
                    ) && !it.approachMeasureRequired &&
                        size == lookaheadDelegate?.size &&
                        wrappedNonNull.size == wrappedNonNull.lookaheadDelegate?.size
                }
                wrappedNonNull.forcePlaceWithLookaheadOffset = approachComplete
            }
        }
        measureResult.placeChildren()
        wrappedNonNull.forcePlaceWithLookaheadOffset = false
    }

    override fun calculateAlignmentLine(alignmentLine: AlignmentLine): Int {
        return lookaheadDelegate?.getCachedAlignmentLine(alignmentLine)
            ?: calculateAlignmentAndPlaceChildAsNeeded(alignmentLine)
    }

    override fun performDraw(canvas: Canvas, graphicsLayer: GraphicsLayer?) {
        wrappedNonNull.draw(canvas, graphicsLayer)
        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
    checkPrecondition(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
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy