Loading mechanics/benchmark/tests/src/com/android/mechanics/benchmark/MotionValueBenchmark.kt +2 −2 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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 Loading mechanics/compose/src/com/android/mechanics/compose/modifier/MotionValueNode.kt +4 −3 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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, ) Loading Loading @@ -119,6 +120,6 @@ internal class MotionValueNode( } fun updateSpec(spec: MotionSpec) { motionValue.spec = spec // motionValue.spec = spec } } mechanics/src/com/android/mechanics/ComposableMotionValue.kt +51 −22 Original line number Diff line number Diff line Loading @@ -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) } } } mechanics/src/com/android/mechanics/MotionValue.kt +40 −20 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 * Loading @@ -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. Loading @@ -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 Loading Loading @@ -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, ) Loading Loading @@ -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 Loading @@ -284,7 +304,7 @@ private class ObservableComputations( override var lastSegment: SegmentData by mutableStateOf( spec.segmentAtInput(currentInput, currentDirection), this.spec.segmentAtInput(currentInput, currentDirection), referentialEqualityPolicy(), ) Loading mechanics/testing/src/com/android/mechanics/testing/ComposeMotionValueToolkit.kt +4 −4 Original line number Diff line number Diff line Loading @@ -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>, Loading @@ -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 Loading
mechanics/benchmark/tests/src/com/android/mechanics/benchmark/MotionValueBenchmark.kt +2 −2 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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 Loading
mechanics/compose/src/com/android/mechanics/compose/modifier/MotionValueNode.kt +4 −3 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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, ) Loading Loading @@ -119,6 +120,6 @@ internal class MotionValueNode( } fun updateSpec(spec: MotionSpec) { motionValue.spec = spec // motionValue.spec = spec } }
mechanics/src/com/android/mechanics/ComposableMotionValue.kt +51 −22 Original line number Diff line number Diff line Loading @@ -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) } } }
mechanics/src/com/android/mechanics/MotionValue.kt +40 −20 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 * Loading @@ -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. Loading @@ -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 Loading Loading @@ -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, ) Loading Loading @@ -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 Loading @@ -284,7 +304,7 @@ private class ObservableComputations( override var lastSegment: SegmentData by mutableStateOf( spec.segmentAtInput(currentInput, currentDirection), this.spec.segmentAtInput(currentInput, currentDirection), referentialEqualityPolicy(), ) Loading
mechanics/testing/src/com/android/mechanics/testing/ComposeMotionValueToolkit.kt +4 −4 Original line number Diff line number Diff line Loading @@ -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>, Loading @@ -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