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

Commit cf3ea899 authored by Mike Schneider's avatar Mike Schneider Committed by Android (Google) Code Review
Browse files

Merge changes from topic "mm-velocity" into main

* changes:
  Use `directMapped` velocity to prime new springs.
  Protect MotionValue runtime from non-finite numbers
parents 956157a9 c165c186
Loading
Loading
Loading
Loading
+71 −10
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.mechanics

import android.util.Log
import androidx.compose.runtime.FloatState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -28,6 +29,7 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.util.fastCoerceAtLeast
import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.fastIsFinite
import androidx.compose.ui.util.lerp
import androidx.compose.ui.util.packFloats
import androidx.compose.ui.util.unpackFloat1
@@ -196,6 +198,13 @@ class MotionValue(

            try {
                debugIsAnimating = 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 (continueRunning.invoke(this@MotionValue)) {

                    withFrameNanos { frameTimeNanos ->
@@ -222,6 +231,19 @@ class MotionValue(
                    // cause a re-computation if the current state is being read before the next
                    // frame).

                    if (isAnimatingUninterrupted) {
                        val currentDirectMapped = currentDirectMapped
                        val lastDirectMapped =
                            lastSegment.mapping.map(lastInput) - lastAnimation.targetValue

                        val frameDuration =
                            (currentAnimationTimeNanos - lastFrameTimeNanos) / 1_000_000_000.0
                        val staticDelta = (currentDirectMapped - lastDirectMapped)
                        directMappedVelocity = (staticDelta / frameDuration).toFloat()
                    } else {
                        directMappedVelocity = 0f
                    }

                    var scheduleNextFrame = !isStable
                    if (capturedSegment != currentSegment) {
                        capturedSegment = currentSegment
@@ -273,6 +295,7 @@ class MotionValue(
                            )
                    }

                    isAnimatingUninterrupted = scheduleNextFrame
                    if (scheduleNextFrame) {
                        continue
                    }
@@ -374,6 +397,11 @@ class MotionValue(
    private var lastAnimation: DiscontinuityAnimation by
        mutableStateOf(DiscontinuityAnimation.None, referentialEqualityPolicy())

    // The change velocity of the `currentDirectMapped`, in `units/sec`. Only non-zero if the
    // animation loop is processing every frame (while animating or while the input changes
    // continuously).
    private var directMappedVelocity: Float = 0f

    // ---- Last frame's input and output ----------------------------------------------------------

    // The state below captures relevant input values (including frame time) and the computed spring
@@ -670,10 +698,23 @@ class MotionValue(
            SegmentChangeType.Direction,
            SegmentChangeType.Spec -> {
                // Determine the delta in the output, as produced by the old and new mapping.
                val delta =
                    currentSegment.mapping.map(currentInput) - lastSegment.mapping.map(currentInput)
                val currentMapping = currentSegment.mapping.map(currentInput)
                val lastMapping = lastSegment.mapping.map(currentInput)
                val delta = currentMapping - lastMapping

                val deltaIsFinite = delta.fastIsFinite()
                if (!deltaIsFinite) {
                    Log.wtf(
                        TAG,
                        "Delta between mappings is undefined!\n" +
                            "  MotionValue: $label\n" +
                            "  input: $currentInput\n" +
                            "  lastMapping: $lastMapping (lastSegment: $lastSegment)\n" +
                            "  currentMapping: $currentMapping (currentSegment: $currentSegment)",
                    )
                }

                if (delta == 0f) {
                if (delta == 0f || !deltaIsFinite) {
                    // Nothing new to animate.
                    lastAnimation
                } else {
@@ -687,7 +728,7 @@ class MotionValue(
                    val newTarget = delta - lastSpringState.displacement
                    DiscontinuityAnimation(
                        newTarget,
                        SpringState(-newTarget, lastSpringState.velocity),
                        SpringState(-newTarget, lastSpringState.velocity + directMappedVelocity),
                        springParameters,
                        lastFrameTimeNanos,
                    )
@@ -765,14 +806,28 @@ class MotionValue(
                            )
                        lastAnimationTime = nextBreakpointCrossTime

                        val beforeBreakpoint = mappings[segmentIndex].map(nextBreakpoint.position)
                        val afterBreakpoint =
                            mappings[segmentIndex + directionOffset].map(nextBreakpoint.position)
                        val mappingBefore = mappings[segmentIndex]
                        val beforeBreakpoint = mappingBefore.map(nextBreakpoint.position)
                        val mappingAfter = mappings[segmentIndex + directionOffset]
                        val afterBreakpoint = mappingAfter.map(nextBreakpoint.position)

                        val delta = afterBreakpoint - beforeBreakpoint
                        springTarget += delta
                        springState = springState.addDisplacement(-delta)
                        val deltaIsFinite = delta.fastIsFinite()
                        if (!deltaIsFinite) {
                            Log.wtf(
                                TAG,
                                "Delta between breakpoints is undefined!\n" +
                                    "  MotionValue: $label\n" +
                                    "  position: ${nextBreakpoint.position}\n" +
                                    "  before: $beforeBreakpoint (mapping: $mappingBefore)\n" +
                                    "  after: $afterBreakpoint (mapping: $mappingAfter)",
                            )
                        }

                        if (deltaIsFinite) {
                            springTarget += delta
                            springState = springState.nudge(displacementDelta = -delta)
                        }
                        segmentIndex += directionOffset
                        lastBreakpoint = nextBreakpoint
                        guaranteeState =
@@ -793,6 +848,10 @@ class MotionValue(
                            }
                    }

                    if (springState.displacement != 0f) {
                        springState = springState.nudge(velocityDelta = directMappedVelocity)
                    }

                    val tightened =
                        currentGuaranteeState.updatedSpringParameters(
                            currentSegment.entryBreakpoint
@@ -828,7 +887,9 @@ class MotionValue(
    }

    private val currentDirectMapped: Float
        get() = currentSegment.mapping.map(currentInput()) - currentAnimation.targetValue
        get() {
            return currentSegment.mapping.map(currentInput()) - currentAnimation.targetValue
        }

    private val currentAnimatedDelta: Float
        get() = currentAnimation.targetValue + currentSpringState.displacement
+19 −12
Original line number Diff line number Diff line
@@ -417,6 +417,11 @@ private class DirectionalMotionSpecBuilderImpl(override val defaultSpring: Sprin
            check(!sourceValue.isNaN())

            val sourcePosition = breakpoints.last().position
            val breakpointDistance = atPosition - sourcePosition
            val mapping =
                if (breakpointDistance == 0f) {
                    Mapping.Fixed(sourceValue)
                } else {

                    if (fractionalMapping.isNaN()) {
                        val delta = targetValue - sourceValue
@@ -427,8 +432,10 @@ private class DirectionalMotionSpecBuilderImpl(override val defaultSpring: Sprin
                    }

                    val offset = sourceValue - (sourcePosition * fractionalMapping)
                    Mapping.Linear(fractionalMapping, offset)
                }

            mappings.add(Mapping.Linear(fractionalMapping, offset))
            mappings.add(mapping)
            targetValue = Float.NaN
            sourceValue = Float.NaN
            fractionalMapping = Float.NaN
+20 −14
Original line number Diff line number Diff line
@@ -305,20 +305,26 @@ private class FluentSpecBuilder<R>(
            check(!sourceValue.isNaN())

            val sourcePosition = breakpoints.last().position

            val breakpointDistance = atPosition - sourcePosition
            val mapping =
                if (breakpointDistance == 0f) {
                    Mapping.Fixed(sourceValue)
                } else {
                    if (fractionalMapping.isNaN()) {
                        val delta = targetValue - sourceValue
                fractionalMapping = delta / (atPosition - sourcePosition)
                        fractionalMapping = delta / breakpointDistance
                    } else {
                val delta = (atPosition - sourcePosition) * fractionalMapping
                        val delta = breakpointDistance * fractionalMapping
                        targetValue = sourceValue + delta
                    }

                    val offset =
                        if (buildForward) sourceValue - (sourcePosition * fractionalMapping)
                        else targetValue - (atPosition * fractionalMapping)
                    Mapping.Linear(fractionalMapping, offset)
                }

            mappings.add(Mapping.Linear(fractionalMapping, offset))
            mappings.add(mapping)
            targetValue = Float.NaN
            sourceValue = Float.NaN
            fractionalMapping = Float.NaN
+15 −0
Original line number Diff line number Diff line
@@ -95,6 +95,10 @@ fun interface Mapping {

    /** `f(x) = value` */
    data class Fixed(val value: Float) : Mapping {
        init {
            require(value.isFinite())
        }

        override fun map(input: Float): Float {
            return value
        }
@@ -102,6 +106,11 @@ fun interface Mapping {

    /** `f(x) = factor*x + offset` */
    data class Linear(val factor: Float, val offset: Float = 0f) : Mapping {
        init {
            require(factor.isFinite())
            require(offset.isFinite())
        }

        override fun map(input: Float): Float {
            return input * factor + offset
        }
@@ -109,6 +118,12 @@ fun interface Mapping {

    data class Tanh(val scaling: Float, val tilt: Float, val offset: Float = 0f) : Mapping {

        init {
            require(scaling.isFinite())
            require(tilt.isFinite())
            require(offset.isFinite())
        }

        override fun map(input: Float): Float {
            return scaling * kotlin.math.tanh((input + offset) / (scaling * tilt))
        }
+3 −2
Original line number Diff line number Diff line
@@ -51,8 +51,9 @@ value class SpringState(val packedValue: Long) {
        return currentEnergy <= maxStableEnergy
    }

    fun addDisplacement(displacementDelta: Float): SpringState {
        return SpringState(displacement + displacementDelta, velocity)
    /** Adds the specified [displacementDelta] and [velocityDelta] to the returned state. */
    fun nudge(displacementDelta: Float = 0f, velocityDelta: Float = 0f): SpringState {
        return SpringState(displacement + displacementDelta, velocity + velocityDelta)
    }

    override fun toString(): String {
Loading