From c30ee464eb894e57f84ce13dd0b275b4a9c49e55 Mon Sep 17 00:00:00 2001 From: Ruslan Shestopalyuk Date: Thu, 1 Aug 2024 03:52:44 -0700 Subject: [PATCH] Kotlinify AnimationDriver (#45836) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/45836 # Changelog: [Internal] - As in the title. Reviewed By: cortinico Differential Revision: D60502487 fbshipit-source-id: 003f2eade8f8425636b2023397ec29545175b487 --- ...nimationDriver.java => AnimationDriver.kt} | 27 ++++++----- .../facebook/react/animated/DecayAnimation.kt | 20 ++++---- .../animated/FrameBasedAnimationDriver.kt | 11 +++-- .../animated/NativeAnimatedNodesManager.java | 46 +++++++++---------- .../react/animated/SpringAnimation.kt | 13 +++--- 5 files changed, 60 insertions(+), 57 deletions(-) rename packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/{AnimationDriver.java => AnimationDriver.kt} (62%) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/AnimationDriver.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/AnimationDriver.kt similarity index 62% rename from packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/AnimationDriver.java rename to packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/AnimationDriver.kt index fc77deed5bb..e55fa2435d4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/AnimationDriver.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/AnimationDriver.kt @@ -5,28 +5,27 @@ * LICENSE file in the root directory of this source tree. */ -package com.facebook.react.animated; +package com.facebook.react.animated -import com.facebook.react.bridge.Callback; -import com.facebook.react.bridge.JSApplicationCausedNativeException; -import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.Callback +import com.facebook.react.bridge.JSApplicationCausedNativeException +import com.facebook.react.bridge.ReadableMap /** * Base class for different types of animation drivers. Can be used to implement simple time-based * animations as well as spring based animations. */ -/*package*/ abstract class AnimationDriver { - - /*package*/ boolean mHasFinished = false; - /*package*/ ValueAnimatedNode mAnimatedValue; - /*package*/ Callback mEndCallback; - /*package*/ int mId; +internal abstract class AnimationDriver { + @JvmField internal var hasFinished = false + @JvmField internal var animatedValue: ValueAnimatedNode? = null + @JvmField internal var endCallback: Callback? = null + @JvmField internal var id = 0 /** * This method gets called in the main animation loop with a frame time passed down from the * android choreographer callback. */ - public abstract void runAnimationStep(long frameTimeNanos); + abstract fun runAnimationStep(frameTimeNanos: Long) /** * This method will get called when some of the configuration gets updated while the animation is @@ -34,8 +33,8 @@ import com.facebook.react.bridge.ReadableMap; * transition. E.g. in case of a spring animation we want to keep the current value and speed and * start animating with the new properties (different destination or spring settings) */ - public void resetConfig(ReadableMap config) { - throw new JSApplicationCausedNativeException( - "Animation config for " + getClass().getSimpleName() + " cannot be reset"); + open fun resetConfig(config: ReadableMap) { + throw JSApplicationCausedNativeException( + "Animation config for ${javaClass.simpleName} cannot be reset") } } 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 index 0ad47792b95..6a08effe9a9 100644 --- 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 @@ -36,20 +36,21 @@ internal class DecayAnimation(config: ReadableMap) : AnimationDriver() { lastValue = 0.0 iterations = if (config.hasKey("iterations")) config.getInt("iterations") else 1 currentLoop = 1 - mHasFinished = iterations == 0 + hasFinished = iterations == 0 } public override fun runAnimationStep(frameTimeNanos: Long) { + val animatedValue = requireNotNull(animatedValue) { "Animated value should not be null" } 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 + if (fromValue == lastValue) { // first iteration, assign [fromValue] based on [animatedValue] + fromValue = animatedValue.nodeValue + } else { // not the first iteration, reset [animatedValue] based on [fromValue] + animatedValue.nodeValue = fromValue } - lastValue = mAnimatedValue.nodeValue + lastValue = animatedValue.nodeValue } val value = (fromValue + @@ -57,15 +58,16 @@ internal class DecayAnimation(config: ReadableMap) : AnimationDriver() { (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 + // set [startFrameTimeMillis] to -1 to reset instance variables on the next + // [runAnimationStep] startFrameTimeMillis = -1 currentLoop++ } else { // animation has completed - mHasFinished = true + hasFinished = true return } } lastValue = value - mAnimatedValue.nodeValue = value + animatedValue.nodeValue = value } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/FrameBasedAnimationDriver.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/FrameBasedAnimationDriver.kt index 517812d3a8a..ab772fb69d2 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/FrameBasedAnimationDriver.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/FrameBasedAnimationDriver.kt @@ -48,16 +48,17 @@ internal class FrameBasedAnimationDriver(config: ReadableMap) : AnimationDriver( config.getInt("iterations") else 1 currentLoop = 1 - mHasFinished = iterations == 0 + hasFinished = iterations == 0 startFrameTimeNanos = -1 } override fun runAnimationStep(frameTimeNanos: Long) { + val animatedValue = requireNotNull(animatedValue) { "Animated value should not be null" } if (startFrameTimeNanos < 0) { startFrameTimeNanos = frameTimeNanos if (currentLoop == 1) { // initiate start value when animation runs for the first time - fromValue = mAnimatedValue.nodeValue + fromValue = animatedValue.nodeValue } } val timeFromStartMillis = (frameTimeNanos - startFrameTimeNanos) / 1000000 @@ -74,7 +75,7 @@ internal class FrameBasedAnimationDriver(config: ReadableMap) : AnimationDriver( logCount++ } return - } else if (mHasFinished) { + } else if (hasFinished) { // nothing to do here return } @@ -88,12 +89,12 @@ internal class FrameBasedAnimationDriver(config: ReadableMap) : AnimationDriver( } else { // animation has completed, no more frames left nextValue = toValue - mHasFinished = true + hasFinished = true } } else { nextValue = fromValue + frames[frameIndex] * (toValue - fromValue) } - mAnimatedValue.nodeValue = nextValue + animatedValue.nodeValue = nextValue } companion object { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java index dfece9acaa9..d4d2f31c87f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java @@ -286,9 +286,9 @@ public class NativeAnimatedNodesManager implements EventDispatcherListener { throw new JSApplicationIllegalArgumentException( "startAnimatingNode: Unsupported animation type [" + animatedNodeTag + "]: " + type); } - animation.mId = animationId; - animation.mEndCallback = endCallback; - animation.mAnimatedValue = (ValueAnimatedNode) node; + animation.id = animationId; + animation.endCallback = endCallback; + animation.animatedValue = (ValueAnimatedNode) node; mActiveAnimations.put(animationId, animation); } @@ -301,21 +301,21 @@ public class NativeAnimatedNodesManager implements EventDispatcherListener { WritableArray events = null; for (int i = 0; i < mActiveAnimations.size(); i++) { AnimationDriver animation = mActiveAnimations.valueAt(i); - if (animatedNode.equals(animation.mAnimatedValue)) { - if (animation.mEndCallback != null) { + if (animatedNode.equals(animation.animatedValue)) { + if (animation.endCallback != null) { // Invoke animation end callback with {finished: false} WritableMap endCallbackResponse = Arguments.createMap(); endCallbackResponse.putBoolean("finished", false); - endCallbackResponse.putDouble("value", animation.mAnimatedValue.nodeValue); - animation.mEndCallback.invoke(endCallbackResponse); + endCallbackResponse.putDouble("value", animation.animatedValue.nodeValue); + animation.endCallback.invoke(endCallbackResponse); } else if (mReactApplicationContext != null) { // If no callback is passed in, this /may/ be an animation set up by the single-op // instruction from JS, meaning that no jsi::functions are passed into native and // we communicate via RCTDeviceEventEmitter instead of callbacks. WritableMap params = Arguments.createMap(); - params.putInt("animationId", animation.mId); + params.putInt("animationId", animation.id); params.putBoolean("finished", false); - params.putDouble("value", animation.mAnimatedValue.nodeValue); + params.putDouble("value", animation.animatedValue.nodeValue); if (events == null) { events = Arguments.createArray(); } @@ -339,21 +339,21 @@ public class NativeAnimatedNodesManager implements EventDispatcherListener { WritableArray events = null; for (int i = 0; i < mActiveAnimations.size(); i++) { AnimationDriver animation = mActiveAnimations.valueAt(i); - if (animation.mId == animationId) { - if (animation.mEndCallback != null) { + if (animation.id == animationId) { + if (animation.endCallback != null) { // Invoke animation end callback with {finished: false} WritableMap endCallbackResponse = Arguments.createMap(); endCallbackResponse.putBoolean("finished", false); - endCallbackResponse.putDouble("value", animation.mAnimatedValue.nodeValue); - animation.mEndCallback.invoke(endCallbackResponse); + endCallbackResponse.putDouble("value", animation.animatedValue.nodeValue); + animation.endCallback.invoke(endCallbackResponse); } else if (mReactApplicationContext != null) { // If no callback is passed in, this /may/ be an animation set up by the single-op // instruction from JS, meaning that no jsi::functions are passed into native and // we communicate via RCTDeviceEventEmitter instead of callbacks. WritableMap params = Arguments.createMap(); - params.putInt("animationId", animation.mId); + params.putInt("animationId", animation.id); params.putBoolean("finished", false); - params.putDouble("value", animation.mAnimatedValue.nodeValue); + params.putDouble("value", animation.animatedValue.nodeValue); if (events == null) { events = Arguments.createArray(); } @@ -652,9 +652,9 @@ public class NativeAnimatedNodesManager implements EventDispatcherListener { for (int i = 0; i < mActiveAnimations.size(); i++) { AnimationDriver animation = mActiveAnimations.valueAt(i); animation.runAnimationStep(frameTimeNanos); - AnimatedNode valueNode = animation.mAnimatedValue; + AnimatedNode valueNode = animation.animatedValue; mRunUpdateNodeList.add(valueNode); - if (animation.mHasFinished) { + if (animation.hasFinished) { hasFinishedAnimations = true; } } @@ -668,20 +668,20 @@ public class NativeAnimatedNodesManager implements EventDispatcherListener { WritableArray events = null; for (int i = mActiveAnimations.size() - 1; i >= 0; i--) { AnimationDriver animation = mActiveAnimations.valueAt(i); - if (animation.mHasFinished) { - if (animation.mEndCallback != null) { + if (animation.hasFinished) { + if (animation.endCallback != null) { WritableMap endCallbackResponse = Arguments.createMap(); endCallbackResponse.putBoolean("finished", true); - endCallbackResponse.putDouble("value", animation.mAnimatedValue.nodeValue); - animation.mEndCallback.invoke(endCallbackResponse); + endCallbackResponse.putDouble("value", animation.animatedValue.nodeValue); + animation.endCallback.invoke(endCallbackResponse); } else if (mReactApplicationContext != null) { // If no callback is passed in, this /may/ be an animation set up by the single-op // instruction from JS, meaning that no jsi::functions are passed into native and // we communicate via RCTDeviceEventEmitter instead of callbacks. WritableMap params = Arguments.createMap(); - params.putInt("animationId", animation.mId); + params.putInt("animationId", animation.id); params.putBoolean("finished", true); - params.putDouble("value", animation.mAnimatedValue.nodeValue); + params.putDouble("value", animation.animatedValue.nodeValue); if (events == null) { events = Arguments.createArray(); } 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 index 6935ccdf9a1..0f13adfafdb 100644 --- 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 @@ -55,20 +55,21 @@ internal class SpringAnimation(config: ReadableMap) : AnimationDriver() { displacementFromRestThreshold = config.getDouble("restDisplacementThreshold") overshootClampingEnabled = config.getBoolean("overshootClamping") iterations = if (config.hasKey("iterations")) config.getInt("iterations") else 1 - mHasFinished = iterations == 0 + hasFinished = iterations == 0 currentLoop = 0 timeAccumulator = 0.0 springStarted = false } override fun runAnimationStep(frameTimeNanos: Long) { + val animatedValue = requireNotNull(animatedValue) { "Animated value should not be null" } val frameTimeMillis = frameTimeNanos / 1_000_000 if (!springStarted) { if (currentLoop == 0) { - originalValue = mAnimatedValue.nodeValue + originalValue = animatedValue.nodeValue currentLoop = 1 } - currentState.position = mAnimatedValue.nodeValue + currentState.position = animatedValue.nodeValue startValue = currentState.position lastTime = frameTimeMillis timeAccumulator = 0.0 @@ -76,14 +77,14 @@ internal class SpringAnimation(config: ReadableMap) : AnimationDriver() { } advance((frameTimeMillis - lastTime) / 1000.0) lastTime = frameTimeMillis - mAnimatedValue.nodeValue = currentState.position + animatedValue.nodeValue = currentState.position if (isAtRest) { if (iterations == -1 || currentLoop < iterations) { // looping animation, return to start springStarted = false - mAnimatedValue.nodeValue = originalValue + animatedValue.nodeValue = originalValue currentLoop++ } else { // animation has completed - mHasFinished = true + hasFinished = true } } }