Skip to content

Commit

Permalink
Implement native Animated value listeners on Android
Browse files Browse the repository at this point in the history
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
  • Loading branch information
janicduplessis authored and Facebook Github Bot 7 committed Aug 4, 2016
1 parent 30677e7 commit 158d435
Show file tree
Hide file tree
Showing 9 changed files with 266 additions and 15 deletions.
70 changes: 60 additions & 10 deletions Examples/UIExplorer/js/NativeAnimationsExample.js
Expand Up @@ -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 = {
Expand All @@ -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();
};

Expand All @@ -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 (
<TouchableWithoutFeedback onPress={this._onPress}>
<View>
<View style={styles.row}>
<Animated.View
style={[
styles.block,
{
opacity: this.state.anim,
}
]}
/>
</View>
<Text>Value: {this.state.progress}</Text>
</View>
</TouchableWithoutFeedback>
);
}
}

const styles = StyleSheet.create({
row: {
padding: 10,
Expand Down Expand Up @@ -304,4 +345,13 @@ exports.examples = [
);
},
},
{
title: 'Animated value listener',
platform: 'android',
render: function() {
return (
<ValueListenerExample />
);
},
},
];
50 changes: 47 additions & 3 deletions Libraries/Animated/src/AnimatedImplementation.js
Expand Up @@ -11,6 +11,7 @@
*/
'use strict';

var DeviceEventEmitter = require('RCTDeviceEventEmitter');
var InteractionManager = require('InteractionManager');
var Interpolation = require('Interpolation');
var React = require('React');
Expand Down Expand Up @@ -634,6 +635,7 @@ class AnimatedValue extends AnimatedWithChildren {
_animation: ?Animation;
_tracking: ?Animated;
_listeners: {[key: string]: ValueListenerCallback};
__nativeAnimatedValueListener: ?any;

constructor(value: number) {
super();
Expand All @@ -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.
Expand Down Expand Up @@ -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());
}

/**
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
14 changes: 14 additions & 0 deletions Libraries/Animated/src/NativeAnimatedHelper.js
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -153,4 +166,5 @@ module.exports = {
generateNewNodeTag,
generateNewAnimationId,
assertNativeAnimatedModule,
supportsNativeListener,
};
@@ -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);
}
Expand Up @@ -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'),
Expand Down
Expand Up @@ -12,13 +12,16 @@
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;
import com.facebook.react.bridge.ReactApplicationContext;
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;
Expand Down Expand Up @@ -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() {
Expand Down
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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);
Expand Down
Expand Up @@ -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
Expand All @@ -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;
}
}

0 comments on commit 158d435

Please sign in to comment.