New Compose Multiplatform components arrived on Composables UICheck it out →

LookaheadScope

[LookaheadScope] creates a scope in which all layouts will first determine their destination layout through a lookahead pass, followed by an approach pass to run the measurement and placement approach defined in [approachLayout] or [ApproachLayoutModifierNode], in order to gradually reach the destination.

Last updated:

Installation

dependencies {
   implementation("androidx.compose.ui:ui:1.7.0-beta03")
}

Overloads

@Composable
@UiComposable
fun LookaheadScope(content: @Composable @UiComposable LookaheadScope.() -> Unit)

Parameters

namedescription
contentThe child composable to be laid out.

Code Example

LookaheadLayoutCoordinatesSample

@Composable
@Sampled
@OptIn(ExperimentalAnimatableApi::class
fun LookaheadLayoutCoordinatesSample() {
    /**
     * Creates a custom implementation of ApproachLayoutModifierNode to approach the placement
     * of the layout using an animation.
     */
    class AnimatedPlacementModifierNode(
        var lookaheadScope: LookaheadScope
    ) : ApproachLayoutModifierNode, Modifier.Node() {
        // Creates an offset animation, the target of which will be known during placement.
        val offsetAnimation: DeferredTargetAnimation<IntOffset, AnimationVector2D> =
            DeferredTargetAnimation(
                IntOffset.VectorConverter
            )

        override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
            // Since we only animate the placement here, we can consider measurement approach
            // complete.
            return false
        }

        // Returns true when the offset animation is in progress, false otherwise.
        override fun Placeable.PlacementScope.isPlacementApproachInProgress(
            lookaheadCoordinates: LayoutCoordinates
        ): Boolean {
            val target = with(lookaheadScope) {
                lookaheadScopeCoordinates.localLookaheadPositionOf(lookaheadCoordinates).round()
            }
            offsetAnimation.updateTarget(target, coroutineScope)
            return !offsetAnimation.isIdle
        }

        @ExperimentalComposeUiApi
        override fun ApproachMeasureScope.approachMeasure(
            measurable: Measurable,
            constraints: Constraints
        ): MeasureResult {
            val placeable = measurable.measure(constraints)
            return layout(placeable.width, placeable.height) {
                val coordinates = coordinates
                if (coordinates != null) {
                    // Calculates the target offset within the lookaheadScope
                    val target = with(lookaheadScope) {
                        lookaheadScopeCoordinates
                            .localLookaheadPositionOf(coordinates)
                            .round()
                    }

                    // Uses the target offset to start an offset animation
                    val animatedOffset = offsetAnimation.updateTarget(target, coroutineScope)
                    // Calculates the *current* offset within the given LookaheadScope
                    val placementOffset = with(lookaheadScope) {
                        lookaheadScopeCoordinates.localPositionOf(
                            coordinates,
                            Offset.Zero
                        ).round()
                    }
                    // Calculates the delta between animated position in scope and current
                    // position in scope, and places the child at the delta offset. This puts
                    // the child layout at the animated position.
                    val (x, y) = animatedOffset - placementOffset
                    placeable.place(x, y)
                } else {
                    placeable.place(0, 0)
                }
            }
        }
    }

    // Creates a custom node element for the AnimatedPlacementModifierNode above.
    data class AnimatePlacementNodeElement(val lookaheadScope: LookaheadScope) :
        ModifierNodeElement<AnimatedPlacementModifierNode>() {

        override fun update(node: AnimatedPlacementModifierNode) {
            node.lookaheadScope = lookaheadScope
        }

        override fun create(): AnimatedPlacementModifierNode {
            return AnimatedPlacementModifierNode(lookaheadScope)
        }
    }

    val colors = listOf(
        Color(0xffff6f69), Color(0xffffcc5c), Color(0xff264653), Color(0xff2a9d84)
    )

    var isInColumn by remember { mutableStateOf(true) }
    LookaheadScope {
        // Creates movable content containing 4 boxes. They will be put either in a [Row] or in a
        // [Column] depending on the state
        val items = remember {
            movableContentOf {
                colors.forEach { color ->
                    Box(
                        Modifier
                            .padding(15.dp)
                            .size(100.dp, 80.dp)
                            .then(
                                AnimatePlacementNodeElement(this)
                            )
                            .background(color, RoundedCornerShape(20))
                    )
                }
            }
        }

        Box(
            modifier = Modifier
                .fillMaxSize()
                .clickable { isInColumn = !isInColumn }
        ) {
            // As the items get moved between Column and Row, their positions in LookaheadScope
            // will change. The `animatePlacementInScope` modifier created above will
            // observe that final position change via `localLookaheadPositionOf`, and create
            // a position animation.
            if (isInColumn) {
                Column(Modifier.fillMaxSize()) {
                    items()
                }
            } else {
                Row { items() }
            }
        }
    }
}