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

Commit c7f37e3c authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge changes I6d0a1a8d,Ib31b1fd7 into main

* changes:
  Latch input and output of MotionValueCollection on the frame start
  Adding MotionValueCollection
parents 49f8098d af260059
Loading
Loading
Loading
Loading
+13 −13
Original line number Diff line number Diff line
@@ -129,7 +129,7 @@ class MotionValue(
    spec: () -> MotionSpec,
    label: String? = null,
    stableThreshold: Float = StableThresholdEffect,
) : FloatState {
) : MotionValueState {
    private val impl =
        ObservableComputations(
            inputProvider = input,
@@ -144,7 +144,7 @@ class MotionValue(
    @get:FrequentlyChangingValue val spec: MotionSpec by impl::spec

    /** Animated [output] value. */
    @get:FrequentlyChangingValue 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 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 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].
@@ -181,14 +181,14 @@ class MotionValue(
     */
    // TODO(b/441041846): This should not change frequently
    @FrequentlyChangingValue
    operator fun <T> get(key: SemanticKey<T>): T? {
        return impl.semanticState(key)
    override operator fun <T> get(key: SemanticKey<T>): T? {
        return impl.computedSemanticState(key)
    }

    /** The current segment used to compute the output. */
    // TODO(b/441041846): This should not change frequently
    @get:FrequentlyChangingValue
    val segmentKey: SegmentKey
    override val segmentKey: SegmentKey
        get() = impl.currentComputedValues.segment.key

    /**
@@ -223,7 +223,7 @@ class MotionValue(
            impl.keepRunning { continueRunning.invoke(this@MotionValue) }
        }

    val label: String? by impl::label
    override val label: String? by impl::label

    companion object {
        /** Creates a [MotionValue] whose [currentInput] is the animated [output] of [source]. */
@@ -261,7 +261,7 @@ class MotionValue(
     *
     * The returned [DebugInspector] must be [DebugInspector.dispose]d when no longer needed.
     */
    fun debugInspector(): DebugInspector {
    override fun debugInspector(): DebugInspector {
        if (debugInspectorRefCount.getAndIncrement() == 0) {
            impl.debugInspector =
                DebugInspector(
@@ -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,
                        )
                }

+474 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

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
import com.android.mechanics.MotionValue.Companion.StableThresholdSpatial
import com.android.mechanics.debug.DebugInspector
import com.android.mechanics.debug.FrameData
import com.android.mechanics.impl.Computations
import com.android.mechanics.impl.DiscontinuityAnimation
import com.android.mechanics.impl.GuaranteeState
import com.android.mechanics.spec.InputDirection
import com.android.mechanics.spec.MotionSpec
import com.android.mechanics.spec.SegmentData
import com.android.mechanics.spec.SegmentKey
import com.android.mechanics.spec.SemanticKey
import com.android.mechanics.spring.SpringState
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext

/** The type of MotionValue created by the [MotionValueCollection]. */
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,
    internal val gestureContext: GestureContext,
    internal val stableThreshold: Float = StableThresholdSpatial,
    val label: String? = null,
) {
    private val managedComputations = mutableStateSetOf<ManagedMotionComputation>()

    /**
     * Creates a new [ManagedMotionValue], whose output is controlled by [spec].
     *
     * The returned [ManagedMotionValue] must be disposed when not used anymore, while this
     * [MotionValueCollection] is kept active.
     */
    fun create(spec: () -> MotionSpec, label: String? = null): ManagedMotionValue {
        return ManagedMotionComputation(this, spec, label).also { managedComputations.add(it) }
    }

    /**
     * Keeps the all created [ManagedMotionValue]'s animated output running.
     *
     * Clients must call [keepRunning], and keep the coroutine running while any of the created
     * [ManagedMotionValue] is in use. Cancel the coroutine if no values are being used anymore.
     *
     * Internally, this method does suspend, unless there are animations ongoing.
     */
    suspend fun keepRunning(): Nothing {
        withContext(CoroutineName("MotionValueCollection($label)")) {
            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

            val activeComputations = mutableSetOf<ManagedMotionComputation>()
            managedComputations.forEach {
                it.onActivate()
                activeComputations.add(it)
            }
            activeComputationCount = activeComputations.size

            try {
                isAnimating = true

                // indicates whether withFrameNanos is called continuously (as opposed to being
                // suspended for an undetermined amount of time in between withFrameNanos).
                // This is essential after `withFrameNanos` returned: if true at this point,
                // currentAnimationTimeNanos - lastFrameNanos is the duration of the last frame.
                var isAnimatingUninterrupted = false

                while (true) {

                    withFrameNanos { frameTimeNanos ->
                        val addedComputations = managedComputations - activeComputations
                        val removedComputations = activeComputations - managedComputations
                        addedComputations.forEach {
                            it.onActivate()
                            activeComputations.add(it)
                        }
                        removedComputations.forEach {
                            it.onDeactivate()
                            activeComputations.remove(it)
                        }
                        activeComputationCount = activeComputations.size
                        frameCount++

                        currentAnimationTimeNanos = frameTimeNanos
                        lastFrameTimeNanos = capturedFrameTimeNanos
                        lastInput = capturedInput
                        lastGestureDragOffset = capturedGestureDragOffset

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

                        activeComputations.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
                    activeComputations.forEach {
                        if (it.onFrameEnd(isAnimatingUninterrupted)) {
                            scheduleNextFrame = true
                        }
                    }

                    if (capturedInput != currentInput) {
                        capturedInput = currentInput
                        scheduleNextFrame = true
                    }

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

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

                    capturedFrameTimeNanos = currentAnimationTimeNanos

                    isAnimatingUninterrupted = scheduleNextFrame
                    if (scheduleNextFrame) {
                        continue
                    }

                    isAnimating = false
                    activeComputations.forEach { it.debugInspector?.isAnimating = false }
                    snapshotFlow {
                            val hasComputations =
                                activeComputations.isNotEmpty() || managedComputations.isNotEmpty()

                            val wakeup =
                                hasComputations &&
                                    (activeComputations != managedComputations ||
                                        activeComputations.any { it.wantWakeup() } ||
                                        input.invoke() != capturedInput ||
                                        gestureContext.direction != capturedDirection ||
                                        gestureContext.dragOffset != capturedGestureDragOffset)
                            wakeup
                        }
                        .first { it }
                    isAnimating = true
                    activeComputations.forEach { it.debugInspector?.isAnimating = true }
                }
            } finally {
                isActive = false
                activeComputations.forEach { it.onDeactivate() }
                activeComputationCount = 0
            }
        }
    }

    // ---- 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
    var currentInput: Float by mutableFloatStateOf(input.invoke())
        private set

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

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

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

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

    @VisibleForTesting
    var isActive = false
        private set

    @VisibleForTesting
    var isAnimating = false
        private set

    @VisibleForTesting
    var frameCount = 0
        private set

    @VisibleForTesting
    var activeComputationCount = 0
        private set

    @VisibleForTesting
    // Note - this is public so that its accessible by the mechanics:testing library
    val managedMotionValues: Set<ManagedMotionValue>
        get() = managedComputations

    internal fun onDispose(toDispose: ManagedMotionComputation) {
        managedComputations.remove(toDispose)
    }
}

internal class ManagedMotionComputation(
    private val owner: MotionValueCollection,
    private val specProvider: () -> MotionSpec,
    override val label: String?,
) : Computations(), ManagedMotionValue {

    override val stableThreshold: Float
        get() = owner.stableThreshold

    // ----  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? = computedSemanticState(key)

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

    override val floatValue: Float
        get() = output

    override fun dispose() {
        owner.onDispose(this)
    }

    override fun debugInspector(): DebugInspector {
        if (debugInspectorRefCount.getAndIncrement() == 0) {
            debugInspector =
                DebugInspector(
                    FrameData(
                        lastInput,
                        lastSegment.direction,
                        lastGestureDragOffset,
                        lastFrameTimeNanos,
                        lastSpringState,
                        lastSegment,
                        lastAnimation,
                        computedIsOutputFixed,
                    ),
                    owner.isActive,
                    owner.isAnimating,
                    ::onDisposeDebugInspector,
                )
        }

        return checkNotNull(debugInspector)
    }

    private var debugInspectorRefCount = AtomicInteger(0)

    private fun onDisposeDebugInspector() {
        if (debugInspectorRefCount.decrementAndGet() == 0) {
            debugInspector = null
        }
    }

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

    override val currentInput: Float
        get() = owner.currentInput

    override val currentDirection: InputDirection
        get() = owner.currentDirection

    override val currentGestureDragOffset: Float
        get() = owner.currentGestureDragOffset

    override val currentAnimationTimeNanos
        get() = owner.currentAnimationTimeNanos

    // ----  LastFrameState ---------------------------------------------------------------------

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

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

    private var _lastGuaranteeStatePacked: Long by
        mutableLongStateOf(GuaranteeState.Inactive.packedValue)

    override var lastAnimation: DiscontinuityAnimation by
        mutableStateOf(DiscontinuityAnimation.None, referentialEqualityPolicy())

    override var directMappedVelocity: Float = 0f

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

    private var _lastSpringStatePacked: Long by
        mutableLongStateOf(lastAnimation.springStartState.packedValue)

    override val lastFrameTimeNanos
        get() = owner.lastFrameTimeNanos

    override val lastInput
        get() = owner.lastInput

    override val lastGestureDragOffset
        get() = owner.lastGestureDragOffset

    // ---- Computations ---------------------------------------------------------------------------

    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
        capturedSpringState = currentSpringState

        debugInspector?.isAnimating = true
        debugInspector?.isActive = true
    }

    fun onDeactivate() {
        debugInspector?.isAnimating = false
        debugInspector?.isActive = false
    }

    fun onFrameStart() {
        lastSegment = capturedSegment
        lastGuaranteeState = capturedGuaranteeState
        lastAnimation = capturedAnimation
        lastSpringState = capturedSpringState

        output = computedOutput
        outputTarget = computedOutputTarget
        isStable = computedIsStable
    }

    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(
                    currentInput,
                    currentDirection,
                    currentGestureDragOffset,
                    currentAnimationTimeNanos,
                    capturedSpringState,
                    capturedSegment,
                    capturedAnimation,
                    computedIsOutputFixed,
                )
        }

        return scheduleNextFrame
    }

    fun wantWakeup(): Boolean {
        return spec != capturedSegment.spec
    }
}
+63 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.mechanics

import androidx.compose.runtime.FloatState
import androidx.compose.runtime.Stable
import com.android.mechanics.debug.DebugInspector
import com.android.mechanics.spec.SegmentKey
import com.android.mechanics.spec.SemanticKey

/** State produces by a motion value. */
@Stable
sealed interface MotionValueState : FloatState {

    /**
     * Animated [output] value.
     *
     * Same as [floatValue].
     */
    val output: Float

    /**
     * [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.
     */
    val outputTarget: Float

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

    /**
     * The current value for the [SemanticKey].
     *
     * `null` if not defined in the spec.
     */
    operator fun <T> get(key: SemanticKey<T>): T?

    /** The current segment used to compute the output. */
    val segmentKey: SegmentKey

    /** Debug label of the motion value. */
    val label: String?

    /** Provides access to the current state for debugging.. */
    fun debugInspector(): DebugInspector
}
+6 −6

File changed.

Preview size limit exceeded, changes collapsed.

+7 −7

File changed.

Preview size limit exceeded, changes collapsed.

Loading