From 9e7760210f678202bef96bf4d19022cbcc5a3908 Mon Sep 17 00:00:00 2001 From: Ruslan Shestopalyuk Date: Sun, 28 Jul 2024 06:01:14 -0700 Subject: [PATCH] Migrate DecayAnimation to Kotlin (#45719) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/45719 # Changelog: [Internal] - As in the title. Reviewed By: tdn120 Differential Revision: D60284323 fbshipit-source-id: 6cf47a9f9528067b397b0c52b763b416bf62ed9f --- .../react/animated/DecayAnimation.java | 79 ------------------- .../facebook/react/animated/DecayAnimation.kt | 71 +++++++++++++++++ .../NativeAnimatedNodeTraversalTest.kt | 8 +- 3 files changed, 75 insertions(+), 83 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/DecayAnimation.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/DecayAnimation.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/DecayAnimation.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/DecayAnimation.java deleted file mode 100644 index f4d07d8e4c4d5d..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/DecayAnimation.java +++ /dev/null @@ -1,79 +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 decay animations. The - * implementation is copied from the JS version in {@code AnimatedImplementation.js}. - */ -@Nullsafe(Nullsafe.Mode.LOCAL) -class DecayAnimation extends AnimationDriver { - - private final double mVelocity; - - private double mDeceleration; - private long mStartFrameTimeMillis; - private double mFromValue; - private double mLastValue; - private int mIterations; - private int mCurrentLoop; - - public DecayAnimation(ReadableMap config) { - mVelocity = config.getDouble("velocity"); // initial velocity - resetConfig(config); - } - - @Override - public void resetConfig(ReadableMap config) { - mDeceleration = config.getDouble("deceleration"); - mIterations = config.hasKey("iterations") ? config.getInt("iterations") : 1; - mCurrentLoop = 1; - mHasFinished = mIterations == 0; - mStartFrameTimeMillis = -1; - mFromValue = 0; - mLastValue = 0; - } - - @Override - public void runAnimationStep(long frameTimeNanos) { - long frameTimeMillis = frameTimeNanos / 1000000; - if (mStartFrameTimeMillis == -1) { - // since this is the first animation step, consider the start to be on the previous frame - mStartFrameTimeMillis = frameTimeMillis - 16; - if (mFromValue == mLastValue) { // first iteration, assign mFromValue based on mAnimatedValue - mFromValue = mAnimatedValue.nodeValue; - } else { // not the first iteration, reset mAnimatedValue based on mFromValue - mAnimatedValue.nodeValue = mFromValue; - } - mLastValue = mAnimatedValue.nodeValue; - } - - final double value = - mFromValue - + (mVelocity / (1 - mDeceleration)) - * (1 - Math.exp(-(1 - mDeceleration) * (frameTimeMillis - mStartFrameTimeMillis))); - - if (Math.abs(mLastValue - value) < 0.1) { - - if (mIterations == -1 || mCurrentLoop < mIterations) { // looping animation, return to start - // set mStartFrameTimeMillis to -1 to reset instance variables on the next runAnimationStep - mStartFrameTimeMillis = -1; - mCurrentLoop++; - } else { // animation has completed - mHasFinished = true; - return; - } - } - - mLastValue = value; - mAnimatedValue.nodeValue = value; - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/DecayAnimation.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/DecayAnimation.kt new file mode 100644 index 00000000000000..0ad47792b95ce7 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/DecayAnimation.kt @@ -0,0 +1,71 @@ +/* + * 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.abs +import kotlin.math.exp + +/** + * Implementation of [AnimationDriver] providing support for decay animations. The implementation is + * copied from the JS version in `AnimatedImplementation.js`. + */ +internal class DecayAnimation(config: ReadableMap) : AnimationDriver() { + private var velocity: Double = 0.0 + private var deceleration: Double = 0.0 + private var startFrameTimeMillis: Long = -1 + private var fromValue: Double = 0.0 + private var lastValue: Double = 0.0 + private var iterations: Int = 1 + private var currentLoop: Int = 1 + + init { + resetConfig(config) + } + + public override fun resetConfig(config: ReadableMap): Unit { + velocity = config.getDouble("velocity") + deceleration = config.getDouble("deceleration") + startFrameTimeMillis = -1 + fromValue = 0.0 + lastValue = 0.0 + iterations = if (config.hasKey("iterations")) config.getInt("iterations") else 1 + currentLoop = 1 + mHasFinished = iterations == 0 + } + + public override fun runAnimationStep(frameTimeNanos: Long) { + val frameTimeMillis = frameTimeNanos / 1000000 + if (startFrameTimeMillis == -1L) { + // since this is the first animation step, consider the start to be on the previous frame + startFrameTimeMillis = frameTimeMillis - 16 + if (fromValue == lastValue) { // first iteration, assign mFromValue based on mAnimatedValue + fromValue = mAnimatedValue.nodeValue + } else { // not the first iteration, reset mAnimatedValue based on mFromValue + mAnimatedValue.nodeValue = fromValue + } + lastValue = mAnimatedValue.nodeValue + } + val value = + (fromValue + + velocity / (1 - deceleration) * + (1 - exp(-(1 - deceleration) * (frameTimeMillis - startFrameTimeMillis)))) + if (abs(lastValue - value) < 0.1) { + if (iterations == -1 || currentLoop < iterations) { // looping animation, return to start + // set mStartFrameTimeMillis to -1 to reset instance variables on the next runAnimationStep + startFrameTimeMillis = -1 + currentLoop++ + } else { // animation has completed + mHasFinished = true + return + } + } + lastValue = value + mAnimatedValue.nodeValue = value + } +} diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.kt index 9ad3fe408ecf49..f7b2cbb02729b5 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.kt @@ -1120,7 +1120,7 @@ class NativeAnimatedNodeTraversalTest { } // at this point we expect tracking value to be at 75 - assertThat((nativeAnimatedNodesManager.getNodeById(3) as ValueAnimatedNode).value) + assertThat((nativeAnimatedNodesManager.getNodeById(3) as ValueAnimatedNode).getValue()) .isEqualTo(75.0) // we update "toValue" again to 100 and expect the animation to restart from the current @@ -1228,11 +1228,11 @@ class NativeAnimatedNodeTraversalTest { // passes the final point (that is 1) while going backwards var isBoucingBack: Boolean = false var previousValue: Double = - (nativeAnimatedNodesManager.getNodeById(3) as ValueAnimatedNode).value + (nativeAnimatedNodesManager.getNodeById(3) as ValueAnimatedNode).getValue() for (i in 500 downTo 0) { nativeAnimatedNodesManager.runUpdates(nextFrameTime()) val currentValue: Double = - (nativeAnimatedNodesManager.getNodeById(3) as ValueAnimatedNode).value + (nativeAnimatedNodesManager.getNodeById(3) as ValueAnimatedNode).getValue() if (previousValue >= 1.0 && currentValue < 1.0) { isBoucingBack = true break @@ -1253,7 +1253,7 @@ class NativeAnimatedNodeTraversalTest { for (i in 0 until 8 * 60) { nativeAnimatedNodesManager.runUpdates(nextFrameTime()) val currentValue: Double = - (nativeAnimatedNodesManager.getNodeById(3) as ValueAnimatedNode).value + (nativeAnimatedNodesManager.getNodeById(3) as ValueAnimatedNode).getValue() if (!hasTurnedForward) { if (currentValue <= previousValue) { bounceBackInitialFrames++