Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fceb886d authored by Omar Miatello's avatar Omar Miatello Committed by Android (Google) Code Review
Browse files

Merge "feat(MM): Make MotionSpec declarative and state-driven (1/3)" into main

parents 89f447db 836e8b67
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -77,7 +77,7 @@ class MotionValueBenchmark {
    ): TestData {
        val inputState = mutableFloatStateOf(input)
        return TestData(
            motionValue = MotionValue(inputState::floatValue, gestureContext, spec),
            motionValue = MotionValue(inputState::floatValue, gestureContext, { spec }),
            gestureContext = gestureContext,
            input = inputState,
            spec = spec,
@@ -91,7 +91,7 @@ class MotionValueBenchmark {
        val gestureContext = DistanceGestureContext(0f, InputDirection.Max, 2f)
        val input = { 0f }

        benchmarkRule.measureRepeated { MotionValue(input, gestureContext) }
        benchmarkRule.measureRepeated { MotionValue(input, gestureContext, { MotionSpec.Empty }) }
    }

    @Test
+4 −3
Original line number Diff line number Diff line
@@ -47,6 +47,7 @@ import kotlinx.coroutines.launch
 * @param stableThreshold The threshold to determine if the motion value is stable.
 * @param debug Whether this value needs to be registered to a [MotionValueDebugger].
 */
// TODO(b/410524175) MotionValueNode will be removed in a follow up CL
internal class MotionValueNode(
    private var input: () -> Float,
    gestureContext: GestureContext,
@@ -59,9 +60,9 @@ internal class MotionValueNode(

    private val motionValue =
        MotionValue(
            currentInput = { currentInputState },
            input = { currentInputState },
            gestureContext = gestureContext,
            initialSpec = initialSpec,
            spec = { initialSpec },
            label = label,
            stableThreshold = stableThreshold,
        )
@@ -119,6 +120,6 @@ internal class MotionValueNode(
    }

    fun updateSpec(spec: MotionSpec) {
        motionValue.spec = spec
        //   motionValue.spec = spec
    }
}
+51 −22
Original line number Diff line number Diff line
@@ -18,62 +18,91 @@ package com.android.mechanics

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import com.android.mechanics.spec.MotionSpec
import com.android.mechanics.spec.builder.MotionBuilderContext
import com.android.mechanics.spec.builder.rememberMotionBuilderContext

@Composable
fun rememberMotionValue(
    input: () -> Float,
    spec: () -> MotionSpec,
    gestureContext: GestureContext,
    stableThreshold: Float = 0.01f,
    spec: () -> MotionSpec,
    label: String? = null,
    stableThreshold: Float = 0.01f,
): MotionValue {
    val motionValue =
        remember(input) {
            MotionValue(
                input,
                gestureContext,
                initialSpec = spec(),
                input = input,
                gestureContext = gestureContext,
                spec = spec,
                label = label,
                stableThreshold = stableThreshold,
            )
        }

    val currentSpec = spec()
    SideEffect {
        // New spec is intentionally only applied after recomposition.
        motionValue.spec = currentSpec
    }

    LaunchedEffect(motionValue) { motionValue.keepRunning() }
    return motionValue
}

@Composable
fun rememberMotionValue(
    input: () -> Float,
    gestureContext: GestureContext,
    spec: State<MotionSpec>,
    label: String? = null,
    stableThreshold: Float = 0.01f,
): MotionValue {
    return rememberMotionValue(
        input = input,
        gestureContext = gestureContext,
        spec = spec::value,
        label = label,
        stableThreshold = stableThreshold,
    )
}

@Composable
fun rememberDerivedMotionValue(
    input: MotionValue,
    spec: () -> MotionSpec,
    specProvider: () -> MotionSpec,
    stableThreshold: Float = 0.01f,
    label: String? = null,
): MotionValue {
    val motionValue =
        remember(input) {
        remember(input, specProvider) {
            MotionValue.createDerived(
                input,
                initialSpec = spec(),
                source = input,
                spec = specProvider,
                label = label,
                stableThreshold = stableThreshold,
            )
        }

    val currentSpec = spec()
    SideEffect {
        // New spec is intentionally only applied after recomposition.
        motionValue.spec = currentSpec
    }

    LaunchedEffect(motionValue) { motionValue.keepRunning() }
    return motionValue
}

/**
 * Efficiently creates and remembers a [MotionSpec], providing it via a stable lambda.
 *
 * This function memoizes the [MotionSpec] to avoid expensive recalculations. The spec is
 * re-computed only when a state dependency within the `spec` lambda changes, not on every
 * recomposition or each time the output is read.
 *
 * @param calculation A lambda with a [MotionBuilderContext] receiver that defines the [MotionSpec].
 * @return A stable provider `() -> MotionSpec`. Invoking this function is cheap as it returns the
 *   latest cached value.
 */
@Composable
fun rememberMotionSpecAsState(
    calculation: MotionBuilderContext.() -> MotionSpec
): State<MotionSpec> {
    val updatedSpec = rememberUpdatedState(calculation)
    val context = rememberMotionBuilderContext()
    return remember(context) { derivedStateOf { updatedSpec.value(context) } }
}
+40 −20
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.mechanics

import androidx.compose.runtime.FloatState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableLongStateOf
@@ -81,9 +82,17 @@ import kotlinx.coroutines.withContext
 *
 * ## Updating the MotionSpec
 *
 * The [spec] property can be changed at any time. If the new spec produces a different output for
 * the current input, the difference will be animated using the spring parameters defined in
 * [MotionSpec.resetSpring].
 * You can provide a new [MotionSpec] at any time. If the new spec produces a different output value
 * for the current input, the change will be animated smoothly using the spring parameters defined
 * in `[MotionSpec.resetSpring]`.
 *
 * **Important**: The function that provides the spec may be called frequently (for instance, on
 * every frame). To avoid performance issues from re-computing the spec, **you are responsible for
 * caching the result**.
 *
 * For use **in composition**, you can use the [rememberMotionSpecAsState] utility. This composable
 * automatically handles caching, ensuring the spec is only re-created when its state dependencies
 * change.
 *
 * ## Gesture Context
 *
@@ -93,9 +102,9 @@ import kotlinx.coroutines.withContext
 *
 * ## Usage
 *
 * The [MotionValue] does animate the [output] implicitly, whenever a change in [currentInput],
 * [spec], or [gestureContext] requires it. The animated value is computed whenever the [output]
 * property is read, or the latest once the animation frame is complete.
 * The [MotionValue] does animate the [output] implicitly, whenever a change in [input], [spec], or
 * [gestureContext] requires it. The animated value is computed whenever the [output] property is
 * read, or the latest once the animation frame is complete.
 * 1. Create an instance, providing the input value, gesture context, and an initial spec.
 * 2. Call [keepRunning] in a coroutine scope, and keep the coroutine running while the
 *    `MotionValue` is in use.
@@ -104,24 +113,33 @@ import kotlinx.coroutines.withContext
 * Internally, the [keepRunning] coroutine is automatically suspended if there is nothing to
 * animate.
 *
 * @param currentInput Provides the current input value.
 * @param gestureContext The [GestureContext] augmenting the [currentInput].
 * @param input Provides the current input value.
 * @param gestureContext The [GestureContext] augmenting the current input.
 * @param spec Provides the current [MotionSpec]. **Important**: For performance, this should be a
 *   stable provider. In composition, it's strongly recommended to use an helper like
 *   [rememberMotionSpecAsState] to create the spec.
 * @param label An optional label to aid in debugging.
 * @param stableThreshold A threshold value (in output units) that determines when the
 *   [MotionValue]'s internal spring animation is considered stable.
 */
class MotionValue(
    currentInput: () -> Float,
    input: () -> Float,
    gestureContext: GestureContext,
    initialSpec: MotionSpec = MotionSpec.Empty,
    spec: () -> MotionSpec,
    label: String? = null,
    stableThreshold: Float = StableThresholdEffect,
) : FloatState {
    private val impl =
        ObservableComputations(currentInput, gestureContext, initialSpec, stableThreshold, label)
        ObservableComputations(
            inputProvider = input,
            gestureContext = gestureContext,
            specProvider = spec,
            stableThreshold = stableThreshold,
            label = label,
        )

    /** The [MotionSpec] describing the mapping of this [MotionValue]'s input to the output. */
    var spec: MotionSpec by impl::spec
    val spec: MotionSpec by impl::spec

    /** Animated [output] value. */
    val output: Float by impl::output
@@ -202,14 +220,14 @@ class MotionValue(
        /** Creates a [MotionValue] whose [currentInput] is the animated [output] of [source]. */
        fun createDerived(
            source: MotionValue,
            initialSpec: MotionSpec = MotionSpec.Empty,
            spec: () -> MotionSpec,
            label: String? = null,
            stableThreshold: Float = 0.01f,
        ): MotionValue {
            return MotionValue(
                currentInput = source::output,
                input = { source.output },
                gestureContext = source.impl.gestureContext,
                initialSpec = initialSpec,
                spec = derivedStateOf(calculation = spec)::value,
                label = label,
                stableThreshold = stableThreshold,
            )
@@ -259,18 +277,20 @@ class MotionValue(
}

private class ObservableComputations(
    val input: () -> Float,
    private val inputProvider: () -> Float,
    val gestureContext: GestureContext,
    initialSpec: MotionSpec = MotionSpec.Empty,
    private val specProvider: () -> MotionSpec,
    override val stableThreshold: Float,
    override val label: String?,
) : Computations() {

    // ----  CurrentFrameInput ---------------------------------------------------------------------

    override var spec by mutableStateOf(initialSpec)
    override val spec
        get() = specProvider.invoke()

    override val currentInput: Float
        get() = input.invoke()
        get() = inputProvider.invoke()

    override val currentDirection: InputDirection
        get() = gestureContext.direction
@@ -284,7 +304,7 @@ private class ObservableComputations(

    override var lastSegment: SegmentData by
        mutableStateOf(
            spec.segmentAtInput(currentInput, currentDirection),
            this.spec.segmentAtInput(currentInput, currentDirection),
            referentialEqualityPolicy(),
        )

+4 −4
Original line number Diff line number Diff line
@@ -118,7 +118,7 @@ data object ComposeMotionValueToolkit : MotionValueToolkit<MotionValue, Distance
private class ComposeMotionValueTestHarness(
    initialInput: Float,
    initialDirection: InputDirection,
    spec: MotionSpec,
    override var spec: MotionSpec,
    stableThreshold: Float,
    directionChangeSlop: Float,
    val onFrame: StateFlow<Long>,
@@ -131,10 +131,10 @@ private class ComposeMotionValueTestHarness(

    override val underTest =
        MotionValue(
            { input },
            gestureContext,
            input = { input },
            gestureContext = gestureContext,
            spec = { spec },
            stableThreshold = stableThreshold,
            initialSpec = spec,
        )

    val derived = createDerived(underTest)
Loading