diff --git a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java index 7cf3f4237eec61..446ea326ac96c7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java +++ b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java @@ -103,4 +103,6 @@ public static boolean isMapBufferSerializationEnabled() { public static boolean enableScrollViewSnapToAlignmentProp = true; public static boolean useDispatchUniqueForCoalescableEvents = false; + + public static boolean useUpdatedTouchPreprocessing = false; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java index f21e140b1f8f4f..4ee7137456ac6c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java @@ -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 @@ -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() { diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.java index 2b2c6ae9758102..25b4236828221a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.java @@ -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"; @@ -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 @@ -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; @@ -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 @@ -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. + * + *

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; + } }