Skip to content

Commit

Permalink
[NativeAnimated][Android] Add support for animated events
Browse files Browse the repository at this point in the history
  • Loading branch information
janicduplessis committed Aug 5, 2016
1 parent b05c7f7 commit 77953a3
Show file tree
Hide file tree
Showing 11 changed files with 402 additions and 20 deletions.
93 changes: 93 additions & 0 deletions Examples/UIExplorer/js/NativeAnimatedEventExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Copyright (c) 2013-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.
*
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* @flow
*/
'use strict';

var React = require('react');
var ReactNative = require('react-native');
var {
View,
Text,
Animated,
StyleSheet,
TouchableWithoutFeedback,
} = ReactNative;

const styles = StyleSheet.create({
row: {
padding: 10,
zIndex: 1,
},
block: {
width: 50,
height: 50,
backgroundColor: 'blue',
},
});

class EventExample extends React.Component {
static title = '<ScrollView>';
static description = 'Component that enables scrolling through child components.';
state = {
scrollY: new Animated.Value(0),
};

componentDidMount() {
setInterval(() => {
const start = Date.now();
console.warn('lagStart');
setTimeout(() => {
while(Date.now() - start < 2500) {}
console.warn('lagStop');
}, 1);
}, 5000);
}

render() {
const testOpacity = this.state.scrollY.interpolate({
inputRange: [0, 300],
outputRange: [1, 0.5],
});
return (
<Animated.ScrollView
style={{ flex: 1, backgroundColor: 'blue' }}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.scrollY } } }],
{ useNativeDriver: true }
)}
>
<View style={{ height: 5000 }}>
<Animated.View
style={{
opacity: testOpacity,
width: 400,
height: 400,
backgroundColor: 'red',
}}
/>
</View>
</Animated.ScrollView>
);
}
}

module.exports = EventExample;
4 changes: 4 additions & 0 deletions Examples/UIExplorer/js/UIExplorerList.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export type UIExplorerExample = {
};

var ComponentExamples: Array<UIExplorerExample> = [
{
key: 'NativeAnimatedEvent',
module: require('./NativeAnimatedEventExample'),
},
{
key: 'ActivityIndicatorExample',
module: require('./ActivityIndicatorExample'),
Expand Down
2 changes: 2 additions & 0 deletions Libraries/Animated/src/Animated.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
152 changes: 135 additions & 17 deletions Libraries/Animated/src/AnimatedImplementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -1345,6 +1345,10 @@ class AnimatedProps extends Animated {
// may not be up to date.
props[key] = value.__getValue();
}
} else if (value instanceof AnimatedEvent) {
if (!value.__isNative) {
props[key] = value.__getHandler();
}
} else {
props[key] = value;
}
Expand Down Expand Up @@ -1462,6 +1466,30 @@ function createAnimatedComponent(Component: any): any {

componentDidMount() {
this._propsAnimated.setNativeView(this.refs[refName]);

this.attachNativeEvents(this.props);
}

attachNativeEvents(nextProps) {
// TODO: Make sure we remove events properly.

// Make sure to get the scrollable node for components that implement
// `ScrollResponder.Mixin`.
const ref = this.refs[refName].getScrollableNode ?
this.refs[refName].getScrollableNode() :
this.refs[refName];

for (let key in nextProps) {
const prop = nextProps[key];
if (prop instanceof AnimatedEvent && prop.__isNative) {
// TODO: Map event names using the map in UIManager.
if (key === 'onScroll') {
key = 'topScroll';
}

prop.__attach(ref, key);
}
}
}

attachProps(nextProps) {
Expand Down Expand Up @@ -1494,7 +1522,6 @@ function createAnimatedComponent(Component: any): any {
callback,
);


if (this.refs && this.refs[refName]) {
this._propsAnimated.setNativeView(this.refs[refName]);
}
Expand Down Expand Up @@ -1538,7 +1565,7 @@ function createAnimatedComponent(Component: any): any {
);
}
}
}
},
};

return AnimatedComponent;
Expand Down Expand Up @@ -1821,21 +1848,106 @@ 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 && config.listener;
this.__isNative = (config && config.useNativeDriver) || false;

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: Array<any>) => {
if (value instanceof AnimatedValue) {
value.__makeNative();

invariant(
path.indexOf('nativeEvent') >= 0,
'Native driven events only support animated values contained inside `nativeEvent`.'
);
const nativeEventPath = path.slice(path.indexOf('nativeEvent') + 1);

eventMappings.push({
nativeEventPath,
animatedValueTag: value.__getNativeTag(),
});
return;
}

for (const key in value) {
traverse(value[key], [...path, key]);
}
};

this._argMapping.forEach((arg, i) => traverse(arg, [i]));

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) => {
var traverse = (recMapping, recEvt, key) => {
if (typeof recEvt === 'number' && recMapping instanceof AnimatedValue) {
recMapping.setValue(recEvt);
return;
}

if (typeof recMapping !== 'object') {
return;
}

for (var key in recMapping) {
traverse(recMapping[key], recEvt[key], key);
}
};
this._argMapping.forEach((mapping, idx) => {
traverse(mapping, args[idx], 'arg' + idx);
});
if (this._listener) {
this._listener.apply(null, args);
}
};
}

_validateMapping() {
var 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(
Expand All @@ -1850,13 +1962,19 @@ var event = function(
traverse(recMapping[key], recEvt[key], key);
}
};
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();
}
};

/**
Expand Down
12 changes: 12 additions & 0 deletions Libraries/Animated/src/NativeAnimatedHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ var __nativeAnimationIdCount = 1; /* used for started animations */

type EndResult = {finished: bool};
type EndCallback = (result: EndResult) => void;
type EventMapping = {
nativeEventPath: Array<string>;
animatedValueTag: number;
};

/**
* Simple wrappers around NativeANimatedModule to provide flow and autocmplete support for
Expand Down Expand Up @@ -70,6 +74,14 @@ var 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);
}
};

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* 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;

import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.RCTEventEmitter;

import java.util.List;

import javax.annotation.Nullable;

/**
* Handles updating a {@link ValueAnimatedNode} when an event gets dispatched.
*/
/* package */ class EventAnimationDriver implements RCTEventEmitter {
private List<String> mEventPath;
/* package */ ValueAnimatedNode mValueNode;

public EventAnimationDriver(List<String> eventPath, ValueAnimatedNode valueNode) {
mEventPath = eventPath;
mValueNode = valueNode;
}

@Override
public void receiveEvent(int targetTag, String eventName, @Nullable WritableMap event) {
if (event == null) {
throw new IllegalArgumentException("Native animated events must have event data.");
}

// Get the new value for the node by looking into the event map using the provided event path.
ReadableMap curMap = event;
for (int i = 0; i < mEventPath.size() - 1; i++) {
curMap = curMap.getMap(mEventPath.get(i));
}

mValueNode.mValue = curMap.getDouble(mEventPath.get(mEventPath.size() - 1));
}

@Override
public void receiveTouches(String eventName, WritableArray touches, WritableArray changedIndices) {
throw new RuntimeException("receiveTouches is not support by native animated events");
}
}
Loading

0 comments on commit 77953a3

Please sign in to comment.