approachLayout

Functions summary

Modifier
Modifier.approachLayout(
    isMeasurementApproachInProgress: (lookaheadSize: IntSize) -> Boolean,
    isPlacementApproachInProgress: Placeable.PlacementScope.(lookaheadCoordinates: LayoutCoordinates) -> Boolean,
    approachMeasure: ApproachMeasureScope.(measurable: Measurable, constraints: Constraints) -> MeasureResult
)

Creates an approach layout intended to help gradually approach the destination layout calculated in the lookahead pass.

Cmn

Functions

Modifier.approachLayout

fun Modifier.approachLayout(
    isMeasurementApproachInProgress: (lookaheadSize: IntSize) -> Boolean,
    isPlacementApproachInProgress: Placeable.PlacementScope.(lookaheadCoordinates: LayoutCoordinates) -> Boolean = defaultPlacementApproachInProgress,
    approachMeasure: ApproachMeasureScope.(measurable: Measurable, constraints: Constraints) -> MeasureResult
): Modifier

Creates an approach layout intended to help gradually approach 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.

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.

isMeasurementApproachInProgress indicates whether the position is currently 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.

Once both isMeasurementApproachInProgress and isPlacementApproachInProgress return false, the system may skip approach pass until additional approach passes are necessary as indicated by isMeasurementApproachInProgress and isPlacementApproachInProgress.

IMPORTANT: 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.

import androidx.compose.animation.core.AnimationVector2D
import androidx.compose.animation.core.DeferredTargetAnimation
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.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.approachLayout
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.approachLayout` to approach
// th destination size determined by the lookahead pass. A resize animation will be kicked off
// whenever the lookahead size changes, to animate children from current size to destination
// size. Fixed constraints created based on the animation value will be used to measure
// child, so the child layout gradually changes its animated constraints until the approach
// completes.
fun Modifier.animateConstraints(
    sizeAnimation: DeferredTargetAnimation<IntSize, AnimationVector2D>,
    coroutineScope: CoroutineScope,
) =
    this.approachLayout(
        isMeasurementApproachInProgress = { lookaheadSize ->
            // Update the target of the size animation.
            sizeAnimation.updateTarget(lookaheadSize, coroutineScope)
            // Return true if the size animation has pending target change or hasn't finished
            // running.
            !sizeAnimation.isIdle
        }
    ) { measurable, _ ->
        // In the measurement approach, the goal is to gradually reach the destination size
        // (i.e. lookahead size). To achieve that, we use an animation to track the current
        // size, and animate to the destination size whenever it changes. Once the animation
        // finishes, the approach is complete.

        // First, update the target of the animation, and read the current animated size.
        val (width, height) = sizeAnimation.updateTarget(lookaheadSize, coroutineScope)
        // Then create fixed size 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) }

// Creates a size animation with a target unknown at the time of instantiation.
val sizeAnimation = remember { DeferredTargetAnimation(IntSize.VectorConverter) }
val coroutineScope = rememberCoroutineScope()
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(sizeAnimation, coroutineScope)
        .clickable { fullWidth = !fullWidth }
) {
    Box(Modifier.weight(1f).fillMaxHeight().background(Color(0xffff6f69)))
    Box(Modifier.weight(2f).fillMaxHeight().background(Color(0xffffcc5c)))
}