diff --git a/Examples/UIExplorer/js/NativeAnimationsExample.js b/Examples/UIExplorer/js/NativeAnimationsExample.js
index ef92dae605f421..bd7add201d258d 100644
--- a/Examples/UIExplorer/js/NativeAnimationsExample.js
+++ b/Examples/UIExplorer/js/NativeAnimationsExample.js
@@ -22,16 +22,15 @@
*/
'use strict';
-var React = require('react');
-var ReactNative = require('react-native');
-var {
+const React = require('react');
+const ReactNative = require('react-native');
+const {
View,
Text,
Animated,
StyleSheet,
TouchableWithoutFeedback,
} = ReactNative;
-var UIExplorerButton = require('./UIExplorerButton');
class Tester extends React.Component {
state = {
@@ -47,12 +46,8 @@ class Tester extends React.Component {
...this.props.config,
toValue: this.current,
};
- try {
- Animated[this.props.type](this.state.native, { ...config, useNativeDriver: true }).start();
- } catch (e) {
- // uncomment this if you want to get the redbox errors!
- throw e;
- }
+
+ Animated[this.props.type](this.state.native, { ...config, useNativeDriver: true }).start();
Animated[this.props.type](this.state.js, { ...config, useNativeDriver: false }).start();
};
@@ -78,6 +73,52 @@ class Tester extends React.Component {
}
}
+class ValueListenerExample extends React.Component {
+ state = {
+ anim: new Animated.Value(0),
+ progress: 0,
+ };
+ _current = 0;
+
+ componentDidMount() {
+ this.state.anim.addListener((e) => this.setState({ progress: e.value }));
+ }
+
+ componentWillUnmount() {
+ this.state.anim.removeAllListeners();
+ }
+
+ _onPress = () => {
+ this._current = this._current ? 0 : 1;
+ const config = {
+ duration: 1000,
+ toValue: this._current,
+ };
+
+ Animated.timing(this.state.anim, { ...config, useNativeDriver: true }).start();
+ };
+
+ render() {
+ return (
+
+
+
+
+
+ Value: {this.state.progress}
+
+
+ );
+ }
+}
+
const styles = StyleSheet.create({
row: {
padding: 10,
@@ -304,4 +345,13 @@ exports.examples = [
);
},
},
+ {
+ title: 'Animated value listener',
+ platform: 'android',
+ render: function() {
+ return (
+
+ );
+ },
+ },
];
diff --git a/Libraries/Animated/src/AnimatedImplementation.js b/Libraries/Animated/src/AnimatedImplementation.js
index 7f21c4ffb5b41f..3d78bfed4484c2 100644
--- a/Libraries/Animated/src/AnimatedImplementation.js
+++ b/Libraries/Animated/src/AnimatedImplementation.js
@@ -11,6 +11,7 @@
*/
'use strict';
+var DeviceEventEmitter = require('RCTDeviceEventEmitter');
var InteractionManager = require('InteractionManager');
var Interpolation = require('Interpolation');
var React = require('React');
@@ -634,6 +635,7 @@ class AnimatedValue extends AnimatedWithChildren {
_animation: ?Animation;
_tracking: ?Animated;
_listeners: {[key: string]: ValueListenerCallback};
+ __nativeAnimatedValueListener: ?any;
constructor(value: number) {
super();
@@ -652,6 +654,14 @@ class AnimatedValue extends AnimatedWithChildren {
return this._value + this._offset;
}
+ __makeNative() {
+ super.__makeNative();
+
+ if (Object.keys(this._listeners).length) {
+ this._startListeningToNativeValueUpdates();
+ }
+ }
+
/**
* Directly set the value. This will stop any animations running on the value
* and update all the bound properties.
@@ -693,15 +703,49 @@ class AnimatedValue extends AnimatedWithChildren {
addListener(callback: ValueListenerCallback): string {
var id = String(_uniqueId++);
this._listeners[id] = callback;
+ if (this.__isNative) {
+ this._startListeningToNativeValueUpdates();
+ }
return id;
}
removeListener(id: string): void {
delete this._listeners[id];
+ if (this.__isNative && Object.keys(this._listeners).length === 0) {
+ this._stopListeningForNativeValueUpdates();
+ }
}
removeAllListeners(): void {
this._listeners = {};
+ if (this.__isNative) {
+ this._stopListeningForNativeValueUpdates();
+ }
+ }
+
+ _startListeningToNativeValueUpdates() {
+ if (this.__nativeAnimatedValueListener ||
+ !NativeAnimatedHelper.supportsNativeListener()) {
+ return;
+ }
+
+ NativeAnimatedAPI.startListeningToAnimatedNodeValue(this.__getNativeTag());
+ this.__nativeAnimatedValueListener = DeviceEventEmitter.addListener('onAnimatedValueUpdate', (data) => {
+ if (data.tag !== this.__getNativeTag()) {
+ return;
+ }
+ this._updateValue(data.value, false /* flush */);
+ });
+ }
+
+ _stopListeningForNativeValueUpdates() {
+ if (!this.__nativeAnimatedValueListener ||
+ !NativeAnimatedHelper.supportsNativeListener()) {
+ return;
+ }
+
+ this.__nativeAnimatedValueListener.remove();
+ NativeAnimatedAPI.stopListeningToAnimatedNodeValue(this.__getNativeTag());
}
/**
@@ -1204,7 +1248,7 @@ class AnimatedStyle extends AnimatedWithChildren {
if (value instanceof Animated) {
if (!value.__isNative) {
// We cannot use value of natively driven nodes this way as the value we have access from JS
- // may not be up to date
+ // may not be up to date.
style[key] = value.__getValue();
}
} else {
@@ -1296,9 +1340,9 @@ class AnimatedProps extends Animated {
for (var key in this._props) {
var value = this._props[key];
if (value instanceof Animated) {
- if (!value.__isNative) {
+ if (!value.__isNative || value instanceof AnimatedStyle) {
// We cannot use value of natively driven nodes this way as the value we have access from JS
- // may not be up to date
+ // may not be up to date.
props[key] = value.__getValue();
}
} else {
diff --git a/Libraries/Animated/src/NativeAnimatedHelper.js b/Libraries/Animated/src/NativeAnimatedHelper.js
index 3380df240976a6..9138a4308e351b 100644
--- a/Libraries/Animated/src/NativeAnimatedHelper.js
+++ b/Libraries/Animated/src/NativeAnimatedHelper.js
@@ -30,6 +30,14 @@ var API = {
assertNativeAnimatedModule();
NativeAnimatedModule.createAnimatedNode(tag, config);
},
+ startListeningToAnimatedNodeValue: function(tag: number) {
+ assertNativeAnimatedModule();
+ NativeAnimatedModule.startListeningToAnimatedNodeValue(tag);
+ },
+ stopListeningToAnimatedNodeValue: function(tag: number) {
+ assertNativeAnimatedModule();
+ NativeAnimatedModule.stopListeningToAnimatedNodeValue(tag);
+ },
connectAnimatedNodes: function(parentTag: number, childTag: number): void {
assertNativeAnimatedModule();
NativeAnimatedModule.connectAnimatedNodes(parentTag, childTag);
@@ -144,6 +152,11 @@ function assertNativeAnimatedModule(): void {
invariant(NativeAnimatedModule, 'Native animated module is not available');
}
+// TODO: remove this when iOS supports native listeners.
+function supportsNativeListener(): bool {
+ return !!NativeAnimatedModule.startListeningToAnimatedNodeValue;
+}
+
module.exports = {
API,
validateProps,
@@ -153,4 +166,5 @@ module.exports = {
generateNewNodeTag,
generateNewAnimationId,
assertNativeAnimatedModule,
+ supportsNativeListener,
};
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/AnimatedNodeValueListener.java b/ReactAndroid/src/main/java/com/facebook/react/animated/AnimatedNodeValueListener.java
new file mode 100644
index 00000000000000..fa0248cb3e1947
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/animated/AnimatedNodeValueListener.java
@@ -0,0 +1,17 @@
+/**
+ * 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;
+
+/**
+ * Interface used to listen to {@link ValueAnimatedNode} updates.
+ */
+public interface AnimatedNodeValueListener {
+ void onValueUpdate(double value);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/BUCK b/ReactAndroid/src/main/java/com/facebook/react/animated/BUCK
index 73444282c089f7..d6c34221f18ed1 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/animated/BUCK
+++ b/ReactAndroid/src/main/java/com/facebook/react/animated/BUCK
@@ -7,8 +7,8 @@ android_library(
]),
deps = [
react_native_target('java/com/facebook/react/bridge:bridge'),
+ react_native_target('java/com/facebook/react/modules/core:core'),
react_native_target('java/com/facebook/react/uimanager:uimanager'),
-
react_native_target('java/com/facebook/react/uimanager/annotations:annotations'),
react_native_dep('third-party/java/infer-annotations:infer-annotations'),
react_native_dep('third-party/java/jsr-305:jsr-305'),
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java
index ab4e19fc534565..303fb83253bc5f 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java
@@ -12,6 +12,7 @@
import javax.annotation.Nullable;
import com.facebook.infer.annotation.Assertions;
+import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.OnBatchCompleteListener;
@@ -19,6 +20,8 @@
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.uimanager.GuardedChoreographerFrameCallback;
import com.facebook.react.uimanager.ReactChoreographer;
import com.facebook.react.uimanager.UIImplementation;
@@ -190,6 +193,36 @@ public void execute(NativeAnimatedNodesManager animatedNodesManager) {
});
}
+ @ReactMethod
+ public void startListeningToAnimatedNodeValue(final int tag) {
+ final AnimatedNodeValueListener listener = new AnimatedNodeValueListener() {
+ public void onValueUpdate(double value) {
+ WritableMap onAnimatedValueData = Arguments.createMap();
+ onAnimatedValueData.putInt("tag", tag);
+ onAnimatedValueData.putDouble("value", value);
+ getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
+ .emit("onAnimatedValueUpdate", onAnimatedValueData);
+ }
+ };
+
+ mOperations.add(new UIThreadOperation() {
+ @Override
+ public void execute(NativeAnimatedNodesManager animatedNodesManager) {
+ animatedNodesManager.startListeningToAnimatedNodeValue(tag, listener);
+ }
+ });
+ }
+
+ @ReactMethod
+ public void stopListeningToAnimatedNodeValue(final int tag) {
+ mOperations.add(new UIThreadOperation() {
+ @Override
+ public void execute(NativeAnimatedNodesManager animatedNodesManager) {
+ animatedNodesManager.stopListeningToAnimatedNodeValue(tag);
+ }
+ });
+ }
+
@ReactMethod
public void dropAnimatedNode(final int tag) {
mOperations.add(new UIThreadOperation() {
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 1aa5e8631274fa..8f5ad6ec81de93 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java
@@ -89,6 +89,24 @@ public void dropAnimatedNode(int tag) {
mAnimatedNodes.remove(tag);
}
+ public void startListeningToAnimatedNodeValue(int tag, AnimatedNodeValueListener listener) {
+ AnimatedNode node = mAnimatedNodes.get(tag);
+ if (node == null || !(node instanceof ValueAnimatedNode)) {
+ throw new JSApplicationIllegalArgumentException("Animated node with tag " + tag +
+ " does not exists or is not a 'value' node");
+ }
+ ((ValueAnimatedNode) node).setValueListener(listener);
+ }
+
+ public void stopListeningToAnimatedNodeValue(int tag) {
+ AnimatedNode node = mAnimatedNodes.get(tag);
+ if (node == null || !(node instanceof ValueAnimatedNode)) {
+ throw new JSApplicationIllegalArgumentException("Animated node with tag " + tag +
+ " does not exists or is not a 'value' node");
+ }
+ ((ValueAnimatedNode) node).setValueListener(null);
+ }
+
public void setAnimatedNodeValue(int tag, double value) {
AnimatedNode node = mAnimatedNodes.get(tag);
if (node == null || !(node instanceof ValueAnimatedNode)) {
@@ -324,6 +342,10 @@ public void runUpdates(long frameTimeNanos) {
// Send property updates to native view manager
((PropsAnimatedNode) nextNode).updateView(mUIImplementation);
}
+ if (nextNode instanceof ValueAnimatedNode) {
+ // Potentially send events to JS when the node's value is updated
+ ((ValueAnimatedNode) nextNode).onValueUpdate();
+ }
if (nextNode.mChildren != null) {
for (int i = 0; i < nextNode.mChildren.size(); i++) {
AnimatedNode child = nextNode.mChildren.get(i);
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/ValueAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/ValueAnimatedNode.java
index 7f573e3063c284..4d8a42d5fcb7b6 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/animated/ValueAnimatedNode.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/animated/ValueAnimatedNode.java
@@ -11,13 +11,15 @@
import com.facebook.react.bridge.ReadableMap;
+import javax.annotation.Nullable;
+
/**
* Basic type of animated node that maps directly from {@code Animated.Value(x)} of Animated.js
* library.
*/
/*package*/ class ValueAnimatedNode extends AnimatedNode {
-
/*package*/ double mValue = Double.NaN;
+ private @Nullable AnimatedNodeValueListener mValueListener;
public ValueAnimatedNode() {
// empty constructor that can be used by subclasses
@@ -26,4 +28,15 @@ public ValueAnimatedNode() {
public ValueAnimatedNode(ReadableMap config) {
mValue = config.getDouble("value");
}
+
+ public void onValueUpdate() {
+ if (mValueListener == null) {
+ return;
+ }
+ mValueListener.onValueUpdate(mValue);
+ }
+
+ public void setValueListener(@Nullable AnimatedNodeValueListener listener) {
+ mValueListener = listener;
+ }
}
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 5f1a63f2730046..e321a0b6e8073a 100644
--- a/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java
+++ b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java
@@ -140,6 +140,64 @@ public void testFramesAnimation() {
verifyNoMoreInteractions(mUIImplementationMock);
}
+ @Test
+ public void testNodeValueListenerIfNotListening() {
+ int nodeId = 1;
+
+ createSimpleAnimatedViewWithOpacity(1000, 0d);
+ JavaOnlyArray frames = JavaOnlyArray.of(0d, 0.2d, 0.4d, 0.6d, 0.8d, 1d);
+
+ Callback animationCallback = mock(Callback.class);
+ AnimatedNodeValueListener valueListener = mock(AnimatedNodeValueListener.class);
+
+ mNativeAnimatedNodesManager.startListeningToAnimatedNodeValue(nodeId, valueListener);
+ mNativeAnimatedNodesManager.startAnimatingNode(
+ 1,
+ nodeId,
+ JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 1d),
+ animationCallback);
+
+ mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
+ verify(valueListener).onValueUpdate(eq(0d));
+
+ mNativeAnimatedNodesManager.stopListeningToAnimatedNodeValue(nodeId);
+
+ reset(valueListener);
+ mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
+ verifyNoMoreInteractions(valueListener);
+ }
+
+ @Test
+ public void testNodeValueListenerIfListening() {
+ int nodeId = 1;
+
+ createSimpleAnimatedViewWithOpacity(1000, 0d);
+ JavaOnlyArray frames = JavaOnlyArray.of(0d, 0.2d, 0.4d, 0.6d, 0.8d, 1d);
+
+ Callback animationCallback = mock(Callback.class);
+ AnimatedNodeValueListener valueListener = mock(AnimatedNodeValueListener.class);
+
+ mNativeAnimatedNodesManager.startListeningToAnimatedNodeValue(nodeId, valueListener);
+ mNativeAnimatedNodesManager.startAnimatingNode(
+ 1,
+ nodeId,
+ JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 1d),
+ animationCallback);
+
+ mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
+ verify(valueListener).onValueUpdate(eq(0d));
+
+ for (int i = 0; i < frames.size(); i++) {
+ reset(valueListener);
+ mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
+ verify(valueListener).onValueUpdate(eq(frames.getDouble(i)));
+ }
+
+ reset(valueListener);
+ mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
+ verifyNoMoreInteractions(valueListener);
+ }
+
@Test
public void testAnimationCallbackFinish() {
createSimpleAnimatedViewWithOpacity(1000, 0d);