From e3f652d5afbfe9fb0178ac99df449fe76687bc0d Mon Sep 17 00:00:00 2001 From: Ruslan Shestopalyuk Date: Sun, 28 Jul 2024 07:45:24 -0700 Subject: [PATCH] Migrate SpringAnimation to Kotlin Differential Revision: D60347906 --- .../react/animated/SpringAnimation.java | 207 ------------------ .../react/animated/SpringAnimation.kt | 193 ++++++++++++++++ 2 files changed, 193 insertions(+), 207 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/SpringAnimation.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/SpringAnimation.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/SpringAnimation.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/SpringAnimation.java deleted file mode 100644 index 32e50e84114b11..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/SpringAnimation.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.animated; - -import com.facebook.infer.annotation.Nullsafe; -import com.facebook.react.bridge.ReadableMap; - -/** - * Implementation of {@link AnimationDriver} providing support for spring animations. The - * implementation has been copied from android implementation of Rebound library (see http://facebook.github.io/rebound/) - */ -/*package*/ @Nullsafe(Nullsafe.Mode.LOCAL) -class SpringAnimation extends AnimationDriver { - - // maximum amount of time to simulate per physics iteration in seconds (4 frames at 60 FPS) - private static final double MAX_DELTA_TIME_SEC = 0.064; - // fixed timestep to use in the physics solver in seconds - private static final double SOLVER_TIMESTEP_SEC = 0.001; - - // storage for the current and prior physics state while integration is occurring - private static class PhysicsState { - double position; - double velocity; - } - - private long mLastTime; - private boolean mSpringStarted; - - // configuration - private double mSpringStiffness; - private double mSpringDamping; - private double mSpringMass; - private double mInitialVelocity; - private boolean mOvershootClampingEnabled; - - // all physics simulation objects are final and reused in each processing pass - private final PhysicsState mCurrentState = new PhysicsState(); - private double mStartValue; - private double mEndValue; - // thresholds for determining when the spring is at rest - private double mRestSpeedThreshold; - private double mDisplacementFromRestThreshold; - private double mTimeAccumulator; - // for controlling loop - private int mIterations; - private int mCurrentLoop; - private double mOriginalValue; - - SpringAnimation(ReadableMap config) { - mCurrentState.velocity = config.getDouble("initialVelocity"); - resetConfig(config); - } - - @Override - public void resetConfig(ReadableMap config) { - mSpringStiffness = config.getDouble("stiffness"); - mSpringDamping = config.getDouble("damping"); - mSpringMass = config.getDouble("mass"); - mInitialVelocity = mCurrentState.velocity; - mEndValue = config.getDouble("toValue"); - mRestSpeedThreshold = config.getDouble("restSpeedThreshold"); - mDisplacementFromRestThreshold = config.getDouble("restDisplacementThreshold"); - mOvershootClampingEnabled = config.getBoolean("overshootClamping"); - mIterations = config.hasKey("iterations") ? config.getInt("iterations") : 1; - mHasFinished = mIterations == 0; - mCurrentLoop = 0; - mTimeAccumulator = 0; - mSpringStarted = false; - } - - @Override - public void runAnimationStep(long frameTimeNanos) { - long frameTimeMillis = frameTimeNanos / 1000000; - if (!mSpringStarted) { - if (mCurrentLoop == 0) { - mOriginalValue = mAnimatedValue.nodeValue; - mCurrentLoop = 1; - } - mStartValue = mCurrentState.position = mAnimatedValue.nodeValue; - mLastTime = frameTimeMillis; - mTimeAccumulator = 0.0; - mSpringStarted = true; - } - advance((frameTimeMillis - mLastTime) / 1000.0); - mLastTime = frameTimeMillis; - mAnimatedValue.nodeValue = mCurrentState.position; - if (isAtRest()) { - if (mIterations == -1 || mCurrentLoop < mIterations) { // looping animation, return to start - mSpringStarted = false; - mAnimatedValue.nodeValue = mOriginalValue; - mCurrentLoop++; - } else { // animation has completed - mHasFinished = true; - } - } - } - - /** - * get the displacement from rest for a given physics state - * - * @param state the state to measure from - * @return the distance displaced by - */ - private double getDisplacementDistanceForState(PhysicsState state) { - return Math.abs(mEndValue - state.position); - } - - /** - * check if the current state is at rest - * - * @return is the spring at rest - */ - private boolean isAtRest() { - return Math.abs(mCurrentState.velocity) <= mRestSpeedThreshold - && (getDisplacementDistanceForState(mCurrentState) <= mDisplacementFromRestThreshold - || mSpringStiffness == 0); - } - - /** - * Check if the spring is overshooting beyond its target. - * - * @return true if the spring is overshooting its target - */ - private boolean isOvershooting() { - return mSpringStiffness > 0 - && ((mStartValue < mEndValue && mCurrentState.position > mEndValue) - || (mStartValue > mEndValue && mCurrentState.position < mEndValue)); - } - - private void advance(double realDeltaTime) { - if (isAtRest()) { - return; - } - - // clamp the amount of realTime to simulate to avoid stuttering in the UI. We should be able - // to catch up in a subsequent advance if necessary. - double adjustedDeltaTime = realDeltaTime; - if (realDeltaTime > MAX_DELTA_TIME_SEC) { - adjustedDeltaTime = MAX_DELTA_TIME_SEC; - } - - mTimeAccumulator += adjustedDeltaTime; - - double c = mSpringDamping; - double m = mSpringMass; - double k = mSpringStiffness; - double v0 = -mInitialVelocity; - - double zeta = c / (2 * Math.sqrt(k * m)); - double omega0 = Math.sqrt(k / m); - double omega1 = omega0 * Math.sqrt(1.0 - (zeta * zeta)); - double x0 = mEndValue - mStartValue; - - double velocity; - double position; - double t = mTimeAccumulator; - if (zeta < 1) { - // Under damped - double envelope = Math.exp(-zeta * omega0 * t); - position = - mEndValue - - envelope - * ((v0 + zeta * omega0 * x0) / omega1 * Math.sin(omega1 * t) - + x0 * Math.cos(omega1 * t)); - // This looks crazy -- it's actually just the derivative of the - // oscillation function - velocity = - zeta - * omega0 - * envelope - * (Math.sin(omega1 * t) * (v0 + zeta * omega0 * x0) / omega1 - + x0 * Math.cos(omega1 * t)) - - envelope - * (Math.cos(omega1 * t) * (v0 + zeta * omega0 * x0) - - omega1 * x0 * Math.sin(omega1 * t)); - } else { - // Critically damped spring - double envelope = Math.exp(-omega0 * t); - position = mEndValue - envelope * (x0 + (v0 + omega0 * x0) * t); - velocity = envelope * (v0 * (t * omega0 - 1) + t * x0 * (omega0 * omega0)); - } - - mCurrentState.position = position; - mCurrentState.velocity = velocity; - - // End the spring immediately if it is overshooting and overshoot clamping is enabled. - // Also make sure that if the spring was considered within a resting threshold that it's now - // snapped to its end value. - if (isAtRest() || (mOvershootClampingEnabled && isOvershooting())) { - // Don't call setCurrentValue because that forces a call to onSpringUpdate - if (mSpringStiffness > 0) { - mStartValue = mEndValue; - mCurrentState.position = mEndValue; - } else { - mEndValue = mCurrentState.position; - mStartValue = mEndValue; - } - mCurrentState.velocity = 0; - } - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/SpringAnimation.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/SpringAnimation.kt new file mode 100644 index 00000000000000..993461ba7ef412 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/SpringAnimation.kt @@ -0,0 +1,193 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.animated + +import com.facebook.react.bridge.ReadableMap +import kotlin.math.* + +/** + * Implementation of [AnimationDriver] providing support for spring animations. The implementation + * has been copied from android implementation of Rebound library (see + * [http://facebook.github.io/rebound/](http://facebook.github.io/rebound/)) + */ +internal class SpringAnimation(config: ReadableMap) : AnimationDriver() { + // storage for the current and prior physics state while integration is occurring + private class PhysicsState { + var position = 0.0 + var velocity = 0.0 + } + + private var lastTime: Long = 0 + private var springStarted = false + // configuration + private var springStiffness = 0.0 + private var springDamping = 0.0 + private var springMass = 0.0 + private var initialVelocity = 0.0 + private var overshootClampingEnabled = false + // all physics simulation objects are final and reused in each processing pass + private val currentState = PhysicsState() + private var startValue = 0.0 + private var endValue = 0.0 + // thresholds for determining when the spring is at rest + private var restSpeedThreshold = 0.0 + private var displacementFromRestThreshold = 0.0 + private var timeAccumulator = 0.0 + // for controlling loop + private var iterations = 0 + private var currentLoop = 0 + private var originalValue = 0.0 + + init { + currentState.velocity = config.getDouble("initialVelocity") + resetConfig(config) + } + + override fun resetConfig(config: ReadableMap) { + springStiffness = config.getDouble("stiffness") + springDamping = config.getDouble("damping") + springMass = config.getDouble("mass") + initialVelocity = currentState.velocity + endValue = config.getDouble("toValue") + restSpeedThreshold = config.getDouble("restSpeedThreshold") + displacementFromRestThreshold = config.getDouble("restDisplacementThreshold") + overshootClampingEnabled = config.getBoolean("overshootClamping") + iterations = if (config.hasKey("iterations")) config.getInt("iterations") else 1 + mHasFinished = iterations == 0 + currentLoop = 0 + timeAccumulator = 0.0 + springStarted = false + } + + override fun runAnimationStep(frameTimeNanos: Long) { + val frameTimeMillis = frameTimeNanos / 1000000 + if (!springStarted) { + if (currentLoop == 0) { + originalValue = mAnimatedValue.nodeValue + currentLoop = 1 + } + currentState.position = mAnimatedValue.nodeValue + startValue = currentState.position + lastTime = frameTimeMillis + timeAccumulator = 0.0 + springStarted = true + } + advance((frameTimeMillis - lastTime) / 1000.0) + lastTime = frameTimeMillis + mAnimatedValue.nodeValue = currentState.position + if (isAtRest) { + if (iterations == -1 || currentLoop < iterations) { // looping animation, return to start + springStarted = false + mAnimatedValue.nodeValue = originalValue + currentLoop++ + } else { // animation has completed + mHasFinished = true + } + } + } + + /** + * get the displacement from rest for a given physics state + * + * @param state the state to measure from + * @return the distance displaced by + */ + private fun getDisplacementDistanceForState(state: PhysicsState): Double = + abs(endValue - state.position) + + private val isAtRest: Boolean + /** + * check if the current state is at rest + * + * @return is the spring at rest + */ + get() = + (abs(currentState.velocity) <= restSpeedThreshold && + (getDisplacementDistanceForState(currentState) <= displacementFromRestThreshold || + springStiffness == 0.0)) + + private val isOvershooting: Boolean + /** + * Check if the spring is overshooting beyond its target. + * + * @return true if the spring is overshooting its target + */ + get() = + (springStiffness > 0 && + (startValue < endValue && currentState.position > endValue || + startValue > endValue && currentState.position < endValue)) + + private fun advance(realDeltaTime: Double) { + if (isAtRest) { + return + } + + // clamp the amount of realTime to simulate to avoid stuttering in the UI. We should be able + // to catch up in a subsequent advance if necessary. + var adjustedDeltaTime = realDeltaTime + if (realDeltaTime > MAX_DELTA_TIME_SEC) { + adjustedDeltaTime = MAX_DELTA_TIME_SEC + } + timeAccumulator += adjustedDeltaTime + val c = springDamping + val m = springMass + val k = springStiffness + val v0 = -initialVelocity + val zeta = c / (2 * sqrt(k * m)) + val omega0 = sqrt(k / m) + val omega1 = omega0 * sqrt(1.0 - zeta * zeta) + val x0 = endValue - startValue + val velocity: Double + val position: Double + val t = timeAccumulator + if (zeta < 1) { + // Under damped + val envelope = exp(-zeta * omega0 * t) + position = + (endValue - + envelope * + ((v0 + zeta * omega0 * x0) / omega1 * sin(omega1 * t) + x0 * cos(omega1 * t))) + // This looks crazy -- it's actually just the derivative of the + // oscillation function + velocity = + ((zeta * + omega0 * + envelope * + (sin(omega1 * t) * (v0 + zeta * omega0 * x0) / omega1 + x0 * cos(omega1 * t))) - + envelope * + (cos(omega1 * t) * (v0 + zeta * omega0 * x0) - omega1 * x0 * sin(omega1 * t))) + } else { + // Critically damped spring + val envelope = exp(-omega0 * t) + position = endValue - envelope * (x0 + (v0 + omega0 * x0) * t) + velocity = envelope * (v0 * (t * omega0 - 1) + t * x0 * (omega0 * omega0)) + } + currentState.position = position + currentState.velocity = velocity + + // End the spring immediately if it is overshooting and overshoot clamping is enabled. + // Also make sure that if the spring was considered within a resting threshold that it's now + // snapped to its end value. + if (isAtRest || overshootClampingEnabled && isOvershooting) { + // Don't call setCurrentValue because that forces a call to onSpringUpdate + if (springStiffness > 0) { + startValue = endValue + currentState.position = endValue + } else { + endValue = currentState.position + startValue = endValue + } + currentState.velocity = 0.0 + } + } + + companion object { + // maximum amount of time to simulate per physics iteration in seconds (4 frames at 60 FPS) + private const val MAX_DELTA_TIME_SEC = 0.064 + } +}