LookaheadScope
interface LookaheadScope
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 LayoutCoordinates in lookahead
coordinate space using toLookaheadCoordinates.
Properties
val Placeable.PlacementScope.lookaheadScopeCoordinates: LayoutCoordinates
Returns the LayoutCoordinates of the LookaheadScope. This is only accessible from
Placeable.PlacementScope (i.e. during placement time).
Note: The returned coordinates is not coordinates in the lookahead coordinate space. If
the lookahead coordinates of the lookaheadScope is needed, suggest converting the returned
coordinates using toLookaheadCoordinates.
Functions
fun LayoutCoordinates.toLookaheadCoordinates(): LayoutCoordinates
Converts a LayoutCoordinates into a LayoutCoordinates in the Lookahead coordinate space.
This can be used for layouts within LookaheadScope.
fun LayoutCoordinates.localLookaheadPositionOf(
sourceCoordinates: LayoutCoordinates,
relativeToSource: Offset = Offset.Zero,
includeMotionFrameOfReference: Boolean = true,
): Offset
Converts relativeToSource in sourceCoordinates's lookahead coordinate space into local
lookahead coordinates. This is a convenient method for 1) converting both this coordinates
and sourceCoordinates into lookahead space coordinates using toLookaheadCoordinates,
and 2) invoking LayoutCoordinates.localPositionOf with the converted coordinates.
For layouts where LayoutCoordinates.introducesMotionFrameOfReference returns true (placed
under Placeable.PlacementScope.withMotionFrameOfReferencePlacement) you may pass
includeMotionFrameOfReference as false to get their position while excluding the
additional Offset.
Code Examples
LookaheadLayoutCoordinatesSample
@OptIn(ExperimentalAnimatableApi::class)
@Composable
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
}
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() }
}
}
}
}
