ApproachLayoutModifierNode
interface ApproachLayoutModifierNode : LayoutModifierNode
ApproachLayoutModifierNode
is designed to support gradually approaching the destination layout
calculated in the lookahead pass. This can be particularly helpful when the destination layout is
anticipated to change drastically and would consequently result in visual disruptions.
In order to create a smooth approach, an interpolation (often through animations) can be used in
approachMeasure
to interpolate the measurement or placement from a previously recorded size
and/or position to the destination/target size and/or position. The destination size is available
in ApproachMeasureScope
as ApproachMeasureScope.lookaheadSize
. And the target position can
also be acquired in ApproachMeasureScope
during placement by using
LookaheadScope.localLookaheadPositionOf
with the layout's
Placeable.PlacementScope.coordinates
. The sample code below illustrates how that can be
achieved.
During the lookahead pass, measure
will be invoked. By default measure
simply passes the
incoming constraints to its child, and returns the child measure result to parent without any
modification. The default behavior for measure
is simply a pass through of constraints and
measure results without modification. This can be overridden as needed. approachMeasure
will be
invoked during the approach pass after lookahead.
isMeasurementApproachInProgress
signals whether the measurement is in progress of approaching
destination size. It will be queried after the destination has been determined by the lookahead
pass, before approachMeasure
is invoked. The lookahead size is provided to
isMeasurementApproachInProgress
for convenience in deciding whether the destination size has
been reached.
isPlacementApproachInProgress
indicates whether the position is actively approaching
destination defined by the lookahead, hence it's a signal to the system for whether additional
approach placements are necessary. isPlacementApproachInProgress
will be invoked after the
destination position has been determined by lookahead pass, and before the placement phase in
approachMeasure
.
IMPORTANT: When both isMeasurementApproachInProgress
and isPlacementApproachInProgress
become false, the approach is considered complete. Approach pass will subsequently snap the
measurement and placement to lookahead measurement and placement. Once approach is complete,
approachMeasure
may never be invoked until either isMeasurementApproachInProgress
or
isPlacementApproachInProgress
becomes true again. Therefore it is important to ensure
approachMeasure
and measure
result in the same measurement and placement when the approach is
complete. Otherwise, there may be visual discontinuity when we snap the measurement and placement
to lookahead.
It is important to be accurate in isPlacementApproachInProgress
and
isMeasurementApproachInProgress
. A prolonged indication of incomplete approach will prevent the
system from potentially skipping approach pass when possible.
Functions
fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean
isMeasurementApproachInProgress
signals whether the measurement is currently approaching
destination size. It will be queried after the destination has been determined by the
lookahead pass, before approachMeasure
is invoked. The lookahead size is provided to
isMeasurementApproachInProgress
for convenience in deciding whether the destination size
has been reached.
Note: It is important to be accurate in isPlacementApproachInProgress
and
isMeasurementApproachInProgress
. A prolonged indication of incomplete approach will prevent
the system from potentially skipping approach pass when possible.
fun Placeable.PlacementScope.isPlacementApproachInProgress(
lookaheadCoordinates: LayoutCoordinates
): Boolean
isPlacementApproachInProgress
indicates whether the position is approaching destination
defined by the lookahead, hence it's a signal to the system for whether additional approach
placements are necessary. isPlacementApproachInProgress
will be invoked after the
destination position has been determined by lookahead pass, and before the placement phase in
approachMeasure
.
Note: It is important to be accurate in isPlacementApproachInProgress
and
isMeasurementApproachInProgress
. A prolonged indication of incomplete approach will prevent
the system from potentially skipping approach pass when possible.
By default, isPlacementApproachInProgress
returns false.
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints,
): MeasureResult
fun ApproachMeasureScope.approachMeasure(
measurable: Measurable,
constraints: Constraints,
): MeasureResult
approachMeasure
defines how the measurement and placement of the layout approach the
destination size and position. In order to achieve a smooth approach from the current size
and position to the destination, an interpolation (often through animations) can be used in
approachMeasure
to interpolate the measurement or placement from a previously recorded size
and position to the destination/target size and position. The destination size is available
in ApproachMeasureScope
as ApproachMeasureScope.lookaheadSize
. And the target position
can also be acquired in ApproachMeasureScope
during placement by using
LookaheadScope.localLookaheadPositionOf
with the layout's
Placeable.PlacementScope.coordinates
. Please see sample code below for how that can be
achieved.
Note: approachMeasure
is only guaranteed to be invoked when either
isMeasurementApproachInProgress
or isMeasurementApproachInProgress
is true. Otherwise,
the system will consider the approach complete (i.e. destination reached) and may skip the
approach pass when possible.
fun ApproachIntrinsicMeasureScope.minApproachIntrinsicWidth(
measurable: IntrinsicMeasurable,
height: Int,
): Int
The function used to calculate minIntrinsicWidth for the approach pass changes.
fun ApproachIntrinsicMeasureScope.minApproachIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int,
): Int
The function used to calculate minIntrinsicHeight for the approach pass changes.
fun ApproachIntrinsicMeasureScope.maxApproachIntrinsicWidth(
measurable: IntrinsicMeasurable,
height: Int,
): Int
The function used to calculate maxIntrinsicWidth for the approach pass changes.
fun ApproachIntrinsicMeasureScope.maxApproachIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int,
): Int
The function used to calculate maxIntrinsicHeight for the approach pass changes.
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() }
}
}
}
}