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 https://github.com/facebook/react-native/pull/9253

Differential Revision: D3759844

Pulled By: foghina

fbshipit-source-id: 86989c705847955bd65e6cf5a7d572ec7ccd3eb4
This commit is contained in:
Janic Duplessis
2016-09-19 04:07:36 -07:00
committed by Facebook Github Bot 9
parent 19d0429a76
commit 6565929358
13 changed files with 527 additions and 34 deletions

View File

@@ -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");
}
}

View File

@@ -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);
}
});
}
}

View File

@@ -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<AnimatedNode> mAnimatedNodes = new SparseArray<>();
private final ArrayList<AnimationDriver> mActiveAnimations = new ArrayList<>();
private final ArrayList<AnimatedNode> mUpdatedNodes = new ArrayList<>();
private final Map<String, EventAnimationDriver> mEventDrivers = new HashMap<>();
private final Map<String, Map<String, String>> 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<String, Map<String, String>>) 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<String> 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<String, String> 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

View File

@@ -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;

View File

@@ -92,6 +92,7 @@ public class EventDispatcher implements LifecycleEventListener {
private final Map<String, Short> mEventNameToEventId = MapBuilder.newHashMap();
private final DispatchEventsRunnable mDispatchEventsRunnable = new DispatchEventsRunnable();
private final ArrayList<Event> mEventStaging = new ArrayList<>();
private final ArrayList<EventDispatcherListener> 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();

View File

@@ -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);
}