Loading mechanics/src/com/android/mechanics/impl/Computations.kt +86 −36 Original line number Diff line number Diff line Loading @@ -39,17 +39,27 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static ) // currentComputedValues input private var memoizedSpec: MotionSpec? = null private var memoizedSpec: MotionSpec = MotionSpec.InitiallyUndefined private var memoizedInput: Float = Float.MIN_VALUE private var memoizedAnimationTimeNanos: Long = Long.MIN_VALUE private var memoizedDirection: InputDirection = InputDirection.Min // currentComputedValues output private lateinit var memoizedComputedValues: ComputedValues private var memoizedComputedValues: ComputedValues = ComputedValues( MotionSpec.InitiallyUndefined.segmentAtInput(memoizedInput, memoizedDirection), GuaranteeState.Inactive, DiscontinuityAnimation.None, ) internal val currentComputedValues: ComputedValues get() { val currentSpec: MotionSpec = spec if (currentSpec == MotionSpec.InitiallyUndefined) { requireNoMotionSpecSet() return memoizedComputedValues } val currentInput: Float = currentInput val currentAnimationTimeNanos: Long = currentAnimationTimeNanos val currentDirection: InputDirection = currentDirection Loading @@ -63,11 +73,21 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static return memoizedComputedValues } val isInitialComputation = memoizedSpec == MotionSpec.InitiallyUndefined memoizedSpec = currentSpec memoizedInput = currentInput memoizedAnimationTimeNanos = currentAnimationTimeNanos memoizedDirection = currentDirection memoizedComputedValues = if (isInitialComputation) { ComputedValues( currentSpec.segmentAtInput(currentInput, currentDirection), GuaranteeState.Inactive, DiscontinuityAnimation.None, ) } else { val segment: SegmentData = computeSegmentData( spec = currentSpec, Loading Loading @@ -99,9 +119,9 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static animationTimeNanos = currentAnimationTimeNanos, ) return ComputedValues(segment, guarantee, animation).also { memoizedComputedValues = it ComputedValues(segment, guarantee, animation) } return memoizedComputedValues } // currentSpringState input Loading Loading @@ -613,4 +633,34 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static } } } /** * Precondition to ensure that this [Computations] has not yet been initialized with a * MotionSpec other than [MotionSpec.InitiallyUndefined]. * * This precondition is added since the desired behavior of the MotionValue when toggling back * to a [MotionSpec.InitiallyUndefined] spec is unclear. If there is a compelling usecase, this * restriction could be lifted. */ private fun requireNoMotionSpecSet() { // A MotionValue's spec can be MotionValue.Undefined initially. However, once a real spec // has been set, it cannot be changed back to MotionValue.Undefined. require(memoizedSpec == MotionSpec.InitiallyUndefined) { // memoizedSpec is only ever Undefined initially, before a motionSpec was set. // This is used as a signal to detect if a user switches back to Undefined. "MotionSpec must not be changed back to undefined!\n" + " MotionValue: $label\n" + " last MotionSpec: $memoizedSpec" } // memoizedComputedValues must not have been reassigned either. require( with(memoizedComputedValues) { segment.spec == MotionSpec.InitiallyUndefined && guarantee == GuaranteeState.Inactive && animation == DiscontinuityAnimation.None } ) } } mechanics/src/com/android/mechanics/spec/MotionSpec.kt +30 −0 Original line number Diff line number Diff line Loading @@ -150,6 +150,19 @@ data class MotionSpec( /* Empty motion spec, the output is the same as the input. */ val Empty = MotionSpec(DirectionalMotionSpec.Empty) /** * Placeholder to indicate that a [MotionSpec] cannot be supplied yet. * * As long as this spec is set, the MotionValue output is NaN. When the MotionValue is first * supplied with an actual spec, the output value will be set immediately, without an * animation. * * This must only ever be supplied as a spec for new `MotionValue`s, which never were * supplied any other spec. Supplying this [InitiallyUndefined] spec to a MotionValue that * has already been supplied a spec will throw an exception. */ val InitiallyUndefined = MotionSpec(DirectionalMotionSpec.InitiallyUndefined) } } Loading Loading @@ -247,5 +260,22 @@ data class DirectionalMotionSpec( listOf(Breakpoint.minLimit, Breakpoint.maxLimit), listOf(Mapping.Identity), ) /** Internal marker for [MotionSpec.InitiallyUndefined]. */ internal val InitiallyUndefined = DirectionalMotionSpec( listOf(Breakpoint.minLimit, Breakpoint.maxLimit), listOf( object : Mapping { override fun map(input: Float): Float { return Float.NaN } override fun toString(): String { return "InitiallyUndefined" } } ), ) } } mechanics/src/com/android/mechanics/view/ViewMotionValue.kt +3 −3 Original line number Diff line number Diff line Loading @@ -222,9 +222,9 @@ private class ImperativeComputations( repeatMode = ValueAnimator.RESTART repeatCount = ValueAnimator.INFINITE start() pause() addUpdateListener { val isAnimationFinished = updateOutputValue(currentPlayTime) debugInspector?.isAnimating = !isAnimationFinished if (isAnimationFinished) { pause() } Loading @@ -234,14 +234,12 @@ private class ImperativeComputations( fun ensureFrameRequested() { if (animationFrameDriver.isPaused) { animationFrameDriver.resume() debugInspector?.isAnimating = true } } fun pauseFrameRequests() { if (animationFrameDriver.isRunning) { animationFrameDriver.pause() debugInspector?.isAnimating = false } } Loading Loading @@ -290,6 +288,8 @@ private class ImperativeComputations( ) } if (currentValues.segment.spec == MotionSpec.InitiallyUndefined) return true listeners.fastForEach { it.onMotionValueUpdated(motionValue) } // Prepare last* state Loading mechanics/tests/goldens/unspecifiedSpec_atTheBeginning_jumpcutsToFirstValue.json 0 → 100644 +92 −0 Original line number Diff line number Diff line { "frame_ids": [ 0, 16, 32, 48, 64 ], "features": [ { "name": "input", "type": "float", "data_points": [ 0, 5, 10, 15, 20 ] }, { "name": "gestureDirection", "type": "string", "data_points": [ "Max", "Max", "Max", "Max", "Max" ] }, { "name": "output", "type": "float", "data_points": [ "NaN", "NaN", "NaN", 15, 20 ] }, { "name": "outputTarget", "type": "float", "data_points": [ "NaN", "NaN", "NaN", 15, 20 ] }, { "name": "outputSpring", "type": "springParameters", "data_points": [ { "stiffness": 100000, "dampingRatio": 1 }, { "stiffness": 100000, "dampingRatio": 1 }, { "stiffness": 100000, "dampingRatio": 1 }, { "stiffness": 100000, "dampingRatio": 1 }, { "stiffness": 100000, "dampingRatio": 1 } ] }, { "name": "isStable", "type": "boolean", "data_points": [ true, true, true, true, true ] } ] } No newline at end of file mechanics/tests/goldens/unspecifiedSpec_outputIsNan.json 0 → 100644 +102 −0 Original line number Diff line number Diff line { "frame_ids": [ 0, 16, 32, 48, 64, 80 ], "features": [ { "name": "input", "type": "float", "data_points": [ 0, 20, 40, 60, 80, 100 ] }, { "name": "gestureDirection", "type": "string", "data_points": [ "Max", "Max", "Max", "Max", "Max", "Max" ] }, { "name": "output", "type": "float", "data_points": [ "NaN", "NaN", "NaN", "NaN", "NaN", "NaN" ] }, { "name": "outputTarget", "type": "float", "data_points": [ "NaN", "NaN", "NaN", "NaN", "NaN", "NaN" ] }, { "name": "outputSpring", "type": "springParameters", "data_points": [ { "stiffness": 100000, "dampingRatio": 1 }, { "stiffness": 100000, "dampingRatio": 1 }, { "stiffness": 100000, "dampingRatio": 1 }, { "stiffness": 100000, "dampingRatio": 1 }, { "stiffness": 100000, "dampingRatio": 1 }, { "stiffness": 100000, "dampingRatio": 1 } ] }, { "name": "isStable", "type": "boolean", "data_points": [ true, true, true, true, true, true ] } ] } No newline at end of file Loading
mechanics/src/com/android/mechanics/impl/Computations.kt +86 −36 Original line number Diff line number Diff line Loading @@ -39,17 +39,27 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static ) // currentComputedValues input private var memoizedSpec: MotionSpec? = null private var memoizedSpec: MotionSpec = MotionSpec.InitiallyUndefined private var memoizedInput: Float = Float.MIN_VALUE private var memoizedAnimationTimeNanos: Long = Long.MIN_VALUE private var memoizedDirection: InputDirection = InputDirection.Min // currentComputedValues output private lateinit var memoizedComputedValues: ComputedValues private var memoizedComputedValues: ComputedValues = ComputedValues( MotionSpec.InitiallyUndefined.segmentAtInput(memoizedInput, memoizedDirection), GuaranteeState.Inactive, DiscontinuityAnimation.None, ) internal val currentComputedValues: ComputedValues get() { val currentSpec: MotionSpec = spec if (currentSpec == MotionSpec.InitiallyUndefined) { requireNoMotionSpecSet() return memoizedComputedValues } val currentInput: Float = currentInput val currentAnimationTimeNanos: Long = currentAnimationTimeNanos val currentDirection: InputDirection = currentDirection Loading @@ -63,11 +73,21 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static return memoizedComputedValues } val isInitialComputation = memoizedSpec == MotionSpec.InitiallyUndefined memoizedSpec = currentSpec memoizedInput = currentInput memoizedAnimationTimeNanos = currentAnimationTimeNanos memoizedDirection = currentDirection memoizedComputedValues = if (isInitialComputation) { ComputedValues( currentSpec.segmentAtInput(currentInput, currentDirection), GuaranteeState.Inactive, DiscontinuityAnimation.None, ) } else { val segment: SegmentData = computeSegmentData( spec = currentSpec, Loading Loading @@ -99,9 +119,9 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static animationTimeNanos = currentAnimationTimeNanos, ) return ComputedValues(segment, guarantee, animation).also { memoizedComputedValues = it ComputedValues(segment, guarantee, animation) } return memoizedComputedValues } // currentSpringState input Loading Loading @@ -613,4 +633,34 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static } } } /** * Precondition to ensure that this [Computations] has not yet been initialized with a * MotionSpec other than [MotionSpec.InitiallyUndefined]. * * This precondition is added since the desired behavior of the MotionValue when toggling back * to a [MotionSpec.InitiallyUndefined] spec is unclear. If there is a compelling usecase, this * restriction could be lifted. */ private fun requireNoMotionSpecSet() { // A MotionValue's spec can be MotionValue.Undefined initially. However, once a real spec // has been set, it cannot be changed back to MotionValue.Undefined. require(memoizedSpec == MotionSpec.InitiallyUndefined) { // memoizedSpec is only ever Undefined initially, before a motionSpec was set. // This is used as a signal to detect if a user switches back to Undefined. "MotionSpec must not be changed back to undefined!\n" + " MotionValue: $label\n" + " last MotionSpec: $memoizedSpec" } // memoizedComputedValues must not have been reassigned either. require( with(memoizedComputedValues) { segment.spec == MotionSpec.InitiallyUndefined && guarantee == GuaranteeState.Inactive && animation == DiscontinuityAnimation.None } ) } }
mechanics/src/com/android/mechanics/spec/MotionSpec.kt +30 −0 Original line number Diff line number Diff line Loading @@ -150,6 +150,19 @@ data class MotionSpec( /* Empty motion spec, the output is the same as the input. */ val Empty = MotionSpec(DirectionalMotionSpec.Empty) /** * Placeholder to indicate that a [MotionSpec] cannot be supplied yet. * * As long as this spec is set, the MotionValue output is NaN. When the MotionValue is first * supplied with an actual spec, the output value will be set immediately, without an * animation. * * This must only ever be supplied as a spec for new `MotionValue`s, which never were * supplied any other spec. Supplying this [InitiallyUndefined] spec to a MotionValue that * has already been supplied a spec will throw an exception. */ val InitiallyUndefined = MotionSpec(DirectionalMotionSpec.InitiallyUndefined) } } Loading Loading @@ -247,5 +260,22 @@ data class DirectionalMotionSpec( listOf(Breakpoint.minLimit, Breakpoint.maxLimit), listOf(Mapping.Identity), ) /** Internal marker for [MotionSpec.InitiallyUndefined]. */ internal val InitiallyUndefined = DirectionalMotionSpec( listOf(Breakpoint.minLimit, Breakpoint.maxLimit), listOf( object : Mapping { override fun map(input: Float): Float { return Float.NaN } override fun toString(): String { return "InitiallyUndefined" } } ), ) } }
mechanics/src/com/android/mechanics/view/ViewMotionValue.kt +3 −3 Original line number Diff line number Diff line Loading @@ -222,9 +222,9 @@ private class ImperativeComputations( repeatMode = ValueAnimator.RESTART repeatCount = ValueAnimator.INFINITE start() pause() addUpdateListener { val isAnimationFinished = updateOutputValue(currentPlayTime) debugInspector?.isAnimating = !isAnimationFinished if (isAnimationFinished) { pause() } Loading @@ -234,14 +234,12 @@ private class ImperativeComputations( fun ensureFrameRequested() { if (animationFrameDriver.isPaused) { animationFrameDriver.resume() debugInspector?.isAnimating = true } } fun pauseFrameRequests() { if (animationFrameDriver.isRunning) { animationFrameDriver.pause() debugInspector?.isAnimating = false } } Loading Loading @@ -290,6 +288,8 @@ private class ImperativeComputations( ) } if (currentValues.segment.spec == MotionSpec.InitiallyUndefined) return true listeners.fastForEach { it.onMotionValueUpdated(motionValue) } // Prepare last* state Loading
mechanics/tests/goldens/unspecifiedSpec_atTheBeginning_jumpcutsToFirstValue.json 0 → 100644 +92 −0 Original line number Diff line number Diff line { "frame_ids": [ 0, 16, 32, 48, 64 ], "features": [ { "name": "input", "type": "float", "data_points": [ 0, 5, 10, 15, 20 ] }, { "name": "gestureDirection", "type": "string", "data_points": [ "Max", "Max", "Max", "Max", "Max" ] }, { "name": "output", "type": "float", "data_points": [ "NaN", "NaN", "NaN", 15, 20 ] }, { "name": "outputTarget", "type": "float", "data_points": [ "NaN", "NaN", "NaN", 15, 20 ] }, { "name": "outputSpring", "type": "springParameters", "data_points": [ { "stiffness": 100000, "dampingRatio": 1 }, { "stiffness": 100000, "dampingRatio": 1 }, { "stiffness": 100000, "dampingRatio": 1 }, { "stiffness": 100000, "dampingRatio": 1 }, { "stiffness": 100000, "dampingRatio": 1 } ] }, { "name": "isStable", "type": "boolean", "data_points": [ true, true, true, true, true ] } ] } No newline at end of file
mechanics/tests/goldens/unspecifiedSpec_outputIsNan.json 0 → 100644 +102 −0 Original line number Diff line number Diff line { "frame_ids": [ 0, 16, 32, 48, 64, 80 ], "features": [ { "name": "input", "type": "float", "data_points": [ 0, 20, 40, 60, 80, 100 ] }, { "name": "gestureDirection", "type": "string", "data_points": [ "Max", "Max", "Max", "Max", "Max", "Max" ] }, { "name": "output", "type": "float", "data_points": [ "NaN", "NaN", "NaN", "NaN", "NaN", "NaN" ] }, { "name": "outputTarget", "type": "float", "data_points": [ "NaN", "NaN", "NaN", "NaN", "NaN", "NaN" ] }, { "name": "outputSpring", "type": "springParameters", "data_points": [ { "stiffness": 100000, "dampingRatio": 1 }, { "stiffness": 100000, "dampingRatio": 1 }, { "stiffness": 100000, "dampingRatio": 1 }, { "stiffness": 100000, "dampingRatio": 1 }, { "stiffness": 100000, "dampingRatio": 1 }, { "stiffness": 100000, "dampingRatio": 1 } ] }, { "name": "isStable", "type": "boolean", "data_points": [ true, true, true, true, true, true ] } ] } No newline at end of file