Permalink
Browse files

Support for spring animations

Summary:
This change adds support for spring animations to be run off the JS thread on android. The implementation is based on the android spring implementation from Rebound (http://facebook.github.io/rebound/) but since only a small subset of the library is used the relevant parts are copied instead of making RN to import the whole library.

**Test Plan**
Run java tests: `buck test ReactAndroid/src/test/java/com/facebook/react/animated`
Add `useNativeDriver: true` to spring animation in animated example app, run it on android
Closes #8860

Differential Revision: D3676436

fbshipit-source-id: 3a4b1b006725a938562712989b93dd4090577c48
  • Loading branch information...
1 parent 0222107 commit 8f75d7346f1b80a4e7139f138ff409c8244c538a @kmagiera kmagiera committed with Facebook Github Bot 2 Aug 5, 2016
@@ -449,6 +449,7 @@ class SpringAnimation extends Animation {
_lastTime: number;
_onUpdate: (value: number) => void;
_animationFrame: any;
+ _useNativeDriver: bool;
constructor(
config: SpringAnimationConfigSingle,
@@ -461,6 +462,7 @@ class SpringAnimation extends Animation {
this._initialVelocity = config.velocity;
this._lastVelocity = withDefault(config.velocity, 0);
this._toValue = config.toValue;
+ this._useNativeDriver = config.useNativeDriver !== undefined ? config.useNativeDriver : false;
this.__isInteraction = config.isInteraction !== undefined ? config.isInteraction : true;
var springConfig;
@@ -483,11 +485,25 @@ class SpringAnimation extends Animation {
this._friction = springConfig.friction;
}
+ _getNativeAnimationConfig() {
+ return {
+ type: 'spring',
+ overshootClamping: this._overshootClamping,
+ restDisplacementThreshold: this._restDisplacementThreshold,
+ restSpeedThreshold: this._restSpeedThreshold,
+ tension: this._tension,
+ friction: this._friction,
+ initialVelocity: withDefault(this._initialVelocity, this._lastVelocity),
+ toValue: this._toValue,
+ };
+ }
+
start(
fromValue: number,
onUpdate: (value: number) => void,
onEnd: ?EndCallback,
previousAnimation: ?Animation,
+ animatedValue: AnimatedValue
): void {
this.__active = true;
this._startPosition = fromValue;
@@ -507,7 +523,11 @@ class SpringAnimation extends Animation {
this._initialVelocity !== null) {
this._lastVelocity = this._initialVelocity;
}
- this.onUpdate();
+ if (this._useNativeDriver) {
+ this.__startNativeAnimation(animatedValue);
+ } else {
+ this.onUpdate();
+ }
}
getInternalState(): Object {
@@ -135,6 +135,8 @@ public void startAnimatingNode(
final AnimationDriver animation;
if ("frames".equals(type)) {
animation = new FrameBasedAnimationDriver(animationConfig);
+ } else if ("spring".equals(type)) {
+ animation = new SpringAnimation(animationConfig);
} else {
throw new JSApplicationIllegalArgumentException("Unsupported animation type: " + type);
}
@@ -0,0 +1,218 @@
+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 mSpringFriction;
+ private double mSpringTension;
+ private boolean mOvershootClampingEnabled;
+
+ // all physics simulation objects are final and reused in each processing pass
+ private final PhysicsState mCurrentState = new PhysicsState();
+ private final PhysicsState mPreviousState = new PhysicsState();
+ private final PhysicsState mTempState = 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 = 0;
+
+ SpringAnimation(ReadableMap config) {
+ mSpringFriction = config.getDouble("friction");
+ mSpringTension = config.getDouble("tension");
+ mCurrentState.velocity = config.getDouble("initialVelocity");
+ mEndValue = config.getDouble("toValue");
+ mRestSpeedThreshold = config.getDouble("restSpeedThreshold");
+ mDisplacementFromRestThreshold = config.getDouble("restDisplacementThreshold");
+ mOvershootClampingEnabled = config.getBoolean("overshootClamping");
+ }
+
+ @Override
+ public void runAnimationStep(long frameTimeNanos) {
+ long frameTimeMillis = frameTimeNanos / 1000000;
+ if (!mSpringStarted) {
+ mStartValue = mCurrentState.position = mAnimatedValue.mValue;
+ mLastTime = frameTimeMillis;
+ mSpringStarted = true;
+ }
+ advance((frameTimeMillis - mLastTime) / 1000.0);
+ mLastTime = frameTimeMillis;
+ mAnimatedValue.mValue = mCurrentState.position;
+ mHasFinished = isAtRest();
+ }
+
+ /**
+ * 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 ||
+ mSpringTension == 0);
+ }
+
+ /**
+ * Check if the spring is overshooting beyond its target.
+ * @return true if the spring is overshooting its target
+ */
+ private boolean isOvershooting() {
+ return mSpringTension > 0 &&
+ ((mStartValue < mEndValue && mCurrentState.position > mEndValue) ||
+ (mStartValue > mEndValue && mCurrentState.position < mEndValue));
+ }
+
+ /**
+ * linear interpolation between the previous and current physics state based on the amount of
+ * timestep remaining after processing the rendering delta time in timestep sized chunks.
+ * @param alpha from 0 to 1, where 0 is the previous state, 1 is the current state
+ */
+ private void interpolate(double alpha) {
+ mCurrentState.position = mCurrentState.position * alpha + mPreviousState.position *(1-alpha);
+ mCurrentState.velocity = mCurrentState.velocity * alpha + mPreviousState.velocity *(1-alpha);
+ }
+
+ /**
+ * advance the physics simulation in SOLVER_TIMESTEP_SEC sized chunks to fulfill the required
+ * realTimeDelta.
+ * The math is inlined inside the loop since it made a huge performance impact when there are
+ * several springs being advanced.
+ * @param time clock time
+ * @param realDeltaTime clock drift
+ */
+ 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 tension = mSpringTension;
+ double friction = mSpringFriction;
+
+ double position = mCurrentState.position;
+ double velocity = mCurrentState.velocity;
+ double tempPosition = mTempState.position;
+ double tempVelocity = mTempState.velocity;
+
+ double aVelocity, aAcceleration;
+ double bVelocity, bAcceleration;
+ double cVelocity, cAcceleration;
+ double dVelocity, dAcceleration;
+
+ double dxdt, dvdt;
+
+ // iterate over the true time
+ while (mTimeAccumulator >= SOLVER_TIMESTEP_SEC) {
+ /* begin debug
+ iterations++;
+ end debug */
+ mTimeAccumulator -= SOLVER_TIMESTEP_SEC;
+
+ if (mTimeAccumulator < SOLVER_TIMESTEP_SEC) {
+ // This will be the last iteration. Remember the previous state in case we need to
+ // interpolate
+ mPreviousState.position = position;
+ mPreviousState.velocity = velocity;
+ }
+
+ // Perform an RK4 integration to provide better detection of the acceleration curve via
+ // sampling of Euler integrations at 4 intervals feeding each derivative into the calculation
+ // of the next and taking a weighted sum of the 4 derivatives as the final output.
+
+ // This math was inlined since it made for big performance improvements when advancing several
+ // springs in one pass of the BaseSpringSystem.
+
+ // The initial derivative is based on the current velocity and the calculated acceleration
+ aVelocity = velocity;
+ aAcceleration = (tension * (mEndValue - tempPosition)) - friction * velocity;
+
+ // Calculate the next derivatives starting with the last derivative and integrating over the
+ // timestep
+ tempPosition = position + aVelocity * SOLVER_TIMESTEP_SEC * 0.5;
+ tempVelocity = velocity + aAcceleration * SOLVER_TIMESTEP_SEC * 0.5;
+ bVelocity = tempVelocity;
+ bAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity;
+
+ tempPosition = position + bVelocity * SOLVER_TIMESTEP_SEC * 0.5;
+ tempVelocity = velocity + bAcceleration * SOLVER_TIMESTEP_SEC * 0.5;
+ cVelocity = tempVelocity;
+ cAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity;
+
+ tempPosition = position + cVelocity * SOLVER_TIMESTEP_SEC;
+ tempVelocity = velocity + cAcceleration * SOLVER_TIMESTEP_SEC;
+ dVelocity = tempVelocity;
+ dAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity;
+
+ // Take the weighted sum of the 4 derivatives as the final output.
+ dxdt = 1.0/6.0 * (aVelocity + 2.0 * (bVelocity + cVelocity) + dVelocity);
+ dvdt = 1.0/6.0 * (aAcceleration + 2.0 * (bAcceleration + cAcceleration) + dAcceleration);
+
+ position += dxdt * SOLVER_TIMESTEP_SEC;
+ velocity += dvdt * SOLVER_TIMESTEP_SEC;
+ }
+
+ mTempState.position = tempPosition;
+ mTempState.velocity = tempVelocity;
+
+ mCurrentState.position = position;
+ mCurrentState.velocity = velocity;
+
+ if (mTimeAccumulator > 0) {
+ interpolate(mTimeAccumulator / SOLVER_TIMESTEP_SEC);
+ }
+
+ // 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 (tension > 0) {
+ mStartValue = mEndValue;
+ mCurrentState.position = mEndValue;
+ } else {
+ mEndValue = mCurrentState.position;
+ mStartValue = mEndValue;
+ }
+ mCurrentState.velocity = 0;
+ }
+ }
+}
@@ -35,6 +35,7 @@
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.atMost;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
@@ -199,6 +200,66 @@ public void testNodeValueListenerIfListening() {
}
@Test
+ public void testSpringAnimation() {
+ createSimpleAnimatedViewWithOpacity(1000, 0d);
+
+ Callback animationCallback = mock(Callback.class);
+ mNativeAnimatedNodesManager.startAnimatingNode(
+ 1,
+ 1,
+ JavaOnlyMap.of(
+ "type",
+ "spring",
+ "friction",
+ 7d,
+ "tension",
+ 40.0d,
+ "initialVelocity",
+ 0d,
+ "toValue",
+ 1d,
+ "restSpeedThreshold",
+ 0.001d,
+ "restDisplacementThreshold",
+ 0.001d,
+ "overshootClamping",
+ false),
+ animationCallback);
+
+ ArgumentCaptor<ReactStylesDiffMap> stylesCaptor =
+ ArgumentCaptor.forClass(ReactStylesDiffMap.class);
+
+ reset(mUIImplementationMock);
+ mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
+ verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
+ assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN)).isEqualTo(0);
+
+ double previousValue = 0d;
+ boolean wasGreaterThanOne = false;
+ /* run 3 secs of animation */
+ for (int i = 0; i < 3 * 60; i++) {
+ reset(mUIImplementationMock);
+ mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
+ verify(mUIImplementationMock, atMost(1))
+ .synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
+ double currentValue = stylesCaptor.getValue().getDouble("opacity", Double.NaN);
+ if (currentValue > 1d) {
+ wasGreaterThanOne = true;
+ }
+ // verify that animation step is relatively small
+ assertThat(Math.abs(currentValue - previousValue)).isLessThan(0.1d);
+ previousValue = currentValue;
+ }
+ // verify that we've reach the final value at the end of animation
+ assertThat(previousValue).isEqualTo(1d);
+ // verify that value has reached some maximum value that is greater than the final value (bounce)
+ assertThat(wasGreaterThanOne);
+ reset(mUIImplementationMock);
+ mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
+ verifyNoMoreInteractions(mUIImplementationMock);
+ }
+
+ @Test
public void testAnimationCallbackFinish() {
createSimpleAnimatedViewWithOpacity(1000, 0d);

0 comments on commit 8f75d73

Please sign in to comment.