Skip to content

Commit

Permalink
Refactor touch event dispatch
Browse files Browse the repository at this point in the history
Summary:
Updates touch events in Fabric to be dispatched through the same pipeline as the rest of events, instead of relying on custom dispatch behavior.

Previous method of handling touches was reusing Paper behavior which required:
1. Transform event into a Paper-compatible form of WritableArray and dispatch it to `RCTEventEmitter.receiveTouches`.
2. Intercept `receiveTouches` for Fabric and redirect it to `FabricEventEmitter`
3. Perform transformations copied from Paper JS renderer in Java, transform it to the final form and dispatch this event as usual after.

The new behavior uses emitter's `receiveEvent` method directly to dispatch events. Additionally, it should decrease allocations done when transforming events during step 3 above, as `WritableNativeMap`-based operations performed many re-allocations when reading/re-creating arrays.

Changelog:
[Android][Changed] - Added an experimental touch dispatch path

Reviewed By: JoshuaGross

Differential Revision: D31280052

fbshipit-source-id: 829c2646ac6b0ebff0f0106159e76d84324ac732
  • Loading branch information
Andrei Shikov authored and facebook-github-bot committed Oct 14, 2021
1 parent 53fd0f4 commit a2feaeb
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,6 @@ public static boolean isMapBufferSerializationEnabled() {
public static boolean enableScrollViewSnapToAlignmentProp = true;

public static boolean useDispatchUniqueForCoalescableEvents = false;

public static boolean useUpdatedTouchPreprocessing = false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReactSoftExceptionLogger;
import com.facebook.react.bridge.SoftAssertions;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.config.ReactFeatureFlags;

/**
* An event representing the start, end or movement of a touch. Corresponds to a single {@link
Expand Down Expand Up @@ -194,7 +196,41 @@ public void dispatch(RCTEventEmitter rctEventEmitter) {

@Override
public void dispatchModern(RCTModernEventEmitter rctEventEmitter) {
dispatch(rctEventEmitter);
if (ReactFeatureFlags.useUpdatedTouchPreprocessing) {
TouchesHelper.sendTouchEventModern(rctEventEmitter, this, /* useDispatchV2 */ false);
} else {
dispatch(rctEventEmitter);
}
}

@Override
public void dispatchModernV2(RCTModernEventEmitter rctEventEmitter) {
if (ReactFeatureFlags.useUpdatedTouchPreprocessing) {
TouchesHelper.sendTouchEventModern(rctEventEmitter, this, /* useDispatchV2 */ true);
} else {
dispatch(rctEventEmitter);
}
}

@Override
protected int getEventCategory() {
TouchEventType type = mTouchEventType;
if (type == null) {
return EventCategoryDef.UNSPECIFIED;
}

switch (type) {
case START:
return EventCategoryDef.CONTINUOUS_START;
case END:
case CANCEL:
return EventCategoryDef.CONTINUOUS_END;
case MOVE:
return EventCategoryDef.CONTINUOUS;
}

// Something something smart compiler...
return super.getEventCategory();
}

public MotionEvent getMotionEvent() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@

import android.view.MotionEvent;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactSoftExceptionLogger;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeArray;
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";
Expand All @@ -28,14 +29,16 @@ public class TouchesHelper {
private static final String LOCATION_X_KEY = "locationX";
private static final String LOCATION_Y_KEY = "locationY";

private static final String TAG = "TouchesHelper";

/**
* Creates catalyst pointers array in format that is expected by RCTEventEmitter JS module from
* 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(TouchEvent event) {
WritableArray touches = Arguments.createArray();
private static WritableMap[] createPointersArray(TouchEvent event) {
MotionEvent motionEvent = event.getMotionEvent();
WritableMap[] touches = new WritableMap[motionEvent.getPointerCount()];

// Calculate the coordinates for the target view.
// The MotionEvent contains the X,Y of the touch in the coordinate space of the root view
Expand Down Expand Up @@ -63,7 +66,8 @@ private static WritableArray createsPointersArray(TouchEvent event) {
touch.putInt(TARGET_KEY, event.getViewTag());
touch.putDouble(TIMESTAMP_KEY, event.getTimestampMs());
touch.putDouble(POINTER_IDENTIFIER_KEY, motionEvent.getPointerId(index));
touches.pushMap(touch);

touches[index] = touch;
}

return touches;
Expand All @@ -78,7 +82,8 @@ private static WritableArray createsPointersArray(TouchEvent event) {
*/
public static void sendTouchEvent(RCTEventEmitter rctEventEmitter, TouchEvent touchEvent) {
TouchEventType type = touchEvent.getTouchEventType();
WritableArray pointers = createsPointersArray(touchEvent);

WritableArray pointers = getWritableArray(createPointersArray(touchEvent));
MotionEvent motionEvent = touchEvent.getMotionEvent();

// For START and END events send only index of the pointer that is associated with that event
Expand All @@ -96,4 +101,98 @@ public static void sendTouchEvent(RCTEventEmitter rctEventEmitter, TouchEvent to

rctEventEmitter.receiveTouches(TouchEventType.getJSEventName(type), pointers, changedIndices);
}

/**
* Generate touch event data to match JS expectations. Combines logic in {@link #sendTouchEvent}
* and FabricEventEmitter to create the same data structure in a more efficient manner.
*
* <p>Touches have to be dispatched as separate events for each changed pointer to make JS process
* them correctly. To avoid allocations, we preprocess touch events in Java world and then convert
* them to native before dispatch.
*
* @param eventEmitter emitter to dispatch event to
* @param event the touch event to extract data from
* @param useDispatchV2 whether to dispatch additional data used by {@link Event#dispatchModernV2}
*/
public static void sendTouchEventModern(
RCTModernEventEmitter eventEmitter, TouchEvent event, boolean useDispatchV2) {
TouchEventType type = event.getTouchEventType();
MotionEvent motionEvent = event.getMotionEvent();

if (motionEvent == null) {
ReactSoftExceptionLogger.logSoftException(
TAG,
new IllegalStateException(
"Cannot dispatch a TouchEvent that has no MotionEvent; the TouchEvent has been recycled"));
return;
}

WritableMap[] touches = createPointersArray(event);
WritableMap[] changedTouches = null;

switch (type) {
case START:
int newPointerIndex = motionEvent.getActionIndex();

changedTouches = new WritableMap[] {touches[newPointerIndex].copy()};
break;
case END:
int finishedPointerIndex = motionEvent.getActionIndex();
/*
* Clear finished pointer index for compatibility with W3C touch "end" events, where the
* active touches don't include the set that has just been "ended".
*/
WritableMap finishedPointer = touches[finishedPointerIndex];
touches[finishedPointerIndex] = null;

changedTouches = new WritableMap[] {finishedPointer};
break;
case MOVE:
changedTouches = new WritableMap[touches.length];
for (int i = 0; i < touches.length; i++) {
changedTouches[i] = touches[i].copy();
}
break;
case CANCEL:
changedTouches = touches;
touches = new WritableMap[0];
break;
}

WritableArray touchesArray = getWritableArray(touches);
WritableArray changedTouchesArray = getWritableArray(/* copyObjects */ true, changedTouches);

for (WritableMap eventData : changedTouches) {
eventData.putArray(CHANGED_TOUCHES_KEY, changedTouchesArray);
eventData.putArray(TOUCHES_KEY, touchesArray);

if (useDispatchV2) {
eventEmitter.receiveEvent(
event.getSurfaceId(),
event.getViewTag(),
event.getEventName(),
event.canCoalesce(),
0,
eventData,
event.getEventCategory());
} else {
eventEmitter.receiveEvent(
event.getSurfaceId(), event.getViewTag(), event.getEventName(), eventData);
}
}
}

private static WritableArray getWritableArray(WritableMap... objects) {
return getWritableArray(false, objects);
}

private static WritableArray getWritableArray(boolean copyObjects, WritableMap... objects) {
WritableArray result = new WritableNativeArray();
for (WritableMap object : objects) {
if (object != null) {
result.pushMap(copyObjects ? object.copy() : object);
}
}
return result;
}
}

0 comments on commit a2feaeb

Please sign in to comment.