diff --git a/Examples/UIExplorer/js/NativeAnimationsExample.js b/Examples/UIExplorer/js/NativeAnimationsExample.js
index e87a8161fd2..ecafb4751be 100644
--- a/Examples/UIExplorer/js/NativeAnimationsExample.js
+++ b/Examples/UIExplorer/js/NativeAnimationsExample.js
@@ -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 (
+
+
+
+
+ Scroll me!
+
+
+
+ );
+ }
+}
+
const styles = StyleSheet.create({
row: {
padding: 10,
@@ -429,4 +469,13 @@ exports.examples = [
);
},
},
+ {
+ title: 'Animated events',
+ platform: 'android',
+ render: function() {
+ return (
+
+ );
+ },
+ },
];
diff --git a/Libraries/Animated/src/Animated.js b/Libraries/Animated/src/Animated.js
index bce17d3272a..ebb79b51b69 100644
--- a/Libraries/Animated/src/Animated.js
+++ b/Libraries/Animated/src/Animated.js
@@ -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),
};
diff --git a/Libraries/Animated/src/AnimatedImplementation.js b/Libraries/Animated/src/AnimatedImplementation.js
index c9e61113168..ad2f0cfcc1a 100644
--- a/Libraries/Animated/src/AnimatedImplementation.js
+++ b/Libraries/Animated/src/AnimatedImplementation.js
@@ -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,6 +1598,7 @@ function createAnimatedComponent(Component: any): any {
componentWillUnmount() {
this._propsAnimated && this._propsAnimated.__detach();
+ this._detachNativeEvents(this.props);
}
setNativeProps(props) {
@@ -1603,14 +1606,50 @@ function createAnimatedComponent(Component: any): any {
}
componentWillMount() {
- this.attachProps(this.props);
+ this._attachProps(this.props);
}
componentDidMount() {
this._propsAnimated.setNativeView(this._component);
+
+ this._attachNativeEvents(this.props);
}
- attachProps(nextProps) {
+ _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) {
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,
- config?: ?EventConfig,
-): () => void {
- return function(...args): void {
- var traverse = function(recMapping, recEvt, key) {
+class AnimatedEvent {
+ _argMapping: Array;
+ _listener: ?Function;
+ __isNative: bool;
+
+ constructor(
+ argMapping: Array,
+ 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,
+ config?: EventConfig,
+): any {
+ const animatedEvent = new AnimatedEvent(argMapping, config);
+ if (animatedEvent.__isNative) {
+ return animatedEvent;
+ } else {
+ return animatedEvent.__getHandler();
+ }
};
/**
diff --git a/Libraries/Animated/src/NativeAnimatedHelper.js b/Libraries/Animated/src/NativeAnimatedHelper.js
index 117ec61c890..9659d8b7bae 100644
--- a/Libraries/Animated/src/NativeAnimatedHelper.js
+++ b/Libraries/Animated/src/NativeAnimatedHelper.js
@@ -21,6 +21,10 @@ let __nativeAnimationIdCount = 1; /* used for started animations */
type EndResult = {finished: bool};
type EndCallback = (result: EndResult) => void;
+type EventMapping = {
+ nativeEventPath: Array;
+ 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);
+ }
};
/**
diff --git a/Libraries/Animated/src/__tests__/Animated-test.js b/Libraries/Animated/src/__tests__/Animated-test.js
index d57f9b67244..01feba35586 100644
--- a/Libraries/Animated/src/__tests__/Animated-test.js
+++ b/Libraries/Animated/src/__tests__/Animated-test.js
@@ -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});
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/EventAnimationDriver.java b/ReactAndroid/src/main/java/com/facebook/react/animated/EventAnimationDriver.java
new file mode 100644
index 00000000000..20a6a8f8334
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/animated/EventAnimationDriver.java
@@ -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 mEventPath;
+ /* package */ ValueAnimatedNode mValueNode;
+
+ public EventAnimationDriver(List 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");
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java
index 45a39e34394..4ef8f3e3a65 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java
@@ -25,7 +25,6 @@ import com.facebook.react.module.annotations.ReactModule;
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;
import com.facebook.react.uimanager.UIManagerModule;
import java.util.ArrayList;
@@ -95,11 +94,9 @@ public class NativeAnimatedModule extends ReactContextBaseJavaModule implements
mReactChoreographer = ReactChoreographer.getInstance();
ReactApplicationContext reactCtx = getReactApplicationContext();
- UIImplementation uiImplementation =
- reactCtx.getNativeModule(UIManagerModule.class).getUIImplementation();
+ UIManagerModule uiManager = reactCtx.getNativeModule(UIManagerModule.class);
- final NativeAnimatedNodesManager nodesManager =
- new NativeAnimatedNodesManager(uiImplementation);
+ final NativeAnimatedNodesManager nodesManager = new NativeAnimatedNodesManager(uiManager);
mAnimatedFrameCallback = new GuardedChoreographerFrameCallback(reactCtx) {
@Override
protected void doFrameGuarded(final long frameTimeNanos) {
@@ -312,4 +309,24 @@ public class NativeAnimatedModule extends ReactContextBaseJavaModule implements
}
});
}
+
+ @ReactMethod
+ public void addAnimatedEventToView(final int viewTag, final String eventName, final ReadableMap eventMapping) {
+ mOperations.add(new UIThreadOperation() {
+ @Override
+ public void execute(NativeAnimatedNodesManager animatedNodesManager) {
+ animatedNodesManager.addAnimatedEventToView(viewTag, eventName, eventMapping);
+ }
+ });
+ }
+
+ @ReactMethod
+ public void removeAnimatedEventFromView(final int viewTag, final String eventName) {
+ mOperations.add(new UIThreadOperation() {
+ @Override
+ public void execute(NativeAnimatedNodesManager animatedNodesManager) {
+ animatedNodesManager.removeAnimatedEventFromView(viewTag, eventName);
+ }
+ });
+ }
}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java
index f312c88e612..e11db2835fb 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java
@@ -11,16 +11,24 @@ package com.facebook.react.animated;
import android.util.SparseArray;
+import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
+import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.UIImplementation;
+import com.facebook.react.uimanager.UIManagerModule;
+import com.facebook.react.uimanager.events.Event;
+import com.facebook.react.uimanager.events.EventDispatcherListener;
import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
import java.util.Queue;
import javax.annotation.Nullable;
@@ -38,16 +46,21 @@ import javax.annotation.Nullable;
*
* IMPORTANT: This class should be accessed only from the UI Thread
*/
-/*package*/ class NativeAnimatedNodesManager {
+/*package*/ class NativeAnimatedNodesManager implements EventDispatcherListener {
private final SparseArray mAnimatedNodes = new SparseArray<>();
private final ArrayList mActiveAnimations = new ArrayList<>();
private final ArrayList mUpdatedNodes = new ArrayList<>();
+ private final Map mEventDrivers = new HashMap<>();
+ private final Map> mCustomEventTypes;
private final UIImplementation mUIImplementation;
private int mAnimatedGraphBFSColor = 0;
- public NativeAnimatedNodesManager(UIImplementation uiImplementation) {
- mUIImplementation = uiImplementation;
+ public NativeAnimatedNodesManager(UIManagerModule uiManager) {
+ mUIImplementation = uiManager.getUIImplementation();
+ uiManager.getEventDispatcher().addListener(this);
+ Object customEventTypes = Assertions.assertNotNull(uiManager.getConstants()).get("customDirectEventTypes");
+ mCustomEventTypes = (Map>) customEventTypes;
}
/*package*/ @Nullable AnimatedNode getNodeById(int id) {
@@ -238,6 +251,58 @@ import javax.annotation.Nullable;
propsAnimatedNode.mConnectedViewTag = -1;
}
+ public void addAnimatedEventToView(int viewTag, String eventName, ReadableMap eventMapping) {
+ int nodeTag = eventMapping.getInt("animatedValueTag");
+ AnimatedNode node = mAnimatedNodes.get(nodeTag);
+ if (node == null) {
+ throw new JSApplicationIllegalArgumentException("Animated node with tag " + nodeTag +
+ " does not exists");
+ }
+ if (!(node instanceof ValueAnimatedNode)) {
+ throw new JSApplicationIllegalArgumentException("Animated node connected to event should be" +
+ "of type " + ValueAnimatedNode.class.getName());
+ }
+
+ ReadableArray path = eventMapping.getArray("nativeEventPath");
+ List pathList = new ArrayList<>(path.size());
+ for (int i = 0; i < path.size(); i++) {
+ pathList.add(path.getString(i));
+ }
+
+ EventAnimationDriver event = new EventAnimationDriver(pathList, (ValueAnimatedNode) node);
+ mEventDrivers.put(viewTag + eventName, event);
+ }
+
+ public void removeAnimatedEventFromView(int viewTag, String eventName) {
+ mEventDrivers.remove(viewTag + eventName);
+ }
+
+ @Override
+ public boolean onEventDispatch(Event event) {
+ // Only support events dispatched from the UI thread.
+ if (!UiThreadUtil.isOnUiThread()) {
+ return false;
+ }
+
+ if (!mEventDrivers.isEmpty()) {
+ // If the event has a different name in native convert it to it's JS name.
+ String eventName = event.getEventName();
+ Map customEventType = mCustomEventTypes.get(eventName);
+ if (customEventType != null) {
+ eventName = customEventType.get("registrationName");
+ }
+
+ EventAnimationDriver eventDriver = mEventDrivers.get(event.getViewTag() + eventName);
+ if (eventDriver != null) {
+ event.dispatch(eventDriver);
+ mUpdatedNodes.add(eventDriver.mValueNode);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
/**
* Animation loop performs two BFSes over the graph of animated nodes. We use incremented
* {@code mAnimatedGraphBFSColor} to mark nodes as visited in each of the BFSes which saves
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java
index d0c9f09e047..c96e9af5914 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java
@@ -12,10 +12,8 @@ package com.facebook.react.animated;
import com.facebook.react.bridge.JavaOnlyMap;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
-import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.UIImplementation;
-import com.facebook.react.uimanager.UIManagerModule;
import java.util.HashMap;
import java.util.Map;
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java
index 8bf009a9e83..ade36a58ca9 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java
@@ -92,6 +92,7 @@ public class EventDispatcher implements LifecycleEventListener {
private final Map mEventNameToEventId = MapBuilder.newHashMap();
private final DispatchEventsRunnable mDispatchEventsRunnable = new DispatchEventsRunnable();
private final ArrayList mEventStaging = new ArrayList<>();
+ private final ArrayList mListeners = new ArrayList<>();
private Event[] mEventsToDispatch = new Event[16];
private int mEventsToDispatchSize = 0;
@@ -112,6 +113,19 @@ public class EventDispatcher implements LifecycleEventListener {
*/
public void dispatchEvent(Event event) {
Assertions.assertCondition(event.isInitialized(), "Dispatched event hasn't been initialized");
+
+ boolean eventHandled = false;
+ for (EventDispatcherListener listener : mListeners) {
+ if (listener.onEventDispatch(event)) {
+ eventHandled = true;
+ }
+ }
+
+ // If the event was handled by one of the event listener don't send it to JS.
+ if (eventHandled) {
+ return;
+ }
+
synchronized (mEventsStagingLock) {
mEventStaging.add(event);
Systrace.startAsyncFlow(
@@ -131,6 +145,20 @@ public class EventDispatcher implements LifecycleEventListener {
}
}
+ /**
+ * Add a listener to this EventDispatcher.
+ */
+ public void addListener(EventDispatcherListener listener) {
+ mListeners.add(listener);
+ }
+
+ /**
+ * Remove a listener from this EventDispatcher.
+ */
+ public void removeListener(EventDispatcherListener listener) {
+ mListeners.remove(listener);
+ }
+
@Override
public void onHostResume() {
UiThreadUtil.assertOnUiThread();
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcherListener.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcherListener.java
new file mode 100644
index 00000000000..20a9940e9a3
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcherListener.java
@@ -0,0 +1,16 @@
+// Copyright 2004-present Facebook. All Rights Reserved.
+
+package com.facebook.react.uimanager.events;
+
+/**
+ * Interface used to intercept events dispatched by {#link EventDispatcher}
+ */
+public interface EventDispatcherListener {
+ /**
+ * Called on every time an event is dispatched using {#link EventDispatcher#dispatchEvent}. Will be
+ * called from the same thread that the event is being dispatched from.
+ * @param event Event that was dispatched
+ * @return If the event was handled. If true the event won't be sent to JS.
+ */
+ boolean onEventDispatch(Event event);
+}
diff --git a/ReactAndroid/src/test/java/com/facebook/react/animated/BUCK b/ReactAndroid/src/test/java/com/facebook/react/animated/BUCK
index b6f4ea8d022..c218e19f0bb 100644
--- a/ReactAndroid/src/test/java/com/facebook/react/animated/BUCK
+++ b/ReactAndroid/src/test/java/com/facebook/react/animated/BUCK
@@ -9,6 +9,7 @@ robolectric3_test(
react_native_target('java/com/facebook/react/animated:animated'),
react_native_target('java/com/facebook/react/bridge:bridge'),
react_native_target('java/com/facebook/react/uimanager:uimanager'),
+ react_native_target('java/com/facebook/react/common:common'),
react_native_target('java/com/facebook/react:react'),
react_native_tests_target('java/com/facebook/react/bridge:testhelpers'),
diff --git a/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java
index db779ea29ee..9f4db9d6fd1 100644
--- a/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java
+++ b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java
@@ -14,8 +14,13 @@ import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.JavaOnlyArray;
import com.facebook.react.bridge.JavaOnlyMap;
import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.UIImplementation;
+import com.facebook.react.uimanager.UIManagerModule;
+import com.facebook.react.uimanager.events.Event;
+import com.facebook.react.uimanager.events.EventDispatcher;
+import com.facebook.react.uimanager.events.RCTEventEmitter;
import org.junit.Before;
import org.junit.Rule;
@@ -56,7 +61,9 @@ public class NativeAnimatedNodeTraversalTest {
public PowerMockRule rule = new PowerMockRule();
private long mFrameTimeNanos;
+ private UIManagerModule mUIManagerMock;
private UIImplementation mUIImplementationMock;
+ private EventDispatcher mEventDispatcherMock;
private NativeAnimatedNodesManager mNativeAnimatedNodesManager;
private long nextFrameTime() {
@@ -80,8 +87,28 @@ public class NativeAnimatedNodeTraversalTest {
});
mFrameTimeNanos = INITIAL_FRAME_TIME_NANOS;
+ mUIManagerMock = mock(UIManagerModule.class);
mUIImplementationMock = mock(UIImplementation.class);
- mNativeAnimatedNodesManager = new NativeAnimatedNodesManager(mUIImplementationMock);
+ mEventDispatcherMock = mock(EventDispatcher.class);
+ PowerMockito.when(mUIManagerMock.getUIImplementation()).thenAnswer(new Answer() {
+ @Override
+ public UIImplementation answer(InvocationOnMock invocation) throws Throwable {
+ return mUIImplementationMock;
+ }
+ });
+ PowerMockito.when(mUIManagerMock.getEventDispatcher()).thenAnswer(new Answer() {
+ @Override
+ public EventDispatcher answer(InvocationOnMock invocation) throws Throwable {
+ return mEventDispatcherMock;
+ }
+ });
+ PowerMockito.when(mUIManagerMock.getConstants()).thenAnswer(new Answer