{% setvar book_path %}/reference/androidx/_book.yaml{% endsetvar %} {% include "_shared/_reference-head-tags.html" %}

LookaheadScope

@ExperimentalComposeUiApi
public interface LookaheadScope

Known direct subclasses
IntermediateMeasureScope

IntermediateMeasureScope provides access to lookahead results to allow intermediateLayout to leverage lookahead results to define intermediate measurements and placements to gradually converge with lookahead.


LookaheadScope provides a receiver scope for all (direct and indirect) child layouts in LookaheadScope. This receiver scope allows access to lookaheadScopeCoordinates from any child's Placeable.PlacementScope. It also allows any child to convert LayoutCoordinates (which can be retrieved in Placeable.PlacementScope) to LookaheadLayoutCoordinates using toLookaheadCoordinates.

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector2D
import androidx.compose.animation.core.VectorConverter
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.getValue
import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.intermediateLayout
import androidx.compose.ui.layout.layout
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round

// Creates a custom modifier to animate the local position of the layout within the
// given LookaheadScope, whenever the relative position changes.
fun Modifier.animatePlacementInScope(lookaheadScope: LookaheadScope) = composed {
    // Creates an offset animation
    var offsetAnimation: Animatable<IntOffset, AnimationVector2D>? by mutableStateOf(
        null
    )
    var targetOffset: IntOffset? by mutableStateOf(null)

    this.intermediateLayout { measurable, constraints ->
        val placeable = measurable.measure(constraints)
        layout(placeable.width, placeable.height) {
            // Converts coordinates of the current layout to LookaheadCoordinates
            val coordinates = coordinates
            if (coordinates != null) {
                // Calculates the target offset within the lookaheadScope
                val target = with(lookaheadScope) {
                    lookaheadScopeCoordinates
                        .localLookaheadPositionOf(coordinates)
                        .round().also { targetOffset = it }
                }

                // Uses the target offset to start an offset animation
                if (target != offsetAnimation?.targetValue) {
                    offsetAnimation?.run {
                        launch { animateTo(target) }
                    } ?: Animatable(target, IntOffset.VectorConverter).let {
                        offsetAnimation = it
                    }
                }
                // Calculates the *current* offset within the given LookaheadScope
                val placementOffset =
                    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) = requireNotNull(offsetAnimation).run { value - placementOffset }
                placeable.place(x, y)
            } else {
                placeable.place(0, 0)
            }
        }
    }
}

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)
                        .animatePlacementInScope(this)
                        .background(color, RoundedCornerShape(20))
                )
            }
        }
    }

    Box(
        modifier = Modifier
            .fillMaxSize()
            .clickable { isInColumn = !isInColumn }
    ) {
        // As the items get moved between Column and Row, their positions in LookaheadLayout
        // 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() }
        }
    }
}

Summary

Public methods

abstract @NonNull LayoutCoordinates

Returns the LayoutCoordinates of the LookaheadScope.

default @NonNull Modifier

This method is deprecated.

default @NonNull Offset

Calculates the localPosition in the Lookahead coordinate space.

abstract @NonNull Modifier

This method is deprecated. onPlaced in LookaheadLayoutScope has been deprecated.

abstract @NonNull LayoutCoordinates

Converts a LayoutCoordinates into a LayoutCoordinates in the Lookahead coordinates space.

Public methods

getLookaheadScopeCoordinates

abstract @NonNull LayoutCoordinates getLookaheadScopeCoordinates(@NonNull Placeable.PlacementScope receiver)

Returns the LayoutCoordinates of the LookaheadScope. This is only accessible from Placeable.PlacementScope (i.e. during placement time).

intermediateLayout

default @NonNull Modifier intermediateLayout(
    @NonNull Modifier receiver,
    @ExtensionFunctionType @NonNull Function4<@NonNull MeasureScope, @NonNull Measurable, @NonNull Constraints, @NonNull IntSize, @NonNull MeasureResult> measure
)

Creates an intermediate layout based on target size of the child layout calculated in the lookahead. This allows the intermediate layout to morph the child layout after lookahead through measure, in which the size of the child layout calculated from the lookahead is provided. intermediateLayout does not participate in the lookahead. It is only invoked for retroactively changing the layout based on the lookahead before the layout is drawn.

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector2D
import androidx.compose.animation.core.VectorConverter
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.intermediateLayout
import androidx.compose.ui.layout.layout
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round

// Creates a custom modifier that animates the constraints and measures child with the
// animated constraints. This modifier is built on top of `Modifier.intermediateLayout`, which
// allows access to the lookahead size of the layout. A resize animation will be kicked off
// whenever the lookahead size changes, to animate children from current size to lookahead size.
// Fixed constraints created based on the animation value will be used to measure
// child, so the child layout gradually changes its size and potentially its child's placement
// to fit within the animated constraints.
fun Modifier.animateConstraints() = composed {
    // Creates a size animation
    var sizeAnimation: Animatable<IntSize, AnimationVector2D>? by remember {
        mutableStateOf(null)
    }

    this.intermediateLayout { measurable, _ ->
        // When layout changes, the lookahead pass will calculate a new final size for the
        // child layout. This lookahead size can be used to animate the size
        // change, such that the animation starts from the current size and gradually
        // change towards `lookaheadSize`.
        if (lookaheadSize != sizeAnimation?.targetValue) {
            sizeAnimation?.run {
                launch { animateTo(lookaheadSize) }
            } ?: Animatable(lookaheadSize, IntSize.VectorConverter).let {
                sizeAnimation = it
            }
        }
        val (width, height) = sizeAnimation!!.value
        // Creates a fixed set of constraints using the animated size
        val animatedConstraints = Constraints.fixed(width, height)
        // Measure child with animated constraints.
        val placeable = measurable.measure(animatedConstraints)
        layout(placeable.width, placeable.height) {
            placeable.place(0, 0)
        }
    }
}

var fullWidth by remember { mutableStateOf(false) }
Row(
    (if (fullWidth) Modifier.fillMaxWidth() else Modifier.width(100.dp))
        .height(200.dp)
        // Use the custom modifier created above to animate the constraints passed
        // to the child, and therefore resize children in an animation.
        .animateConstraints()
        .clickable { fullWidth = !fullWidth }) {
    Box(
        Modifier
            .weight(1f)
            .fillMaxHeight()
            .background(Color.Red)
    )
    Box(
        Modifier
            .weight(2f)
            .fillMaxHeight()
            .background(Color.Yellow)
    )
}

localLookaheadPositionOf

default @NonNull Offset localLookaheadPositionOf(
    @NonNull LayoutCoordinates receiver,
    @NonNull LayoutCoordinates coordinates
)

Calculates the localPosition in the Lookahead coordinate space. This is a convenient method for 1) converting the given LayoutCoordinates to lookahead coordinates using toLookaheadCoordinates, and 2) invoking LayoutCoordinates.localPositionOf with the converted coordinates.

onPlaced

abstract @NonNull Modifier onPlaced(
    @NonNull Modifier receiver,
    @NonNull Function2<@NonNull LookaheadLayoutCoordinates, @NonNull LookaheadLayoutCoordinatesUnit> onPlaced
)

onPlaced gets invoked after the parent LayoutModifier has been placed and before child LayoutModifier is placed. This allows child LayoutModifier to adjust its own placement based on its parent.

onPlaced callback will be invoked with the LookaheadLayoutCoordinates of the LayoutNode emitted by LookaheadLayout as the first parameter, and the LookaheadLayoutCoordinates of this modifier as the second parameter. Given the LookaheadLayoutCoordinatess, both lookahead position and current position of this modifier in the LookaheadLayout's coordinates system can be calculated using LookaheadLayoutCoordinates.localLookaheadPositionOf and LookaheadLayoutCoordinates.localPositionOf, respectively.

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector2D
import androidx.compose.animation.core.VectorConverter
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.getValue
import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.intermediateLayout
import androidx.compose.ui.layout.layout
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round

// Creates a custom modifier to animate the local position of the layout within the
// given LookaheadScope, whenever the relative position changes.
fun Modifier.animatePlacementInScope(lookaheadScope: LookaheadScope) = composed {
    // Creates an offset animation
    var offsetAnimation: Animatable<IntOffset, AnimationVector2D>? by mutableStateOf(
        null
    )
    var targetOffset: IntOffset? by mutableStateOf(null)

    this.intermediateLayout { measurable, constraints ->
        val placeable = measurable.measure(constraints)
        layout(placeable.width, placeable.height) {
            // Converts coordinates of the current layout to LookaheadCoordinates
            val coordinates = coordinates
            if (coordinates != null) {
                // Calculates the target offset within the lookaheadScope
                val target = with(lookaheadScope) {
                    lookaheadScopeCoordinates
                        .localLookaheadPositionOf(coordinates)
                        .round().also { targetOffset = it }
                }

                // Uses the target offset to start an offset animation
                if (target != offsetAnimation?.targetValue) {
                    offsetAnimation?.run {
                        launch { animateTo(target) }
                    } ?: Animatable(target, IntOffset.VectorConverter).let {
                        offsetAnimation = it
                    }
                }
                // Calculates the *current* offset within the given LookaheadScope
                val placementOffset =
                    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) = requireNotNull(offsetAnimation).run { value - placementOffset }
                placeable.place(x, y)
            } else {
                placeable.place(0, 0)
            }
        }
    }
}

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)
                        .animatePlacementInScope(this)
                        .background(color, RoundedCornerShape(20))
                )
            }
        }
    }

    Box(
        modifier = Modifier
            .fillMaxSize()
            .clickable { isInColumn = !isInColumn }
    ) {
        // As the items get moved between Column and Row, their positions in LookaheadLayout
        // 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() }
        }
    }
}

toLookaheadCoordinates

abstract @NonNull LayoutCoordinates toLookaheadCoordinates(@NonNull LayoutCoordinates receiver)

Converts a LayoutCoordinates into a LayoutCoordinates in the Lookahead coordinates space. This is only applicable to child layouts within LookaheadScope.