Permalink
Browse files

Add support for animated events

Summary:
This adds support for `Animated.event` driven natively. This is WIP and would like feedback on how this is implemented.

At the moment, it works by providing a mapping between a view tag, an event name, an event path and an animated value when a view has a prop with a `AnimatedEvent` object. Then we can hook into `EventDispatcher`, check for events that target our view + event name and update the animated value using the event path.

For now it works with the onScroll event but it should be generic enough to work with anything.
Closes #9253

Differential Revision: D3759844

Pulled By: foghina

fbshipit-source-id: 86989c705847955bd65e6cf5a7d572ec7ccd3eb4
  • Loading branch information...
1 parent 19d0429 commit 65659293589848bf48ecefe1f89afb4b562c7022 @janicduplessis janicduplessis committed with Facebook Github Bot 9 Sep 19, 2016
@@ -168,6 +168,46 @@ class InternalSettings extends React.Component {
}
}
+class EventExample extends React.Component {
+ state = {
+ scrollX: new Animated.Value(0),
+ };
+
+ render() {
+ const opacity = this.state.scrollX.interpolate({
+ inputRange: [0, 200],
+ outputRange: [1, 0],
+ });
+ return (
+ <View>
+ <Animated.View
+ style={[
+ styles.block,
+ {
+ opacity,
+ }
+ ]}
+ />
+ <Animated.ScrollView
+ horizontal
+ style={{ height: 100, marginTop: 16 }}
+ onScroll={
+ Animated.event([{
+ nativeEvent: { contentOffset: { x: this.state.scrollX } }
+ }], {
+ useNativeDriver: true,
+ })
+ }
+ >
+ <View style={{ width: 600, backgroundColor: '#eee', justifyContent: 'center' }}>
+ <Text>Scroll me!</Text>
+ </View>
+ </Animated.ScrollView>
+ </View>
+ );
+ }
+}
+
const styles = StyleSheet.create({
row: {
padding: 10,
@@ -429,4 +469,13 @@ exports.examples = [
);
},
},
+ {
+ title: 'Animated events',
+ platform: 'android',
+ render: function() {
+ return (
+ <EventExample />
+ );
+ },
+ },
];
@@ -15,10 +15,12 @@ var AnimatedImplementation = require('AnimatedImplementation');
var Image = require('Image');
var Text = require('Text');
var View = require('View');
+var ScrollView = require('ScrollView');
module.exports = {
...AnimatedImplementation,
View: AnimatedImplementation.createAnimatedComponent(View),
Text: AnimatedImplementation.createAnimatedComponent(Text),
Image: AnimatedImplementation.createAnimatedComponent(Image),
+ ScrollView: AnimatedImplementation.createAnimatedComponent(ScrollView),
};
@@ -1486,6 +1486,8 @@ class AnimatedProps extends Animated {
// JS may not be up to date.
props[key] = value.__getValue();
}
+ } else if (value instanceof AnimatedEvent) {
+ props[key] = value.__getHandler();
} else {
props[key] = value;
}
@@ -1596,21 +1598,58 @@ function createAnimatedComponent(Component: any): any {
componentWillUnmount() {
this._propsAnimated && this._propsAnimated.__detach();
+ this._detachNativeEvents(this.props);
}
setNativeProps(props) {
this._component.setNativeProps(props);
}
componentWillMount() {
- this.attachProps(this.props);
+ this._attachProps(this.props);
}
componentDidMount() {
this._propsAnimated.setNativeView(this._component);
+
+ this._attachNativeEvents(this.props);
+ }
+
+ _attachNativeEvents(newProps) {
+ if (newProps !== this.props) {
+ this._detachNativeEvents(this.props);
+ }
+
+ // Make sure to get the scrollable node for components that implement
+ // `ScrollResponder.Mixin`.
+ const ref = this._component.getScrollableNode ?
+ this._component.getScrollableNode() :
+ this._component;
+
+ for (const key in newProps) {
+ const prop = newProps[key];
+ if (prop instanceof AnimatedEvent && prop.__isNative) {
+ prop.__attach(ref, key);
+ }
+ }
+ }
+
+ _detachNativeEvents(props) {
+ // Make sure to get the scrollable node for components that implement
+ // `ScrollResponder.Mixin`.
+ const ref = this._component.getScrollableNode ?
+ this._component.getScrollableNode() :
+ this._component;
+
+ for (const key in props) {
+ const prop = props[key];
+ if (prop instanceof AnimatedEvent && prop.__isNative) {
+ prop.__detach(ref, key);
+ }
+ }
}
- attachProps(nextProps) {
+ _attachProps(nextProps) {
var oldPropsAnimated = this._propsAnimated;
// The system is best designed when setNativeProps is implemented. It is
@@ -1640,7 +1679,6 @@ function createAnimatedComponent(Component: any): any {
callback,
);
-
if (this._component) {
this._propsAnimated.setNativeView(this._component);
}
@@ -1657,7 +1695,8 @@ function createAnimatedComponent(Component: any): any {
}
componentWillReceiveProps(nextProps) {
- this.attachProps(nextProps);
+ this._attachProps(nextProps);
+ this._attachNativeEvents(nextProps);
}
render() {
@@ -1694,7 +1733,7 @@ function createAnimatedComponent(Component: any): any {
);
}
}
- }
+ },
};
return AnimatedComponent;
@@ -1998,21 +2037,108 @@ var stagger = function(
};
type Mapping = {[key: string]: Mapping} | AnimatedValue;
+type EventConfig = {
+ listener?: ?Function;
+ useNativeDriver?: bool;
+};
-type EventConfig = {listener?: ?Function};
-var event = function(
- argMapping: Array<?Mapping>,
- config?: ?EventConfig,
-): () => void {
- return function(...args): void {
- var traverse = function(recMapping, recEvt, key) {
+class AnimatedEvent {
+ _argMapping: Array<?Mapping>;
+ _listener: ?Function;
+ __isNative: bool;
+
+ constructor(
+ argMapping: Array<?Mapping>,
+ config?: EventConfig = {}
+ ) {
+ this._argMapping = argMapping;
+ this._listener = config.listener;
+ this.__isNative = config.useNativeDriver || false;
+
+ if (this.__isNative) {
+ invariant(!this._listener, 'Listener is not supported for native driven events.');
+ }
+
+ if (__DEV__) {
+ this._validateMapping();
+ }
+ }
+
+ __attach(viewRef, eventName) {
+ invariant(this.__isNative, 'Only native driven events need to be attached.');
+
+ // Find animated values in `argMapping` and create an array representing their
+ // key path inside the `nativeEvent` object. Ex.: ['contentOffset', 'x'].
+ const eventMappings = [];
+
+ const traverse = (value, path) => {
+ if (value instanceof AnimatedValue) {
+ value.__makeNative();
+
+ eventMappings.push({
+ nativeEventPath: path,
+ animatedValueTag: value.__getNativeTag(),
+ });
+ } else if (typeof value === 'object') {
+ for (const key in value) {
+ traverse(value[key], path.concat(key));
+ }
+ }
+ };
+
+ invariant(
+ this._argMapping[0] && this._argMapping[0].nativeEvent,
+ 'Native driven events only support animated values contained inside `nativeEvent`.'
+ );
+
+ // Assume that the event containing `nativeEvent` is always the first argument.
+ traverse(this._argMapping[0].nativeEvent, []);
+
+ const viewTag = findNodeHandle(viewRef);
+
+ eventMappings.forEach((mapping) => {
+ NativeAnimatedAPI.addAnimatedEventToView(viewTag, eventName, mapping);
+ });
+ }
+
+ __detach(viewTag, eventName) {
+ invariant(this.__isNative, 'Only native driven events need to be detached.');
+
+ NativeAnimatedAPI.removeAnimatedEventFromView(viewTag, eventName);
+ }
+
+ __getHandler() {
+ return (...args) => {
+ const traverse = (recMapping, recEvt, key) => {
+ if (typeof recEvt === 'number' && recMapping instanceof AnimatedValue) {
+ recMapping.setValue(recEvt);
+ } else if (typeof recMapping === 'object') {
+ for (const mappingKey in recMapping) {
+ traverse(recMapping[mappingKey], recEvt[mappingKey], mappingKey);
+ }
+ }
+ };
+
+ if (!this.__isNative) {
+ this._argMapping.forEach((mapping, idx) => {
+ traverse(mapping, args[idx], 'arg' + idx);
+ });
+ }
+
+ if (this._listener) {
+ this._listener.apply(null, args);
+ }
+ };
+ }
+
+ _validateMapping() {
+ const traverse = (recMapping, recEvt, key) => {
if (typeof recEvt === 'number') {
invariant(
recMapping instanceof AnimatedValue,
'Bad mapping of type ' + typeof recMapping + ' for key ' + key +
', event value must map to AnimatedValue'
);
- recMapping.setValue(recEvt);
return;
}
invariant(
@@ -2023,17 +2149,23 @@ var event = function(
typeof recEvt === 'object',
'Bad event of type ' + typeof recEvt + ' for key ' + key
);
- for (var key in recMapping) {
- traverse(recMapping[key], recEvt[key], key);
+ for (const mappingKey in recMapping) {
+ traverse(recMapping[mappingKey], recEvt[mappingKey], mappingKey);
}
};
- argMapping.forEach((mapping, idx) => {
- traverse(mapping, args[idx], 'arg' + idx);
- });
- if (config && config.listener) {
- config.listener.apply(null, args);
- }
- };
+ }
+}
+
+var event = function(
+ argMapping: Array<?Mapping>,
+ config?: EventConfig,
+): any {
+ const animatedEvent = new AnimatedEvent(argMapping, config);
+ if (animatedEvent.__isNative) {
+ return animatedEvent;
+ } else {
+ return animatedEvent.__getHandler();
+ }
};
/**
@@ -21,6 +21,10 @@ let __nativeAnimationIdCount = 1; /* used for started animations */
type EndResult = {finished: bool};
type EndCallback = (result: EndResult) => void;
+type EventMapping = {
+ nativeEventPath: Array<string>;
+ animatedValueTag: number;
+};
let nativeEventEmitter;
@@ -73,6 +77,14 @@ const API = {
assertNativeAnimatedModule();
NativeAnimatedModule.dropAnimatedNode(tag);
},
+ addAnimatedEventToView: function(viewTag: number, eventName: string, eventMapping: EventMapping) {
+ assertNativeAnimatedModule();
+ NativeAnimatedModule.addAnimatedEventToView(viewTag, eventName, eventMapping);
+ },
+ removeAnimatedEventFromView(viewTag: number, eventName: string) {
+ assertNativeAnimatedModule();
+ NativeAnimatedModule.removeAnimatedEventFromView(viewTag, eventName);
+ }
};
/**
@@ -13,6 +13,7 @@ jest
.setMock('Text', {})
.setMock('View', {})
.setMock('Image', {})
+ .setMock('ScrollView', {})
.setMock('React', {Component: class {}});
var Animated = require('Animated');
@@ -86,6 +87,7 @@ describe('Animated', () => {
c.componentWillMount();
expect(anim.__detach).not.toBeCalled();
+ c._component = {};
c.componentWillReceiveProps({
style: {
opacity: anim,
@@ -116,7 +118,7 @@ describe('Animated', () => {
c.componentWillMount();
Animated.timing(anim, {toValue: 10, duration: 1000}).start(callback);
-
+ c._component = {};
c.componentWillUnmount();
expect(callback).toBeCalledWith({finished: false});
Oops, something went wrong.

2 comments on commit 6565929

@nikitaMe1nikov

What is necessary to implement this for Gesture Responder System?

@janicduplessis
Member

Quite a lot, the event bubbling login and responder system is all written in JS so it can't be offloaded to native without rewriting all of it. @kmagiera is working on an alternative gesture system that will be able to work with native animations https://github.com/kmagiera/react-native-gesture-handler

Please sign in to comment.