diff --git a/Examples/UIExplorer/js/NativeAnimationsExample.js b/Examples/UIExplorer/js/NativeAnimationsExample.js index 12f317c72..209271b36 100644 --- a/Examples/UIExplorer/js/NativeAnimationsExample.js +++ b/Examples/UIExplorer/js/NativeAnimationsExample.js @@ -41,12 +41,16 @@ class Tester extends React.Component { current = 0; onPress = () => { + const animConfig = ( + this.current && this.props.reverseConfig ? this.props.reverseConfig : this.props.config + ); this.current = this.current ? 0 : 1; const config = { - ...this.props.config, + ...animConfig, toValue: this.current, }; + // $FlowIssue #0000000 Animated[this.props.type](this.state.native, { ...config, useNativeDriver: true }).start(); Animated[this.props.type](this.state.js, { ...config, useNativeDriver: false }).start(); }; @@ -344,6 +348,32 @@ exports.examples = [ ); }, + },{ + title: 'translateX => Animated.decay', + render: function() { + return ( + + {anim => ( + + )} + + ); + }, }, { title: 'Animated value listener', diff --git a/Libraries/Animated/src/AnimatedImplementation.js b/Libraries/Animated/src/AnimatedImplementation.js index 1639b745d..05b7ea05f 100644 --- a/Libraries/Animated/src/AnimatedImplementation.js +++ b/Libraries/Animated/src/AnimatedImplementation.js @@ -351,6 +351,7 @@ class DecayAnimation extends Animation { _velocity: number; _onUpdate: (value: number) => void; _animationFrame: any; + _useNativeDriver: bool; constructor( config: DecayAnimationConfigSingle, @@ -358,13 +359,24 @@ class DecayAnimation extends Animation { super(); this._deceleration = config.deceleration !== undefined ? config.deceleration : 0.998; this._velocity = config.velocity; + this._useNativeDriver = config.useNativeDriver !== undefined ? config.useNativeDriver : false; this.__isInteraction = config.isInteraction !== undefined ? config.isInteraction : true; } + __getNativeAnimationConfig() { + return { + type: 'decay', + deceleration: this._deceleration, + velocity: this._velocity, + }; + } + start( fromValue: number, onUpdate: (value: number) => void, onEnd: ?EndCallback, + previousAnimation: ?Animation, + animatedValue: AnimatedValue, ): void { this.__active = true; this._lastValue = fromValue; @@ -372,7 +384,11 @@ class DecayAnimation extends Animation { this._onUpdate = onUpdate; this.__onEnd = onEnd; this._startTime = Date.now(); - this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this)); + if (this._useNativeDriver) { + this.__startNativeAnimation(animatedValue); + } else { + this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this)); + } } onUpdate(): void { diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/DecayAnimation.java b/ReactAndroid/src/main/java/com/facebook/react/animated/DecayAnimation.java new file mode 100644 index 000000000..84dac0623 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/DecayAnimation.java @@ -0,0 +1,54 @@ +/** + * 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.ReadableMap; + +/** + * Implementation of {@link AnimationDriver} providing support for decay animations. The + * implementation is copied from the JS version in {@code AnimatedImplementation.js}. + */ +public class DecayAnimation extends AnimationDriver { + + private final double mVelocity; + private final double mDeceleration; + + private long mStartFrameTimeMillis = -1; + private double mFromValue; + private double mLastValue; + + public DecayAnimation(ReadableMap config) { + mVelocity = config.getDouble("velocity"); + mDeceleration = config.getDouble("deceleration"); + } + + @Override + public void runAnimationStep(long frameTimeNanos) { + long frameTimeMillis = frameTimeNanos / 1000000; + if (mStartFrameTimeMillis == -1) { + // since this is the first animation step, consider the start to be on the previous frame + mStartFrameTimeMillis = frameTimeMillis - 16; + mFromValue = mAnimatedValue.mValue; + mLastValue = mAnimatedValue.mValue; + } + + final double value = mFromValue + + (mVelocity / (1 - mDeceleration)) * + (1 - Math.exp(-(1 - mDeceleration) * (frameTimeMillis - mStartFrameTimeMillis))); + + if (Math.abs(mLastValue - value) < 0.1) { + mHasFinished = true; + return; + } + + mLastValue = value; + mAnimatedValue.mValue = value; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java index 67a735e16..fe6613e03 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java @@ -139,6 +139,8 @@ import javax.annotation.Nullable; animation = new FrameBasedAnimationDriver(animationConfig); } else if ("spring".equals(type)) { animation = new SpringAnimation(animationConfig); + } else if ("decay".equals(type)) { + animation = new DecayAnimation(animationConfig); } else { throw new JSApplicationIllegalArgumentException("Unsupported animation type: " + type); } diff --git a/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java index a499aa26a..2f5227d8e 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java @@ -17,7 +17,6 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.uimanager.ReactStylesDiffMap; import com.facebook.react.uimanager.UIImplementation; -import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -259,6 +258,63 @@ public class NativeAnimatedNodeTraversalTest { verifyNoMoreInteractions(mUIImplementationMock); } + @Test + public void testDecayAnimation() { + createSimpleAnimatedViewWithOpacity(1000, 0d); + + Callback animationCallback = mock(Callback.class); + mNativeAnimatedNodesManager.startAnimatingNode( + 1, + 1, + JavaOnlyMap.of( + "type", + "decay", + "velocity", + 0.5d, + "deceleration", + 0.998d), + animationCallback); + + ArgumentCaptor stylesCaptor = + ArgumentCaptor.forClass(ReactStylesDiffMap.class); + + reset(mUIImplementationMock); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); + verify(mUIImplementationMock, atMost(1)) + .synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture()); + double previousValue = stylesCaptor.getValue().getDouble("opacity", Double.NaN); + double previousDiff = Double.POSITIVE_INFINITY; + /* 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); + double currentDiff = currentValue - previousValue; + // verify monotonicity + // greater *or equal* because the animation stops during these 3 seconds + assertThat(currentValue).as("on frame " + i).isGreaterThanOrEqualTo(previousValue); + // verify decay + if (i > 3) { + // i > 3 because that's how long it takes to settle previousDiff + if (i % 3 != 0) { + // i % 3 != 0 because every 3 frames we go a tiny + // bit faster, because frame length is 16.(6)ms + assertThat(currentDiff).as("on frame " + i).isLessThanOrEqualTo(previousDiff); + } else { + assertThat(currentDiff).as("on frame " + i).isGreaterThanOrEqualTo(previousDiff); + } + } + previousValue = currentValue; + previousDiff = currentDiff; + } + // should be done in 3s + reset(mUIImplementationMock); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); + verifyNoMoreInteractions(mUIImplementationMock); + } + @Test public void testAnimationCallbackFinish() { createSimpleAnimatedViewWithOpacity(1000, 0d);