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

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

Make MotionValueCollection state driven by animation clock only

Since the latching behavior guarantees that changes are processed once
per frame, in withFrameNanos, all the internal MutableState can be
removed. It keeps the output as MutableState, and read-observes the
input only while the animations are paused.

The failing tests was not correct previously (since it created the spec
on every read), that was only surfaced with this change, not caused by
it.

Test: Unit test
Bug: 404975104
Flag: com.android.systemui.scene_container
Change-Id: I01e99ba09e21f7499ed31fbf40c239af0f98fcb7
parent 4c974c8b
Loading
Loading
Loading
Loading
+76 −141
Original line number Diff line number Diff line
@@ -19,10 +19,8 @@ package com.android.mechanics
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.mutableStateSetOf
import androidx.compose.runtime.referentialEqualityPolicy
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.withFrameNanos
@@ -91,15 +89,9 @@ class MotionValueCollection(
            check(!isActive) { "MotionValueCollection($label) is already running" }
            isActive = true

            // 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
            var capturedDirection = currentDirection
            currentInput = input.invoke()
            currentGestureDragOffset = gestureContext.dragOffset
            currentDirection = gestureContext.direction

            managedComputations.forEach { it.onActivate() }

@@ -113,57 +105,34 @@ class MotionValueCollection(
                var isAnimatingUninterrupted = false

                while (true) {

                    var scheduleNextFrame = false
                    withFrameNanos { frameTimeNanos ->
                        frameCount++

                        currentAnimationTimeNanos = frameTimeNanos
                        lastFrameTimeNanos = capturedFrameTimeNanos
                        lastInput = capturedInput
                        lastGestureDragOffset = capturedGestureDragOffset
                        lastFrameTimeNanos = currentAnimationTimeNanos
                        lastInput = currentInput
                        lastDirection = currentDirection
                        lastGestureDragOffset = currentGestureDragOffset

                        currentAnimationTimeNanos = frameTimeNanos
                        currentInput = input.invoke()
                        currentDirection = gestureContext.direction
                        currentGestureDragOffset = gestureContext.dragOffset

                        managedComputations.forEach { it.onFrameStart() }
                    }

                    // At this point, the complete frame is done (including layout, drawing and
                    // everything else), and this MotionValue has been updated.

                    // Capture the `current*` MotionValue state, so that it can be applied as the
                    // `last*` state when the next frame starts. Its imperative to capture at this
                    // point
                    // already (since the input could change before the next frame starts), while at
                    // the
                    // same time not already applying the `last*` state (as this would cause a
                    // re-computation if the current state is being read before the next frame).

                    var scheduleNextFrame = false
                    managedComputations.forEach {
                        if (it.onFrameEnd(isAnimatingUninterrupted)) {
                        if (
                            lastInput != currentInput ||
                                lastDirection != currentDirection ||
                                lastGestureDragOffset != currentGestureDragOffset
                        ) {
                            scheduleNextFrame = true
                        }
                    }

                    if (capturedInput != currentInput) {
                        capturedInput = currentInput
                        managedComputations.forEach {
                            if (it.onFrameStart(isAnimatingUninterrupted)) {
                                scheduleNextFrame = true
                            }

                    if (capturedGestureDragOffset != currentGestureDragOffset) {
                        capturedGestureDragOffset = currentGestureDragOffset
                        scheduleNextFrame = true
                        }

                    if (capturedDirection != currentDirection) {
                        capturedDirection = currentDirection
                        scheduleNextFrame = true
                    }

                    capturedFrameTimeNanos = currentAnimationTimeNanos

                    isAnimatingUninterrupted = scheduleNextFrame
                    if (scheduleNextFrame) {
                        continue
@@ -181,9 +150,9 @@ class MotionValueCollection(
                                hasComputations &&
                                    (activeComputations != managedComputations ||
                                        activeComputations.any { it.wantWakeup() } ||
                                        input.invoke() != capturedInput ||
                                        gestureContext.direction != capturedDirection ||
                                        gestureContext.dragOffset != capturedGestureDragOffset)
                                        input.invoke() != currentInput ||
                                        gestureContext.direction != currentDirection ||
                                        gestureContext.dragOffset != currentGestureDragOffset)
                            wakeup
                        }
                        .first { it }
@@ -199,23 +168,25 @@ 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)
    internal var currentAnimationTimeNanos = -1L
        private set

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

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

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

    internal var lastFrameTimeNanos by mutableLongStateOf(-1L)
    internal var lastInput by mutableFloatStateOf(currentInput)
    internal var lastGestureDragOffset by mutableFloatStateOf(currentGestureDragOffset)
    internal var lastFrameTimeNanos = -1L
    internal var lastInput = currentInput
    internal var lastGestureDragOffset = currentGestureDragOffset
    internal var lastDirection = currentDirection

    // ---- Testing related state ------------------------------------------------------------------

@@ -267,13 +238,16 @@ internal class ManagedMotionComputation(
    /** Whether an animation is currently running. */
    override var isStable: Boolean by mutableStateOf(false)

    override val spec
        get() = specProvider.invoke()
    override var spec: MotionSpec = specProvider.invoke()
        private set

    override fun <T> get(key: SemanticKey<T>): T? = computedSemanticState(key)
    override fun <T> get(key: SemanticKey<T>): T? {
        val segment = capturedComputedValues.segment
        return segment.spec.semanticState(key, segment.key)
    }

    override val segmentKey: SegmentKey
        get() = currentComputedValues.segment.key
        get() = capturedComputedValues.segment.key

    override val floatValue: Float
        get() = output
@@ -327,36 +301,25 @@ internal class ManagedMotionComputation(
    override val currentAnimationTimeNanos
        get() = owner.currentAnimationTimeNanos

    // ----  LastFrameState ---------------------------------------------------------------------
    private var capturedComputedValues: ComputedValues = currentComputedValues
    private var capturedSpringState: SpringState = currentSpringState

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

    override var lastGuaranteeState: GuaranteeState
        get() = GuaranteeState(_lastGuaranteeStatePacked)
        set(value) {
            _lastGuaranteeStatePacked = value.packedValue
        }
    private var lastComputedValues: ComputedValues = capturedComputedValues

    private var _lastGuaranteeStatePacked: Long by
        mutableLongStateOf(GuaranteeState.Inactive.packedValue)
    override val lastSegment: SegmentData
        get() = lastComputedValues.segment

    override var lastAnimation: DiscontinuityAnimation by
        mutableStateOf(DiscontinuityAnimation.None, referentialEqualityPolicy())
    override val lastGuaranteeState: GuaranteeState
        get() = lastComputedValues.guarantee

    override var directMappedVelocity: Float = 0f
    override val lastAnimation: DiscontinuityAnimation
        get() = lastComputedValues.animation

    override var lastSpringState: SpringState
        get() = SpringState(_lastSpringStatePacked)
        set(value) {
            _lastSpringStatePacked = value.packedValue
        }
    override var lastSpringState: SpringState = SpringState.AtRest

    private var _lastSpringStatePacked: Long by
        mutableLongStateOf(lastAnimation.springStartState.packedValue)
    override var directMappedVelocity: Float = 0f

    override val lastFrameTimeNanos
        get() = owner.lastFrameTimeNanos
@@ -371,22 +334,13 @@ internal class ManagedMotionComputation(

    var debugInspector: DebugInspector? = null

    // 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.
    var capturedSegment = currentComputedValues.segment
    var capturedGuaranteeState = currentComputedValues.guarantee
    var capturedAnimation = currentComputedValues.animation
    var capturedSpringState = currentSpringState

    fun onActivate() {
        val currentComputedValues = currentComputedValues
        capturedSegment = currentComputedValues.segment
        capturedGuaranteeState = currentComputedValues.guarantee
        capturedAnimation = currentComputedValues.animation
        capturedComputedValues = currentComputedValues
        capturedSpringState = currentSpringState
        lastComputedValues = capturedComputedValues
        lastSpringState = capturedSpringState

        onFrameStart()
        onFrameStart(isAnimatingUninterrupted = false)

        debugInspector?.isAnimating = true
        debugInspector?.isActive = true
@@ -397,49 +351,29 @@ internal class ManagedMotionComputation(
        debugInspector?.isActive = false
    }

    fun onFrameStart() {
        lastSegment = capturedSegment
        lastGuaranteeState = capturedGuaranteeState
        lastAnimation = capturedAnimation
    fun onFrameStart(isAnimatingUninterrupted: Boolean): Boolean {
        spec = specProvider.invoke()
        if (isSameSegmentAndAtRest) {
            outputTarget = lastSegment.mapping.map(currentInput)
            output = outputTarget
            isStable = true
        } else {
            lastComputedValues = capturedComputedValues
            lastSpringState = capturedSpringState

        output = computedOutput
        outputTarget = computedOutputTarget
        isStable = computedIsStable
            capturedComputedValues = currentComputedValues
            capturedSpringState = currentSpringState

            outputTarget = capturedComputedValues.segment.mapping.map(currentInput)
            output = outputTarget + capturedSpringState.displacement
            isStable = capturedSpringState == SpringState.AtRest
        }

    fun onFrameEnd(isAnimatingUninterrupted: Boolean): Boolean {
        directMappedVelocity =
            if (isAnimatingUninterrupted) {
                computeDirectMappedVelocity(currentAnimationTimeNanos - lastFrameTimeNanos)
            } else 0f

        var scheduleNextFrame = false
        if (!isSameSegmentAndAtRest) {
            // Read currentComputedValues only once and update it, if necessary
            val currentValues = currentComputedValues

            if (capturedSegment != currentValues.segment) {
                capturedSegment = currentValues.segment
                scheduleNextFrame = true
            }

            if (capturedGuaranteeState != currentValues.guarantee) {
                capturedGuaranteeState = currentValues.guarantee
                scheduleNextFrame = true
            }

            if (capturedAnimation != currentValues.animation) {
                capturedAnimation = currentValues.animation
                scheduleNextFrame = true
            }

            if (capturedSpringState != currentSpringState) {
                capturedSpringState = currentSpringState
                scheduleNextFrame = true
            }
        }

        debugInspector?.run {
            frame =
                FrameData(
@@ -448,16 +382,17 @@ internal class ManagedMotionComputation(
                    currentGestureDragOffset,
                    currentAnimationTimeNanos,
                    capturedSpringState,
                    capturedSegment,
                    capturedAnimation,
                    capturedComputedValues.segment,
                    capturedComputedValues.animation,
                    computedIsOutputFixed,
                )
        }

        return scheduleNextFrame
        return lastSpringState != capturedSpringState ||
            lastComputedValues != capturedComputedValues
    }

    fun wantWakeup(): Boolean {
        return spec != capturedSegment.spec
        return specProvider.invoke() != capturedComputedValues.segment.spec
    }
}
+52 −30
Original line number Diff line number Diff line
@@ -11,7 +11,9 @@
    128,
    144,
    160,
    176
    176,
    192,
    208
  ],
  "features": [
    {
@@ -19,10 +21,12 @@
      "type": "float",
      "data_points": [
        0,
        0.6,
        1.2,
        1.8000001,
        2.4,
        0.5,
        1,
        1.5,
        2,
        2.5,
        3,
        3,
        3,
        3,
@@ -47,6 +51,8 @@
        "Max",
        "Max",
        "Max",
        "Max",
        "Max",
        "Max"
      ]
    },
@@ -56,15 +62,17 @@
      "data_points": [
        0,
        0,
        0.009148657,
        0.11382812,
        0.27218753,
        0.43603384,
        0.58219516,
        0.70204157,
        0.7947967,
        0.8634457,
        0.91236067,
        0,
        0.0696004,
        0.21705192,
        0.38261998,
        0.536185,
        0.6651724,
        0.7667498,
        0.8429822,
        0.89796525,
        1,
        1,
        1
      ]
    },
@@ -83,6 +91,8 @@
        1,
        1,
        1,
        1,
        1,
        1
      ]
    },
@@ -101,6 +111,8 @@
        false,
        false,
        false,
        true,
        true,
        true
      ]
    },
@@ -108,6 +120,8 @@
      "name": "primary-isAnimating",
      "type": "boolean",
      "data_points": [
        false,
        true,
        true,
        true,
        true,
@@ -126,17 +140,19 @@
      "name": "second-output",
      "type": "float",
      "data_points": [
        0,
        0,
        0,
        0,
        0.3957839,
        0.8722446,
        1.2746727,
        1.5661739,
        1.7578185,
        1.8746462,
        2,
        1,
        1,
        1,
        1,
        1,
        1.0696003,
        1.217052,
        1.38262,
        1.536185,
        1.6651723,
        1.7667497,
        1.8429822,
        1.8979652,
        2
      ]
    },
@@ -144,10 +160,12 @@
      "name": "second-outputTarget",
      "type": "float",
      "data_points": [
        0,
        0,
        0,
        0,
        1,
        1,
        1,
        1,
        2,
        2,
        2,
        2,
        2,
@@ -172,7 +190,9 @@
        false,
        false,
        false,
        true,
        false,
        false,
        false,
        true
      ]
    },
@@ -180,6 +200,8 @@
      "name": "second-isAnimating",
      "type": "boolean",
      "data_points": [
        false,
        true,
        true,
        true,
        true,
+4 −7
Original line number Diff line number Diff line
@@ -65,15 +65,12 @@ class MotionValueCollectionTest : MotionBuilderContext by FakeMotionSpecBuilderC
        goldenTest(
            spec = specBuilder(Mapping.Zero) { fixedValue(breakpoint = 1f, value = 1f) },
            createDerived = {
                listOf(
                    it.create(
                        { specBuilder(Mapping.Zero) { fixedValue(breakpoint = 2f, value = 2f) } },
                        "second",
                    )
                )
                val secondSpec =
                    specBuilder(Mapping.One) { fixedValue(breakpoint = 2f, value = 2f) }
                listOf(it.create({ secondSpec }, "second"))
            },
        ) {
            animateValueTo(3f)
            animateValueTo(3f, changePerFrame = 0.5f)
            awaitStable()
        }