mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-05-07 02:08:34 +08:00
Support for animated tracking in native driver
Summary: This PR adds support for Animated tracking to Animated Native Driver implementation on Android and iOS. Animated tracking allows for animation to be started with a "dynamic" end value. Instead of passing a fixed number as end value we can pass a reference to another Animated.Value. Then when that value changes, the animation will be reconfigured to drive the animation to the new destination point. What is important is that animation will keep its state in the process of updating "toValue". That is if it is a spring animation and the end value changes while the previous animation still hasn't settled the new animation will start from the current position and will inherit current velocity. This makes end value transitions very smooth. Animated tracking is available in JS implementation of Animated library but not in the native implementation. Therefore until now, it wasn't possible to utilize native driver when using animated tracking. Offloading animation from JS thread turns out to be crucial for gesture driven animations. This PR is a step forward towards feature parity between JS and native implementations of Animated. Here is a link to example video that shows how tracking can be used to implement chat heads effect: https://twitter.com/kzzzf/status/958362032650244101 In addition this PR fixes an issue with frames animation driver on Android that because of rounding issues was taking one extra frame to start. Because of that change I had to update a number of Android unit tests that were relying on that behavior and running that one additional animation step prior to performing checks. As a part of this PR I'm adding three unit tests for each of the platforms that verifies most important aspects of this implementation. Please refer to the code and look at the test cases top level comments to learn what they do. I'm also adding a section to "Native Animated Example" screen in RNTester app that provides a test case for tracking. In the example we have blue square that fallows the red line drawn on screen. Line uses Animated.Value for it's position while square is connected via tracking spring animation to that value. So it is ought to follow the line. When user taps in the area surrounding the button new position for the red line is selected at random and the value updates. Then we can watch blue screen animate to that position. You can also refer to this video that I use to demonstrate how tracking can be linked with native gesture events using react-native-gesture-handler lib: https://twitter.com/kzzzf/status/958362032650244101 [GENERAL][FEATURE][Native Animated] - Added support for animated tracking to native driver. Now you can use `useNativeDriver` flag with animations that track other Animated.Values Closes https://github.com/facebook/react-native/pull/17896 Differential Revision: D6974170 Pulled By: hramos fbshipit-source-id: 50e918b36ee10f80c1deb866c955661d4cc2619b
This commit is contained in:
committed by
Facebook Github Bot
parent
574c70e771
commit
b48f7e5605
@@ -10,6 +10,8 @@
|
||||
package com.facebook.react.animated;
|
||||
|
||||
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
|
||||
@@ -27,4 +29,15 @@ import com.facebook.react.bridge.Callback;
|
||||
* android choreographer callback.
|
||||
*/
|
||||
public abstract void runAnimationStep(long frameTimeNanos);
|
||||
|
||||
/**
|
||||
* 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
|
||||
* transision. 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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,20 +18,28 @@ import com.facebook.react.bridge.ReadableMap;
|
||||
public class DecayAnimation extends AnimationDriver {
|
||||
|
||||
private final double mVelocity;
|
||||
private final double mDeceleration;
|
||||
|
||||
private long mStartFrameTimeMillis = -1;
|
||||
private double mFromValue = 0d;
|
||||
private double mLastValue = 0d;
|
||||
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");
|
||||
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
|
||||
|
||||
@@ -22,17 +22,24 @@ class FrameBasedAnimationDriver extends AnimationDriver {
|
||||
// 60FPS
|
||||
private static final double FRAME_TIME_MILLIS = 1000d / 60d;
|
||||
|
||||
private long mStartFrameTimeNanos = -1;
|
||||
private final double[] mFrames;
|
||||
private final double mToValue;
|
||||
private long mStartFrameTimeNanos;
|
||||
private double[] mFrames;
|
||||
private double mToValue;
|
||||
private double mFromValue;
|
||||
private int mIterations;
|
||||
private int mCurrentLoop;
|
||||
|
||||
FrameBasedAnimationDriver(ReadableMap config) {
|
||||
resetConfig(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetConfig(ReadableMap config) {
|
||||
ReadableArray frames = config.getArray("frames");
|
||||
int numberOfFrames = frames.size();
|
||||
mFrames = new double[numberOfFrames];
|
||||
if (mFrames == null || mFrames.length != numberOfFrames) {
|
||||
mFrames = new double[numberOfFrames];
|
||||
}
|
||||
for (int i = 0; i < numberOfFrames; i++) {
|
||||
mFrames[i] = frames.getDouble(i);
|
||||
}
|
||||
@@ -40,6 +47,7 @@ class FrameBasedAnimationDriver extends AnimationDriver {
|
||||
mIterations = config.hasKey("iterations") ? config.getInt("iterations") : 1;
|
||||
mCurrentLoop = 1;
|
||||
mHasFinished = mIterations == 0;
|
||||
mStartFrameTimeNanos = -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -49,7 +57,7 @@ class FrameBasedAnimationDriver extends AnimationDriver {
|
||||
mFromValue = mAnimatedValue.mValue;
|
||||
}
|
||||
long timeFromStartMillis = (frameTimeNanos - mStartFrameTimeNanos) / 1000000;
|
||||
int frameIndex = (int) (timeFromStartMillis / FRAME_TIME_MILLIS);
|
||||
int frameIndex = (int) Math.round(timeFromStartMillis / FRAME_TIME_MILLIS);
|
||||
if (frameIndex < 0) {
|
||||
throw new IllegalStateException("Calculated frame index should never be lower than 0");
|
||||
} else if (mHasFinished) {
|
||||
@@ -60,7 +68,7 @@ class FrameBasedAnimationDriver extends AnimationDriver {
|
||||
if (frameIndex >= mFrames.length - 1) {
|
||||
nextValue = mToValue;
|
||||
if (mIterations == -1 || mCurrentLoop < mIterations) { // looping animation, return to start
|
||||
mStartFrameTimeNanos = frameTimeNanos;
|
||||
mStartFrameTimeNanos = frameTimeNanos + ((long) FRAME_TIME_MILLIS) * 1000000L;
|
||||
mCurrentLoop++;
|
||||
} else { // animation has completed, no more frames left
|
||||
mHasFinished = true;
|
||||
|
||||
@@ -105,6 +105,8 @@ import javax.annotation.Nullable;
|
||||
node = new DiffClampAnimatedNode(config, this);
|
||||
} else if ("transform".equals(type)) {
|
||||
node = new TransformAnimatedNode(config, this);
|
||||
} else if ("tracking".equals(type)) {
|
||||
node = new TrackingAnimatedNode(config, this);
|
||||
} else {
|
||||
throw new JSApplicationIllegalArgumentException("Unsupported node type: " + type);
|
||||
}
|
||||
@@ -189,6 +191,15 @@ import javax.annotation.Nullable;
|
||||
throw new JSApplicationIllegalArgumentException("Animated node should be of type " +
|
||||
ValueAnimatedNode.class.getName());
|
||||
}
|
||||
|
||||
final AnimationDriver existingDriver = mActiveAnimations.get(animationId);
|
||||
if (existingDriver != null) {
|
||||
// animation with the given ID is already running, we need to update its configuration instead
|
||||
// of spawning a new one
|
||||
existingDriver.resetConfig(animationConfig);
|
||||
return;
|
||||
}
|
||||
|
||||
String type = animationConfig.getString("type");
|
||||
final AnimationDriver animation;
|
||||
if ("frames".equals(type)) {
|
||||
@@ -214,10 +225,12 @@ import javax.annotation.Nullable;
|
||||
for (int i = 0; i < mActiveAnimations.size(); i++) {
|
||||
AnimationDriver animation = mActiveAnimations.valueAt(i);
|
||||
if (animatedNode.equals(animation.mAnimatedValue)) {
|
||||
// Invoke animation end callback with {finished: false}
|
||||
WritableMap endCallbackResponse = Arguments.createMap();
|
||||
endCallbackResponse.putBoolean("finished", false);
|
||||
animation.mEndCallback.invoke(endCallbackResponse);
|
||||
if (animation.mEndCallback != null) {
|
||||
// Invoke animation end callback with {finished: false}
|
||||
WritableMap endCallbackResponse = Arguments.createMap();
|
||||
endCallbackResponse.putBoolean("finished", false);
|
||||
animation.mEndCallback.invoke(endCallbackResponse);
|
||||
}
|
||||
mActiveAnimations.removeAt(i);
|
||||
i--;
|
||||
}
|
||||
@@ -232,10 +245,12 @@ import javax.annotation.Nullable;
|
||||
for (int i = 0; i < mActiveAnimations.size(); i++) {
|
||||
AnimationDriver animation = mActiveAnimations.valueAt(i);
|
||||
if (animation.mId == animationId) {
|
||||
// Invoke animation end callback with {finished: false}
|
||||
WritableMap endCallbackResponse = Arguments.createMap();
|
||||
endCallbackResponse.putBoolean("finished", false);
|
||||
animation.mEndCallback.invoke(endCallbackResponse);
|
||||
if (animation.mEndCallback != null) {
|
||||
// Invoke animation end callback with {finished: false}
|
||||
WritableMap endCallbackResponse = Arguments.createMap();
|
||||
endCallbackResponse.putBoolean("finished", false);
|
||||
animation.mEndCallback.invoke(endCallbackResponse);
|
||||
}
|
||||
mActiveAnimations.removeAt(i);
|
||||
return;
|
||||
}
|
||||
@@ -445,9 +460,11 @@ import javax.annotation.Nullable;
|
||||
for (int i = mActiveAnimations.size() - 1; i >= 0; i--) {
|
||||
AnimationDriver animation = mActiveAnimations.valueAt(i);
|
||||
if (animation.mHasFinished) {
|
||||
WritableMap endCallbackResponse = Arguments.createMap();
|
||||
endCallbackResponse.putBoolean("finished", true);
|
||||
animation.mEndCallback.invoke(endCallbackResponse);
|
||||
if (animation.mEndCallback != null) {
|
||||
WritableMap endCallbackResponse = Arguments.createMap();
|
||||
endCallbackResponse.putBoolean("finished", true);
|
||||
animation.mEndCallback.invoke(endCallbackResponse);
|
||||
}
|
||||
mActiveAnimations.removeAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,24 +37,32 @@ import com.facebook.react.bridge.ReadableMap;
|
||||
// thresholds for determining when the spring is at rest
|
||||
private double mRestSpeedThreshold;
|
||||
private double mDisplacementFromRestThreshold;
|
||||
private double mTimeAccumulator = 0;
|
||||
private double mTimeAccumulator;
|
||||
// for controlling loop
|
||||
private int mIterations;
|
||||
private int mCurrentLoop = 0;
|
||||
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 = config.getDouble("initialVelocity");
|
||||
mCurrentState.velocity = mInitialVelocity;
|
||||
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
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
package com.facebook.react.animated;
|
||||
|
||||
import com.facebook.react.bridge.JavaOnlyMap;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
|
||||
/* package */ class TrackingAnimatedNode extends AnimatedNode {
|
||||
|
||||
private final NativeAnimatedNodesManager mNativeAnimatedNodesManager;
|
||||
private final int mAnimationId;
|
||||
private final int mToValueNode;
|
||||
private final int mValueNode;
|
||||
private final JavaOnlyMap mAnimationConfig;
|
||||
|
||||
TrackingAnimatedNode(ReadableMap config, NativeAnimatedNodesManager nativeAnimatedNodesManager) {
|
||||
mNativeAnimatedNodesManager = nativeAnimatedNodesManager;
|
||||
mAnimationId = config.getInt("animationId");
|
||||
mToValueNode = config.getInt("toValue");
|
||||
mValueNode = config.getInt("value");
|
||||
mAnimationConfig = JavaOnlyMap.deepClone(config.getMap("animationConfig"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update() {
|
||||
AnimatedNode toValue = mNativeAnimatedNodesManager.getNodeById(mToValueNode);
|
||||
mAnimationConfig.putDouble("toValue", ((ValueAnimatedNode) toValue).getValue());
|
||||
mNativeAnimatedNodesManager.startAnimatingNode(mAnimationId, mValueNode, mAnimationConfig, null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user