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() }
}
}
}
}