Files
react-native/ReactAndroid/src/main/java/com/facebook/react/animated/SpringAnimation.java
T
Krzysztof Magiera b48f7e5605 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
2018-02-16 12:10:01 -08:00

197 lines
6.6 KiB
Java

package com.facebook.react.animated;
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
* <a href="http://facebook.github.io/rebound/">http://facebook.github.io/rebound/</a>)
*/
/*package*/ 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.mValue;
mCurrentLoop = 1;
}
mStartValue = mCurrentState.position = mAnimatedValue.mValue;
mLastTime = frameTimeMillis;
mTimeAccumulator = 0.0;
mSpringStarted = true;
}
advance((frameTimeMillis - mLastTime) / 1000.0);
mLastTime = frameTimeMillis;
mAnimatedValue.mValue = mCurrentState.position;
if (isAtRest()) {
if (mIterations == -1 || mCurrentLoop < mIterations) { // looping animation, return to start
mSpringStarted = false;
mAnimatedValue.mValue = 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;
}
}
}