Skip to content

Commit

Permalink
Kotlinify AnimationDriver (facebook#45836)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: facebook#45836

# Changelog:
[Internal] -

As in the title.

Reviewed By: cortinico

Differential Revision: D60502487

fbshipit-source-id: 003f2eade8f8425636b2023397ec29545175b487
  • Loading branch information
rshest authored and facebook-github-bot committed Aug 1, 2024
1 parent fdfa0b1 commit c30ee46
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,36 @@
* 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
* running. In that case animation should restart keeping its internal state to provide a smooth
* 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")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,36 +36,38 @@ 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 +
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
// 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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -74,7 +75,7 @@ internal class FrameBasedAnimationDriver(config: ReadableMap) : AnimationDriver(
logCount++
}
return
} else if (mHasFinished) {
} else if (hasFinished) {
// nothing to do here
return
}
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,9 @@ public void startAnimatingNode(
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);
}

Expand All @@ -301,21 +301,21 @@ private void stopAnimationsForNode(AnimatedNode animatedNode) {
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();
}
Expand All @@ -339,21 +339,21 @@ public void stopAnimation(int animationId) {
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();
}
Expand Down Expand Up @@ -652,9 +652,9 @@ public void runUpdates(long frameTimeNanos) {
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;
}
}
Expand All @@ -668,20 +668,20 @@ public void runUpdates(long frameTimeNanos) {
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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,35 +55,36 @@ 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
springStarted = true
}
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
}
}
}
Expand Down

0 comments on commit c30ee46

Please sign in to comment.