Skip to content

Commit 158d435

Browse files
janicduplessisFacebook Github Bot 7
authored andcommitted
Implement native Animated value listeners on Android
Summary: Adds support for `Animated.Value#addListener` for native driven nodes on Android. This is based on work by skevy in the exponent RN fork. Also adds a UIExplorer example. ** Test plan ** Run unit tests Tested that by adding a listener to a native driven animated node and checked that the listener callback is called properly. Also tested that it doesn't crash on iOS that doesn't support this yet. Closes #8844 Differential Revision: D3670906 fbshipit-source-id: 15700ed7b93db140d907ce80af4dae6be3102135
1 parent 30677e7 commit 158d435

File tree

9 files changed

+266
-15
lines changed

9 files changed

+266
-15
lines changed

Examples/UIExplorer/js/NativeAnimationsExample.js

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,15 @@
2222
*/
2323
'use strict';
2424

25-
var React = require('react');
26-
var ReactNative = require('react-native');
27-
var {
25+
const React = require('react');
26+
const ReactNative = require('react-native');
27+
const {
2828
View,
2929
Text,
3030
Animated,
3131
StyleSheet,
3232
TouchableWithoutFeedback,
3333
} = ReactNative;
34-
var UIExplorerButton = require('./UIExplorerButton');
3534

3635
class Tester extends React.Component {
3736
state = {
@@ -47,12 +46,8 @@ class Tester extends React.Component {
4746
...this.props.config,
4847
toValue: this.current,
4948
};
50-
try {
51-
Animated[this.props.type](this.state.native, { ...config, useNativeDriver: true }).start();
52-
} catch (e) {
53-
// uncomment this if you want to get the redbox errors!
54-
throw e;
55-
}
49+
50+
Animated[this.props.type](this.state.native, { ...config, useNativeDriver: true }).start();
5651
Animated[this.props.type](this.state.js, { ...config, useNativeDriver: false }).start();
5752
};
5853

@@ -78,6 +73,52 @@ class Tester extends React.Component {
7873
}
7974
}
8075

76+
class ValueListenerExample extends React.Component {
77+
state = {
78+
anim: new Animated.Value(0),
79+
progress: 0,
80+
};
81+
_current = 0;
82+
83+
componentDidMount() {
84+
this.state.anim.addListener((e) => this.setState({ progress: e.value }));
85+
}
86+
87+
componentWillUnmount() {
88+
this.state.anim.removeAllListeners();
89+
}
90+
91+
_onPress = () => {
92+
this._current = this._current ? 0 : 1;
93+
const config = {
94+
duration: 1000,
95+
toValue: this._current,
96+
};
97+
98+
Animated.timing(this.state.anim, { ...config, useNativeDriver: true }).start();
99+
};
100+
101+
render() {
102+
return (
103+
<TouchableWithoutFeedback onPress={this._onPress}>
104+
<View>
105+
<View style={styles.row}>
106+
<Animated.View
107+
style={[
108+
styles.block,
109+
{
110+
opacity: this.state.anim,
111+
}
112+
]}
113+
/>
114+
</View>
115+
<Text>Value: {this.state.progress}</Text>
116+
</View>
117+
</TouchableWithoutFeedback>
118+
);
119+
}
120+
}
121+
81122
const styles = StyleSheet.create({
82123
row: {
83124
padding: 10,
@@ -304,4 +345,13 @@ exports.examples = [
304345
);
305346
},
306347
},
348+
{
349+
title: 'Animated value listener',
350+
platform: 'android',
351+
render: function() {
352+
return (
353+
<ValueListenerExample />
354+
);
355+
},
356+
},
307357
];

Libraries/Animated/src/AnimatedImplementation.js

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
*/
1212
'use strict';
1313

14+
var DeviceEventEmitter = require('RCTDeviceEventEmitter');
1415
var InteractionManager = require('InteractionManager');
1516
var Interpolation = require('Interpolation');
1617
var React = require('React');
@@ -634,6 +635,7 @@ class AnimatedValue extends AnimatedWithChildren {
634635
_animation: ?Animation;
635636
_tracking: ?Animated;
636637
_listeners: {[key: string]: ValueListenerCallback};
638+
__nativeAnimatedValueListener: ?any;
637639

638640
constructor(value: number) {
639641
super();
@@ -652,6 +654,14 @@ class AnimatedValue extends AnimatedWithChildren {
652654
return this._value + this._offset;
653655
}
654656

657+
__makeNative() {
658+
super.__makeNative();
659+
660+
if (Object.keys(this._listeners).length) {
661+
this._startListeningToNativeValueUpdates();
662+
}
663+
}
664+
655665
/**
656666
* Directly set the value. This will stop any animations running on the value
657667
* and update all the bound properties.
@@ -693,15 +703,49 @@ class AnimatedValue extends AnimatedWithChildren {
693703
addListener(callback: ValueListenerCallback): string {
694704
var id = String(_uniqueId++);
695705
this._listeners[id] = callback;
706+
if (this.__isNative) {
707+
this._startListeningToNativeValueUpdates();
708+
}
696709
return id;
697710
}
698711

699712
removeListener(id: string): void {
700713
delete this._listeners[id];
714+
if (this.__isNative && Object.keys(this._listeners).length === 0) {
715+
this._stopListeningForNativeValueUpdates();
716+
}
701717
}
702718

703719
removeAllListeners(): void {
704720
this._listeners = {};
721+
if (this.__isNative) {
722+
this._stopListeningForNativeValueUpdates();
723+
}
724+
}
725+
726+
_startListeningToNativeValueUpdates() {
727+
if (this.__nativeAnimatedValueListener ||
728+
!NativeAnimatedHelper.supportsNativeListener()) {
729+
return;
730+
}
731+
732+
NativeAnimatedAPI.startListeningToAnimatedNodeValue(this.__getNativeTag());
733+
this.__nativeAnimatedValueListener = DeviceEventEmitter.addListener('onAnimatedValueUpdate', (data) => {
734+
if (data.tag !== this.__getNativeTag()) {
735+
return;
736+
}
737+
this._updateValue(data.value, false /* flush */);
738+
});
739+
}
740+
741+
_stopListeningForNativeValueUpdates() {
742+
if (!this.__nativeAnimatedValueListener ||
743+
!NativeAnimatedHelper.supportsNativeListener()) {
744+
return;
745+
}
746+
747+
this.__nativeAnimatedValueListener.remove();
748+
NativeAnimatedAPI.stopListeningToAnimatedNodeValue(this.__getNativeTag());
705749
}
706750

707751
/**
@@ -1204,7 +1248,7 @@ class AnimatedStyle extends AnimatedWithChildren {
12041248
if (value instanceof Animated) {
12051249
if (!value.__isNative) {
12061250
// We cannot use value of natively driven nodes this way as the value we have access from JS
1207-
// may not be up to date
1251+
// may not be up to date.
12081252
style[key] = value.__getValue();
12091253
}
12101254
} else {
@@ -1296,9 +1340,9 @@ class AnimatedProps extends Animated {
12961340
for (var key in this._props) {
12971341
var value = this._props[key];
12981342
if (value instanceof Animated) {
1299-
if (!value.__isNative) {
1343+
if (!value.__isNative || value instanceof AnimatedStyle) {
13001344
// We cannot use value of natively driven nodes this way as the value we have access from JS
1301-
// may not be up to date
1345+
// may not be up to date.
13021346
props[key] = value.__getValue();
13031347
}
13041348
} else {

Libraries/Animated/src/NativeAnimatedHelper.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ var API = {
3030
assertNativeAnimatedModule();
3131
NativeAnimatedModule.createAnimatedNode(tag, config);
3232
},
33+
startListeningToAnimatedNodeValue: function(tag: number) {
34+
assertNativeAnimatedModule();
35+
NativeAnimatedModule.startListeningToAnimatedNodeValue(tag);
36+
},
37+
stopListeningToAnimatedNodeValue: function(tag: number) {
38+
assertNativeAnimatedModule();
39+
NativeAnimatedModule.stopListeningToAnimatedNodeValue(tag);
40+
},
3341
connectAnimatedNodes: function(parentTag: number, childTag: number): void {
3442
assertNativeAnimatedModule();
3543
NativeAnimatedModule.connectAnimatedNodes(parentTag, childTag);
@@ -144,6 +152,11 @@ function assertNativeAnimatedModule(): void {
144152
invariant(NativeAnimatedModule, 'Native animated module is not available');
145153
}
146154

155+
// TODO: remove this when iOS supports native listeners.
156+
function supportsNativeListener(): bool {
157+
return !!NativeAnimatedModule.startListeningToAnimatedNodeValue;
158+
}
159+
147160
module.exports = {
148161
API,
149162
validateProps,
@@ -153,4 +166,5 @@ module.exports = {
153166
generateNewNodeTag,
154167
generateNewAnimationId,
155168
assertNativeAnimatedModule,
169+
supportsNativeListener,
156170
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
package com.facebook.react.animated;
11+
12+
/**
13+
* Interface used to listen to {@link ValueAnimatedNode} updates.
14+
*/
15+
public interface AnimatedNodeValueListener {
16+
void onValueUpdate(double value);
17+
}

ReactAndroid/src/main/java/com/facebook/react/animated/BUCK

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ android_library(
77
]),
88
deps = [
99
react_native_target('java/com/facebook/react/bridge:bridge'),
10+
react_native_target('java/com/facebook/react/modules/core:core'),
1011
react_native_target('java/com/facebook/react/uimanager:uimanager'),
11-
1212
react_native_target('java/com/facebook/react/uimanager/annotations:annotations'),
1313
react_native_dep('third-party/java/infer-annotations:infer-annotations'),
1414
react_native_dep('third-party/java/jsr-305:jsr-305'),

ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@
1212
import javax.annotation.Nullable;
1313

1414
import com.facebook.infer.annotation.Assertions;
15+
import com.facebook.react.bridge.Arguments;
1516
import com.facebook.react.bridge.Callback;
1617
import com.facebook.react.bridge.LifecycleEventListener;
1718
import com.facebook.react.bridge.OnBatchCompleteListener;
1819
import com.facebook.react.bridge.ReactApplicationContext;
1920
import com.facebook.react.bridge.ReactContextBaseJavaModule;
2021
import com.facebook.react.bridge.ReactMethod;
2122
import com.facebook.react.bridge.ReadableMap;
23+
import com.facebook.react.bridge.WritableMap;
24+
import com.facebook.react.modules.core.DeviceEventManagerModule;
2225
import com.facebook.react.uimanager.GuardedChoreographerFrameCallback;
2326
import com.facebook.react.uimanager.ReactChoreographer;
2427
import com.facebook.react.uimanager.UIImplementation;
@@ -190,6 +193,36 @@ public void execute(NativeAnimatedNodesManager animatedNodesManager) {
190193
});
191194
}
192195

196+
@ReactMethod
197+
public void startListeningToAnimatedNodeValue(final int tag) {
198+
final AnimatedNodeValueListener listener = new AnimatedNodeValueListener() {
199+
public void onValueUpdate(double value) {
200+
WritableMap onAnimatedValueData = Arguments.createMap();
201+
onAnimatedValueData.putInt("tag", tag);
202+
onAnimatedValueData.putDouble("value", value);
203+
getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
204+
.emit("onAnimatedValueUpdate", onAnimatedValueData);
205+
}
206+
};
207+
208+
mOperations.add(new UIThreadOperation() {
209+
@Override
210+
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
211+
animatedNodesManager.startListeningToAnimatedNodeValue(tag, listener);
212+
}
213+
});
214+
}
215+
216+
@ReactMethod
217+
public void stopListeningToAnimatedNodeValue(final int tag) {
218+
mOperations.add(new UIThreadOperation() {
219+
@Override
220+
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
221+
animatedNodesManager.stopListeningToAnimatedNodeValue(tag);
222+
}
223+
});
224+
}
225+
193226
@ReactMethod
194227
public void dropAnimatedNode(final int tag) {
195228
mOperations.add(new UIThreadOperation() {

ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,24 @@ public void dropAnimatedNode(int tag) {
8989
mAnimatedNodes.remove(tag);
9090
}
9191

92+
public void startListeningToAnimatedNodeValue(int tag, AnimatedNodeValueListener listener) {
93+
AnimatedNode node = mAnimatedNodes.get(tag);
94+
if (node == null || !(node instanceof ValueAnimatedNode)) {
95+
throw new JSApplicationIllegalArgumentException("Animated node with tag " + tag +
96+
" does not exists or is not a 'value' node");
97+
}
98+
((ValueAnimatedNode) node).setValueListener(listener);
99+
}
100+
101+
public void stopListeningToAnimatedNodeValue(int tag) {
102+
AnimatedNode node = mAnimatedNodes.get(tag);
103+
if (node == null || !(node instanceof ValueAnimatedNode)) {
104+
throw new JSApplicationIllegalArgumentException("Animated node with tag " + tag +
105+
" does not exists or is not a 'value' node");
106+
}
107+
((ValueAnimatedNode) node).setValueListener(null);
108+
}
109+
92110
public void setAnimatedNodeValue(int tag, double value) {
93111
AnimatedNode node = mAnimatedNodes.get(tag);
94112
if (node == null || !(node instanceof ValueAnimatedNode)) {
@@ -324,6 +342,10 @@ public void runUpdates(long frameTimeNanos) {
324342
// Send property updates to native view manager
325343
((PropsAnimatedNode) nextNode).updateView(mUIImplementation);
326344
}
345+
if (nextNode instanceof ValueAnimatedNode) {
346+
// Potentially send events to JS when the node's value is updated
347+
((ValueAnimatedNode) nextNode).onValueUpdate();
348+
}
327349
if (nextNode.mChildren != null) {
328350
for (int i = 0; i < nextNode.mChildren.size(); i++) {
329351
AnimatedNode child = nextNode.mChildren.get(i);

ReactAndroid/src/main/java/com/facebook/react/animated/ValueAnimatedNode.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111

1212
import com.facebook.react.bridge.ReadableMap;
1313

14+
import javax.annotation.Nullable;
15+
1416
/**
1517
* Basic type of animated node that maps directly from {@code Animated.Value(x)} of Animated.js
1618
* library.
1719
*/
1820
/*package*/ class ValueAnimatedNode extends AnimatedNode {
19-
2021
/*package*/ double mValue = Double.NaN;
22+
private @Nullable AnimatedNodeValueListener mValueListener;
2123

2224
public ValueAnimatedNode() {
2325
// empty constructor that can be used by subclasses
@@ -26,4 +28,15 @@ public ValueAnimatedNode() {
2628
public ValueAnimatedNode(ReadableMap config) {
2729
mValue = config.getDouble("value");
2830
}
31+
32+
public void onValueUpdate() {
33+
if (mValueListener == null) {
34+
return;
35+
}
36+
mValueListener.onValueUpdate(mValue);
37+
}
38+
39+
public void setValueListener(@Nullable AnimatedNodeValueListener listener) {
40+
mValueListener = listener;
41+
}
2942
}

0 commit comments

Comments
 (0)