mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-03-29 15:59:39 +08:00
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 https://github.com/facebook/react-native/pull/8844 Differential Revision: D3670906 fbshipit-source-id: 15700ed7b93db140d907ce80af4dae6be3102135
This commit is contained in:
committed by
Facebook Github Bot 7
parent
30677e7193
commit
158d435f36
@@ -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 (
|
||||
<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,
|
||||
@@ -304,4 +345,13 @@ exports.examples = [
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Animated value listener',
|
||||
platform: 'android',
|
||||
render: function() {
|
||||
return (
|
||||
<ValueListenerExample />
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
@@ -12,6 +12,7 @@ package com.facebook.react.animated;
|
||||
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.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;
|
||||
@@ -190,6 +193,36 @@ public class NativeAnimatedModule extends ReactContextBaseJavaModule implements
|
||||
});
|
||||
}
|
||||
|
||||
@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() {
|
||||
|
||||
@@ -89,6 +89,24 @@ import javax.annotation.Nullable;
|
||||
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 @@ import javax.annotation.Nullable;
|
||||
// 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);
|
||||
|
||||
@@ -11,13 +11,15 @@ package com.facebook.react.animated;
|
||||
|
||||
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 @@ import com.facebook.react.bridge.ReadableMap;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +140,64 @@ public class NativeAnimatedNodeTraversalTest {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user