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

Commit af260059 authored by Mike Schneider's avatar Mike Schneider
Browse files

Latch input and output of MotionValueCollection on the frame start

This makes sure the new MotionValueCollection reads the input and
computes the output only exactly once, at the start of the frame.
The output is written to a MutableState, debouncing it from
fast-changing inputs.

Test: Unit tests
Bug: 404975104
Flag: com.android.systemui.scene_container
Change-Id: I6d0a1a8ded087a4a7683f57d6cc81062795f542d
parent fa32ab42
Loading
Loading
Loading
Loading
+8 −8
Original line number Diff line number Diff line
@@ -144,7 +144,7 @@ class MotionValue(
    @get:FrequentlyChangingValue val spec: MotionSpec by impl::spec

    /** Animated [output] value. */
    @get:FrequentlyChangingValue override val output: Float by impl::output
    @get:FrequentlyChangingValue override val output: Float by impl::computedOutput

    /**
     * [output] value, but without animations.
@@ -154,14 +154,14 @@ class MotionValue(
     * While [isStable], [outputTarget] and [output] are the same value.
     */
    // TODO(b/441041846): This should not change frequently
    @get:FrequentlyChangingValue override val outputTarget: Float by impl::outputTarget
    @get:FrequentlyChangingValue override val outputTarget: Float by impl::computedOutputTarget

    /** The [output] exposed as [FloatState]. */
    @get:FrequentlyChangingValue override val floatValue: Float by impl::output
    @get:FrequentlyChangingValue override val floatValue: Float by impl::computedOutput

    /** Whether an animation is currently running. */
    // TODO(b/441041846): This should not change frequently
    @get:FrequentlyChangingValue override val isStable: Boolean by impl::isStable
    @get:FrequentlyChangingValue override val isStable: Boolean by impl::computedIsStable

    /**
     * Whether the output can change its value.
@@ -172,7 +172,7 @@ class MotionValue(
     * changes. This can be used to avoid unnecessary work like recomposition or re-measurement.
     */
    // TODO(b/441041846): This should not change frequently
    @get:FrequentlyChangingValue val isOutputFixed: Boolean by impl::isOutputFixed
    @get:FrequentlyChangingValue val isOutputFixed: Boolean by impl::computedIsOutputFixed

    /**
     * The current value for the [SemanticKey].
@@ -182,7 +182,7 @@ class MotionValue(
    // TODO(b/441041846): This should not change frequently
    @FrequentlyChangingValue
    override operator fun <T> get(key: SemanticKey<T>): T? {
        return impl.semanticState(key)
        return impl.computedSemanticState(key)
    }

    /** The current segment used to compute the output. */
@@ -273,7 +273,7 @@ class MotionValue(
                        impl.lastSpringState,
                        impl.lastSegment,
                        impl.lastAnimation,
                        impl.isOutputFixed,
                        impl.computedIsOutputFixed,
                    ),
                    impl.isActive,
                    impl.debugIsAnimating,
@@ -458,7 +458,7 @@ private class ObservableComputations(
                            capturedSpringState,
                            capturedSegment,
                            capturedAnimation,
                            isOutputFixed,
                            computedIsOutputFixed,
                        )
                }

+39 −14
Original line number Diff line number Diff line
@@ -51,6 +51,9 @@ sealed interface ManagedMotionValue : MotionValueState, DisposableHandle
 * A collection of motion values that all share the same input and gesture context.
 *
 * All [ManagedMotionValue]s are run from the same [keepRunning], and share the same lifecycle.
 *
 * Input, gesture context and spec are updated all at once, at the beginning of the, during
 * [withFrameNanos].
 */
class MotionValueCollection(
    internal val input: () -> Float,
@@ -86,6 +89,8 @@ class MotionValueCollection(
            // These `captured*` values will be applied to the `last*` values, at the beginning
            // of the each new frame.
            // TODO(b/397837971): Encapsulate the state in a StateRecord.
            // TODO(b/397837971): last/current values could all be updated at the beginning of the
            // frame, when latching.
            var capturedFrameTimeNanos = currentAnimationTimeNanos
            var capturedInput = currentInput
            var capturedGestureDragOffset = currentGestureDragOffset
@@ -128,7 +133,9 @@ class MotionValueCollection(
                        lastInput = capturedInput
                        lastGestureDragOffset = capturedGestureDragOffset

                        // TODO - capture input
                        currentInput = input.invoke()
                        currentDirection = gestureContext.direction
                        currentGestureDragOffset = gestureContext.dragOffset

                        activeComputations.forEach { it.onFrameStart() }
                    }
@@ -183,9 +190,9 @@ class MotionValueCollection(
                                hasComputations &&
                                    (activeComputations != managedComputations ||
                                        activeComputations.any { it.wantWakeup() } ||
                                        currentInput != capturedInput ||
                                        currentDirection != capturedDirection ||
                                        currentGestureDragOffset != capturedGestureDragOffset)
                                        input.invoke() != capturedInput ||
                                        gestureContext.direction != capturedDirection ||
                                        gestureContext.dragOffset != capturedGestureDragOffset)
                            wakeup
                        }
                        .first { it }
@@ -201,20 +208,20 @@ class MotionValueCollection(
    }

    // ---- Implementation - State shared with all ManagedMotionComputations  ----------------------

    // Note that all this state is updated exactly once per frame, during [withFrameNanos].
    internal var currentAnimationTimeNanos by mutableLongStateOf(-1L)

    @VisibleForTesting
    val currentInput: Float
        get() = input.invoke()
    var currentInput: Float by mutableFloatStateOf(input.invoke())
        private set

    @VisibleForTesting
    val currentDirection: InputDirection
        get() = gestureContext.direction
    var currentDirection: InputDirection by mutableStateOf(gestureContext.direction)
        private set

    @VisibleForTesting
    val currentGestureDragOffset: Float
        get() = gestureContext.dragOffset
    var currentGestureDragOffset: Float by mutableFloatStateOf(gestureContext.dragOffset)
        private set

    internal var lastFrameTimeNanos by mutableLongStateOf(-1L)
    internal var lastInput by mutableFloatStateOf(currentInput)
@@ -259,10 +266,24 @@ internal class ManagedMotionComputation(

    // ----  ManagedMotionValue --------------------------------------------------------------------

    override var output: Float by mutableFloatStateOf(Float.NaN)

    /**
     * [output] value, but without animations.
     *
     * This value always reports the target value, even before a animation is finished.
     *
     * While [isStable], [outputTarget] and [output] are the same value.
     */
    override var outputTarget: Float by mutableFloatStateOf(Float.NaN)

    /** Whether an animation is currently running. */
    override var isStable: Boolean by mutableStateOf(false)

    override val spec
        get() = specProvider.invoke()

    override fun <T> get(key: SemanticKey<T>): T? = semanticState(key)
    override fun <T> get(key: SemanticKey<T>): T? = computedSemanticState(key)

    override val segmentKey: SegmentKey
        get() = currentComputedValues.segment.key
@@ -286,7 +307,7 @@ internal class ManagedMotionComputation(
                        lastSpringState,
                        lastSegment,
                        lastAnimation,
                        isOutputFixed,
                        computedIsOutputFixed,
                    ),
                    owner.isActive,
                    owner.isAnimating,
@@ -392,6 +413,10 @@ internal class ManagedMotionComputation(
        lastGuaranteeState = capturedGuaranteeState
        lastAnimation = capturedAnimation
        lastSpringState = capturedSpringState

        output = computedOutput
        outputTarget = computedOutputTarget
        isStable = computedIsStable
    }

    fun onFrameEnd(isAnimatingUninterrupted: Boolean): Boolean {
@@ -436,7 +461,7 @@ internal class ManagedMotionComputation(
                    capturedSpringState,
                    capturedSegment,
                    capturedAnimation,
                    isOutputFixed,
                    computedIsOutputFixed,
                )
        }

+6 −6
Original line number Diff line number Diff line
@@ -149,15 +149,15 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static
                lastSegment.spec == spec &&
                lastSegment.isValidForInput(currentInput, currentDirection)

    val output: Float
    val computedOutput: Float
        get() =
            if (isSameSegmentAndAtRest) {
                lastSegment.mapping.map(currentInput)
            } else {
                outputTarget + currentSpringState.displacement
                computedOutputTarget + currentSpringState.displacement
            }

    val outputTarget: Float
    val computedOutputTarget: Float
        get() =
            if (isSameSegmentAndAtRest) {
                lastSegment.mapping.map(currentInput)
@@ -165,7 +165,7 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static
                currentComputedValues.segment.mapping.map(currentInput)
            }

    val isStable: Boolean
    val computedIsStable: Boolean
        get() =
            if (isSameSegmentAndAtRest) {
                true
@@ -180,7 +180,7 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static
     * segment with a [Mapping.Fixed], and that mapping's value has not changed from the previous
     * frame.
     */
    val isOutputFixed: Boolean
    val computedIsOutputFixed: Boolean
        get() {
            if (lastSpringState != SpringState.AtRest) {
                // The spring is still settling.
@@ -213,7 +213,7 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static
            }
        }

    fun <T> semanticState(semanticKey: SemanticKey<T>): T? {
    fun <T> computedSemanticState(semanticKey: SemanticKey<T>): T? {
        return with(if (isSameSegmentAndAtRest) lastSegment else currentComputedValues.segment) {
            spec.semanticState(semanticKey, key)
        }
+7 −7
Original line number Diff line number Diff line
@@ -69,7 +69,7 @@ constructor(
    var spec: MotionSpec by impl::spec

    /** Animated [output] value. */
    val output: Float by impl::output
    val output: Float by impl::computedOutput

    /**
     * [output] value, but without animations.
@@ -78,10 +78,10 @@ constructor(
     *
     * While [isStable], [outputTarget] and [output] are the same value.
     */
    val outputTarget: Float by impl::outputTarget
    val outputTarget: Float by impl::computedOutputTarget

    /** Whether an animation is currently running. */
    val isStable: Boolean by impl::isStable
    val isStable: Boolean by impl::computedIsStable

    /**
     * The current value for the [SemanticKey].
@@ -89,7 +89,7 @@ constructor(
     * `null` if not defined in the spec.
     */
    operator fun <T> get(key: SemanticKey<T>): T? {
        return impl.semanticState(key)
        return impl.computedSemanticState(key)
    }

    /** The current segment used to compute the output. */
@@ -140,7 +140,7 @@ constructor(
                        impl.lastSpringState,
                        impl.lastSegment,
                        impl.lastAnimation,
                        impl.isOutputFixed,
                        impl.computedIsOutputFixed,
                    ),
                    impl.isActive,
                    impl.animationFrameDriver.isRunning,
@@ -284,7 +284,7 @@ private class ImperativeComputations(
                    currentSpringState,
                    currentValues.segment,
                    currentValues.animation,
                    isOutputFixed,
                    computedIsOutputFixed,
                )
        }

@@ -300,7 +300,7 @@ private class ImperativeComputations(
            directMappedVelocity = 0f
        }

        var isAnimationFinished = isStable
        var isAnimationFinished = computedIsStable
        if (lastSegment != currentValues.segment) {
            lastSegment = currentValues.segment
            isAnimationFinished = false
+1 −2
Original line number Diff line number Diff line
@@ -29,8 +29,6 @@ import com.android.mechanics.ManagedMotionValue
import com.android.mechanics.MotionValueCollection
import com.android.mechanics.spec.InputDirection
import com.android.mechanics.spec.MotionSpec
import com.android.mechanics.testing.ComposeMotionValueToolkit.createTimeSeries
import com.android.mechanics.testing.MotionValueToolkit.Companion.FrameDuration
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -111,6 +109,7 @@ object ComposeMotionValueCollectionToolkit :
        val frameIds = mutableListOf<FrameId>()

        fun recordFrame(frameId: TimestampFrameId) {

            frameIds.add(frameId)

            collectionCapture.captureCurrentFrame {
Loading