Refactor EventEmitters to take optional surfaceId, migrate TouchEvent

Summary:
Refactor EventEmitters to take an optional SurfaceId that Fabric will use, and non-Fabric will not.

Migrating touches is a good proof-of-concept for how this could be used generally, and as it turns out, TouchEvent's API is more flexible than most other event APIs (because it uses a dictionary to pass data around, so we can just stuff SurfaceId into it - not efficient, but flexible).

All new APIs are backwards-compatible and designed to work with old-style events, with Fabric and non-Fabric. Native Views that migrate to the new API will be backwards-compatible and get an efficiency boost in Fabric.

Changelog: [Internal]

Reviewed By: mdvacca

Differential Revision: D26025135

fbshipit-source-id: 5b418951e9d0a3882f2d67398f2aaadac8a3a556
This commit is contained in:
Joshua Gross
2021-01-22 19:29:00 -08:00
committed by Facebook GitHub Bot
parent 2fbbdbb2ce
commit 708038d80e
9 changed files with 116 additions and 32 deletions
@@ -9,6 +9,7 @@ package com.facebook.react.fabric.events;
import static com.facebook.react.uimanager.events.TouchesHelper.CHANGED_TOUCHES_KEY;
import static com.facebook.react.uimanager.events.TouchesHelper.TARGET_KEY;
import static com.facebook.react.uimanager.events.TouchesHelper.TARGET_SURFACE_KEY;
import static com.facebook.react.uimanager.events.TouchesHelper.TOP_TOUCH_CANCEL_KEY;
import static com.facebook.react.uimanager.events.TouchesHelper.TOP_TOUCH_END_KEY;
import static com.facebook.react.uimanager.events.TouchesHelper.TOUCHES_KEY;
@@ -76,14 +77,15 @@ public class FabricEventEmitter implements RCTModernEventEmitter {
touch.putArray(TOUCHES_KEY, copyWritableArray(touches));
WritableMap nativeEvent = touch;
int rootNodeID = 0;
int target = nativeEvent.getInt(TARGET_KEY);
if (target < 1) {
int targetSurfaceId = nativeEvent.getInt(TARGET_SURFACE_KEY);
int targetReactTag = nativeEvent.getInt(TARGET_KEY);
if (targetReactTag < 1) {
FLog.e(TAG, "A view is reporting that a touch occurred on tag zero.");
} else {
rootNodeID = target;
rootNodeID = targetReactTag;
}
receiveEvent(rootNodeID, eventTopLevelType, touch);
receiveEvent(targetSurfaceId, rootNodeID, eventTopLevelType, touch);
}
}
@@ -50,6 +50,17 @@ public class JSTouchDispatcher {
mTargetTag = -1;
}
private int getSurfaceId() {
if (mRootViewGroup instanceof ReactRoot) {
return ((ReactRoot) mRootViewGroup).getRootViewTag();
}
if (mRootViewGroup != null && mRootViewGroup.getContext() instanceof ThemedReactContext) {
ThemedReactContext context = (ThemedReactContext) mRootViewGroup.getContext();
return context.getSurfaceId();
}
return -1;
}
/**
* Main catalyst view is responsible for collecting and sending touch events to JS. This method
* reacts for an incoming android native touch events ({@link MotionEvent}) and calls into {@link
@@ -70,9 +81,11 @@ public class JSTouchDispatcher {
// this gesture
mChildIsHandlingNativeGesture = false;
mGestureStartTime = ev.getEventTime();
mTargetTag = findTargetTagAndSetCoordinates(ev);
eventDispatcher.dispatchEvent(
TouchEvent.obtain(
getSurfaceId(),
mTargetTag,
TouchEventType.START,
ev,
@@ -97,6 +110,7 @@ public class JSTouchDispatcher {
findTargetTagAndSetCoordinates(ev);
eventDispatcher.dispatchEvent(
TouchEvent.obtain(
getSurfaceId(),
mTargetTag,
TouchEventType.END,
ev,
@@ -111,6 +125,7 @@ public class JSTouchDispatcher {
findTargetTagAndSetCoordinates(ev);
eventDispatcher.dispatchEvent(
TouchEvent.obtain(
getSurfaceId(),
mTargetTag,
TouchEventType.MOVE,
ev,
@@ -122,6 +137,7 @@ public class JSTouchDispatcher {
// New pointer goes down, this can only happen after ACTION_DOWN is sent for the first pointer
eventDispatcher.dispatchEvent(
TouchEvent.obtain(
getSurfaceId(),
mTargetTag,
TouchEventType.START,
ev,
@@ -133,6 +149,7 @@ public class JSTouchDispatcher {
// Exactly onw of the pointers goes up
eventDispatcher.dispatchEvent(
TouchEvent.obtain(
getSurfaceId(),
mTargetTag,
TouchEventType.END,
ev,
@@ -181,6 +198,7 @@ public class JSTouchDispatcher {
Assertions.assertNotNull(eventDispatcher)
.dispatchEvent(
TouchEvent.obtain(
getSurfaceId(),
mTargetTag,
TouchEventType.CANCEL,
androidEvent,
@@ -49,6 +49,9 @@ public class BlackHoleEventDispatcher implements EventDispatcher {
@Override
public void registerEventEmitter(int uiManagerType, RCTEventEmitter eventEmitter) {}
@Override
public void registerEventEmitter(int uiManagerType, RCTModernEventEmitter eventEmitter) {}
@Override
public void unregisterEventEmitter(int uiManagerType) {}
@@ -26,8 +26,11 @@ public interface EventDispatcher {
void removeBatchEventDispatchedListener(BatchEventDispatchedListener listener);
@Deprecated
void registerEventEmitter(@UIManagerType int uiManagerType, RCTEventEmitter eventEmitter);
void registerEventEmitter(@UIManagerType int uiManagerType, RCTModernEventEmitter eventEmitter);
void unregisterEventEmitter(@UIManagerType int uiManagerType);
void onCatalystInstanceDestroyed();
@@ -263,6 +263,11 @@ public class EventDispatcherImpl implements EventDispatcher, LifecycleEventListe
mReactEventEmitter.register(uiManagerType, eventEmitter);
}
public void registerEventEmitter(
@UIManagerType int uiManagerType, RCTModernEventEmitter eventEmitter) {
mReactEventEmitter.register(uiManagerType, eventEmitter);
}
public void unregisterEventEmitter(@UIManagerType int uiManagerType) {
mReactEventEmitter.unregister(uiManagerType);
}
@@ -361,7 +366,7 @@ public class EventDispatcherImpl implements EventDispatcher, LifecycleEventListe
}
Systrace.endAsyncFlow(
Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, event.getEventName(), event.getUniqueID());
event.dispatch(mReactEventEmitter);
event.dispatchV2(mReactEventEmitter);
event.dispose();
}
clearEventsToDispatch();
@@ -13,6 +13,7 @@ import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
/** Deprecated in favor of RCTModernEventEmitter, which extends this interface. */
@DoNotStrip
@Deprecated
public interface RCTEventEmitter extends JavaScriptModule {
@@ -9,9 +9,7 @@ package com.facebook.react.uimanager.events;
import static com.facebook.react.uimanager.events.TouchesHelper.TARGET_KEY;
import android.util.SparseArray;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactNoCrashSoftException;
@@ -21,11 +19,16 @@ import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.common.UIManagerType;
import com.facebook.react.uimanager.common.ViewUtil;
public class ReactEventEmitter implements RCTEventEmitter {
public class ReactEventEmitter implements RCTModernEventEmitter {
private static final String TAG = "ReactEventEmitter";
private final SparseArray<RCTEventEmitter> mEventEmitters = new SparseArray<>();
@Nullable
private RCTModernEventEmitter mFabricEventEmitter =
null; /* Corresponds to a Fabric EventEmitter */
@Nullable
private RCTEventEmitter mRCTEventEmitter = null; /* Corresponds to a Non-Fabric EventEmitter */
private final ReactApplicationContext mReactContext;
@@ -33,46 +36,61 @@ public class ReactEventEmitter implements RCTEventEmitter {
mReactContext = reactContext;
}
public void register(@UIManagerType int uiManagerType, RCTModernEventEmitter eventEmitter) {
assert uiManagerType == UIManagerType.FABRIC;
mFabricEventEmitter = eventEmitter;
}
public void register(@UIManagerType int uiManagerType, RCTEventEmitter eventEmitter) {
mEventEmitters.put(uiManagerType, eventEmitter);
assert uiManagerType == UIManagerType.DEFAULT;
mRCTEventEmitter = eventEmitter;
}
public void unregister(@UIManagerType int uiManagerType) {
mEventEmitters.remove(uiManagerType);
if (uiManagerType == UIManagerType.DEFAULT) {
mRCTEventEmitter = null;
} else {
mFabricEventEmitter = null;
}
}
@Override
public void receiveEvent(int targetReactTag, String eventName, @Nullable WritableMap event) {
RCTEventEmitter eventEmitter = getEventEmitter(targetReactTag);
if (eventEmitter != null) {
eventEmitter.receiveEvent(targetReactTag, eventName, event);
}
receiveEvent(-1, targetReactTag, eventName, event);
}
@Override
public void receiveTouches(
String eventName, WritableArray touches, WritableArray changedIndices) {
Assertions.assertCondition(touches.size() > 0);
int reactTag = touches.getMap(0).getInt(TARGET_KEY);
RCTEventEmitter eventEmitter = getEventEmitter(reactTag);
if (eventEmitter != null) {
eventEmitter.receiveTouches(eventName, touches, changedIndices);
@UIManagerType int uiManagerType = ViewUtil.getUIManagerType(reactTag);
if (uiManagerType == UIManagerType.FABRIC && mFabricEventEmitter != null) {
mFabricEventEmitter.receiveTouches(eventName, touches, changedIndices);
} else if (uiManagerType == UIManagerType.DEFAULT && getEventEmitter(reactTag) != null) {
mRCTEventEmitter.receiveTouches(eventName, touches, changedIndices);
} else {
ReactSoftException.logSoftException(
TAG,
new ReactNoCrashSoftException(
"Cannot find EventEmitter for receivedTouches: ReactTag["
+ reactTag
+ "] UIManagerType["
+ uiManagerType
+ "] EventName["
+ eventName
+ "]"));
}
}
@Nullable
private RCTEventEmitter getEventEmitter(int reactTag) {
int type = ViewUtil.getUIManagerType(reactTag);
RCTEventEmitter eventEmitter = mEventEmitters.get(type);
if (eventEmitter == null) {
// TODO T54145494: Refactor RN Event Emitter system to make sure reactTags are always managed
// by RN
FLog.e(
TAG, "Unable to find event emitter for reactTag: %d - uiManagerType: %d", reactTag, type);
assert type == UIManagerType.DEFAULT;
if (mRCTEventEmitter == null) {
if (mReactContext.hasActiveCatalystInstance()) {
eventEmitter = mReactContext.getJSModule(RCTEventEmitter.class);
mRCTEventEmitter = mReactContext.getJSModule(RCTEventEmitter.class);
} else {
ReactSoftException.logSoftException(
TAG,
@@ -84,6 +102,30 @@ public class ReactEventEmitter implements RCTEventEmitter {
+ " - No active Catalyst instance!"));
}
}
return eventEmitter;
return mRCTEventEmitter;
}
@Override
public void receiveEvent(
int surfaceId, int targetReactTag, String eventName, @Nullable WritableMap event) {
@UIManagerType int uiManagerType = ViewUtil.getUIManagerType(targetReactTag);
if (uiManagerType == UIManagerType.FABRIC && mFabricEventEmitter != null) {
mFabricEventEmitter.receiveEvent(surfaceId, targetReactTag, eventName, event);
} else if (uiManagerType == UIManagerType.DEFAULT && getEventEmitter(targetReactTag) != null) {
mRCTEventEmitter.receiveEvent(targetReactTag, eventName, event);
} else {
ReactSoftException.logSoftException(
TAG,
new ReactNoCrashSoftException(
"Cannot find EventEmitter for receiveEvent: SurfaceId["
+ surfaceId
+ "] ReactTag["
+ targetReactTag
+ "] UIManagerType["
+ uiManagerType
+ "] EventName["
+ eventName
+ "]"));
}
}
}
@@ -31,6 +31,7 @@ public class TouchEvent extends Event<TouchEvent> {
public static final long UNSET = Long.MIN_VALUE;
public static TouchEvent obtain(
int surfaceId,
int viewTag,
TouchEventType touchEventType,
MotionEvent motionEventToCopy,
@@ -43,6 +44,7 @@ public class TouchEvent extends Event<TouchEvent> {
event = new TouchEvent();
}
event.init(
surfaceId,
viewTag,
touchEventType,
motionEventToCopy,
@@ -64,6 +66,7 @@ public class TouchEvent extends Event<TouchEvent> {
private TouchEvent() {}
private void init(
int surfaceId,
int viewTag,
TouchEventType touchEventType,
MotionEvent motionEventToCopy,
@@ -71,7 +74,7 @@ public class TouchEvent extends Event<TouchEvent> {
float viewX,
float viewY,
TouchEventCoalescingKeyHelper touchEventCoalescingKeyHelper) {
super.init(viewTag);
super.init(surfaceId, viewTag);
SoftAssertions.assertCondition(
gestureStartTime != UNSET, "Gesture start time must be initialized");
@@ -141,7 +144,11 @@ public class TouchEvent extends Event<TouchEvent> {
@Override
public void dispatch(RCTEventEmitter rctEventEmitter) {
TouchesHelper.sendTouchEvent(
rctEventEmitter, Assertions.assertNotNull(mTouchEventType), getViewTag(), this);
rctEventEmitter,
Assertions.assertNotNull(mTouchEventType),
getSurfaceId(),
getViewTag(),
this);
}
public MotionEvent getMotionEvent() {
@@ -16,6 +16,7 @@ import com.facebook.react.uimanager.PixelUtil;
/** Class responsible for generating catalyst touch events based on android {@link MotionEvent}. */
public class TouchesHelper {
public static final String TARGET_SURFACE_KEY = "targetSurface";
public static final String TARGET_KEY = "target";
public static final String CHANGED_TOUCHES_KEY = "changedTouches";
public static final String TOUCHES_KEY = "touches";
@@ -34,7 +35,8 @@ public class TouchesHelper {
* given {@param event} instance. This method use {@param reactTarget} parameter to set as a
* target view id associated with current gesture.
*/
private static WritableArray createsPointersArray(int reactTarget, TouchEvent event) {
private static WritableArray createsPointersArray(
int surfaceId, int reactTarget, TouchEvent event) {
WritableArray touches = Arguments.createArray();
MotionEvent motionEvent = event.getMotionEvent();
@@ -60,6 +62,7 @@ public class TouchesHelper {
float locationY = motionEvent.getY(index) - targetViewCoordinateY;
touch.putDouble(LOCATION_X_KEY, PixelUtil.toDIPFromPixel(locationX));
touch.putDouble(LOCATION_Y_KEY, PixelUtil.toDIPFromPixel(locationY));
touch.putInt(TARGET_SURFACE_KEY, surfaceId);
touch.putInt(TARGET_KEY, reactTarget);
touch.putDouble(TIMESTAMP_KEY, event.getTimestampMs());
touch.putDouble(POINTER_IDENTIFIER_KEY, motionEvent.getPointerId(index));
@@ -81,10 +84,10 @@ public class TouchesHelper {
public static void sendTouchEvent(
RCTEventEmitter rctEventEmitter,
TouchEventType type,
int surfaceId,
int reactTarget,
TouchEvent touchEvent) {
WritableArray pointers = createsPointersArray(reactTarget, touchEvent);
WritableArray pointers = createsPointersArray(surfaceId, reactTarget, touchEvent);
MotionEvent motionEvent = touchEvent.getMotionEvent();
// For START and END events send only index of the pointer that is associated with that event