diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 0f24d4b118b931..ce384231d433c5 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -5737,6 +5737,7 @@ public final class com/facebook/react/uimanager/events/TouchEvent : com/facebook public fun dispatch (Lcom/facebook/react/uimanager/events/RCTEventEmitter;)V public fun dispatchModern (Lcom/facebook/react/uimanager/events/RCTModernEventEmitter;)V public fun getCoalescingKey ()S + public fun getEventCategory ()I public fun getEventName ()Ljava/lang/String; public final fun getMotionEvent ()Landroid/view/MotionEvent; public final fun getTouchEventType ()Lcom/facebook/react/uimanager/events/TouchEventType; @@ -5778,9 +5779,11 @@ public final class com/facebook/react/uimanager/events/TouchEventType$Companion public final fun getJSEventName (Lcom/facebook/react/uimanager/events/TouchEventType;)Ljava/lang/String; } -public class com/facebook/react/uimanager/events/TouchesHelper { +public final class com/facebook/react/uimanager/events/TouchesHelper { + public static final field INSTANCE Lcom/facebook/react/uimanager/events/TouchesHelper; public static final field TARGET_KEY Ljava/lang/String; - public fun ()V + public static final fun sendTouchEvent (Lcom/facebook/react/uimanager/events/RCTModernEventEmitter;Lcom/facebook/react/uimanager/events/TouchEvent;)V + public static final fun sendTouchesLegacy (Lcom/facebook/react/uimanager/events/RCTEventEmitter;Lcom/facebook/react/uimanager/events/TouchEvent;)V } public final class com/facebook/react/uimanager/layoutanimation/InterpolatorType : java/lang/Enum { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.kt index 8feb8b3600f26d..0b763b702a399c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.kt @@ -126,7 +126,7 @@ public class TouchEvent private constructor() : Event() { } } - protected override fun getEventCategory(): Int { + public override fun getEventCategory(): Int { val type = touchEventType ?: return EventCategoryDef.UNSPECIFIED return when (type) { TouchEventType.START -> EventCategoryDef.CONTINUOUS_START diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.java deleted file mode 100644 index 5bebefd45df985..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.uimanager.events; - -import android.view.MotionEvent; -import com.facebook.infer.annotation.Nullsafe; -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.uimanager.PixelUtil; -import com.facebook.systrace.Systrace; - -/** Class responsible for generating catalyst touch events based on android {@link MotionEvent}. */ -@Nullsafe(Nullsafe.Mode.LOCAL) -public class TouchesHelper { - @Deprecated public static final String TARGET_KEY = "target"; - - private static final String TARGET_SURFACE_KEY = "targetSurface"; - private static final String CHANGED_TOUCHES_KEY = "changedTouches"; - private static final String TOUCHES_KEY = "touches"; - private static final String PAGE_X_KEY = "pageX"; - private static final String PAGE_Y_KEY = "pageY"; - private static final String TIMESTAMP_KEY = "timestamp"; - private static final String POINTER_IDENTIFIER_KEY = "identifier"; - - 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 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 - // The TouchEvent contains the X,Y of the touch in the coordinate space of the target view - // Subtracting them allows us to get the coordinates of the target view's top left corner - // We then use this when computing the view specific touches below - // Since only one view is actually handling even multiple touches, the values are all relative - // to this one target view. - float targetViewCoordinateX = motionEvent.getX() - event.getViewX(); - float targetViewCoordinateY = motionEvent.getY() - event.getViewY(); - - for (int index = 0; index < motionEvent.getPointerCount(); index++) { - WritableMap touch = Arguments.createMap(); - // pageX,Y values are relative to the RootReactView - // the motionEvent already contains coordinates in that view - touch.putDouble(PAGE_X_KEY, PixelUtil.toDIPFromPixel(motionEvent.getX(index))); - touch.putDouble(PAGE_Y_KEY, PixelUtil.toDIPFromPixel(motionEvent.getY(index))); - // locationX,Y values are relative to the target view - // To compute the values for the view, we subtract that views location from the event X,Y - float locationX = motionEvent.getX(index) - targetViewCoordinateX; - 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, event.getSurfaceId()); - touch.putInt(TARGET_KEY, event.getViewTag()); - touch.putDouble(TIMESTAMP_KEY, event.getTimestampMs()); - touch.putDouble(POINTER_IDENTIFIER_KEY, motionEvent.getPointerId(index)); - - touches[index] = touch; - } - - return touches; - } - - /** - * Generate and send touch event to RCTEventEmitter JS module associated with the given {@param - * context} for legacy renderer. Touch event can encode multiple concurrent touches (pointers). - * - * @param rctEventEmitter Event emitter used to execute JS module call - * @param touchEvent native touch event to read pointers count and coordinates from - */ - /* package */ static void sendTouchesLegacy( - RCTEventEmitter rctEventEmitter, TouchEvent touchEvent) { - TouchEventType type = touchEvent.getTouchEventType(); - - WritableArray pointers = - getWritableArray(/* copyObjects */ false, createPointersArray(touchEvent)); - MotionEvent motionEvent = touchEvent.getMotionEvent(); - - // For START and END events send only index of the pointer that is associated with that event - // For MOVE and CANCEL events 'changedIndices' array should contain all the pointers indices - WritableArray changedIndices = Arguments.createArray(); - if (type == TouchEventType.MOVE || type == TouchEventType.CANCEL) { - for (int i = 0; i < motionEvent.getPointerCount(); i++) { - changedIndices.pushInt(i); - } - } else if (type == TouchEventType.START || type == TouchEventType.END) { - changedIndices.pushInt(motionEvent.getActionIndex()); - } else { - throw new RuntimeException("Unknown touch type: " + type); - } - - 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 - */ - /* package */ static void sendTouchEvent(RCTModernEventEmitter eventEmitter, TouchEvent event) { - Systrace.beginSection( - Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, - "TouchesHelper.sentTouchEventModern(" + event.getEventName() + ")"); - try { - 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; - } - - if (changedTouches != null) { - for (WritableMap touchData : changedTouches) { - WritableMap eventData = touchData.copy(); - WritableArray changedTouchesArray = - getWritableArray(/* copyObjects */ true, changedTouches); - WritableArray touchesArray = getWritableArray(/* copyObjects */ true, touches); - - eventData.putArray(CHANGED_TOUCHES_KEY, changedTouchesArray); - eventData.putArray(TOUCHES_KEY, touchesArray); - - eventEmitter.receiveEvent( - event.getSurfaceId(), - event.getViewTag(), - event.getEventName(), - event.canCoalesce(), - 0, - eventData, - event.getEventCategory()); - } - } - } finally { - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); - } - } - - private static WritableArray getWritableArray(boolean copyObjects, WritableMap... objects) { - WritableArray result = Arguments.createArray(); - for (WritableMap object : objects) { - if (object != null) { - result.pushMap(copyObjects ? object.copy() : object); - } - } - return result; - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.kt new file mode 100644 index 00000000000000..a51a181e3267d0 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.kt @@ -0,0 +1,196 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.uimanager.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableArray +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.PixelUtil.pxToDp +import com.facebook.react.uimanager.events.TouchEventType.Companion.getJSEventName +import com.facebook.systrace.Systrace + +/** Class responsible for generating catalyst touch events based on android [MotionEvent]. */ +public object TouchesHelper { + @JvmField @Deprecated("Not used in New Architecture") public val TARGET_KEY: String = "target" + + private const val TARGET_SURFACE_KEY = "targetSurface" + private const val CHANGED_TOUCHES_KEY = "changedTouches" + private const val TOUCHES_KEY = "touches" + private const val PAGE_X_KEY = "pageX" + private const val PAGE_Y_KEY = "pageY" + private const val TIMESTAMP_KEY = "timestamp" + private const val POINTER_IDENTIFIER_KEY = "identifier" + + private const val LOCATION_X_KEY = "locationX" + private const val LOCATION_Y_KEY = "locationY" + + /** + * 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 fun createPointersArray(event: TouchEvent): Array { + val motionEvent = event.getMotionEvent() + val touches = arrayOfNulls(motionEvent.pointerCount) + + // Calculate the coordinates for the target view. + // The MotionEvent contains the X,Y of the touch in the coordinate space of the root view + // The TouchEvent contains the X,Y of the touch in the coordinate space of the target view + // Subtracting them allows us to get the coordinates of the target view's top left corner + // We then use this when computing the view specific touches below + // Since only one view is actually handling even multiple touches, the values are all relative + // to this one target view. + val targetViewCoordinateX = motionEvent.x - event.viewX + val targetViewCoordinateY = motionEvent.y - event.viewY + + for (index in 0 until motionEvent.pointerCount) { + val touch = Arguments.createMap() + + // pageX,Y values are relative to the RootReactView + // the motionEvent already contains coordinates in that view + touch.putDouble(PAGE_X_KEY, motionEvent.getX(index).pxToDp().toDouble()) + touch.putDouble(PAGE_Y_KEY, motionEvent.getY(index).pxToDp().toDouble()) + + // locationX,Y values are relative to the target view + // To compute the values for the view, we subtract that views location from the event X,Y + val locationX = motionEvent.getX(index) - targetViewCoordinateX + val locationY = motionEvent.getY(index) - targetViewCoordinateY + touch.putDouble(LOCATION_X_KEY, locationX.pxToDp().toDouble()) + touch.putDouble(LOCATION_Y_KEY, locationY.pxToDp().toDouble()) + + touch.putInt(TARGET_SURFACE_KEY, event.surfaceId) + @Suppress("DEPRECATION") touch.putInt(TARGET_KEY, event.viewTag) + touch.putDouble(TIMESTAMP_KEY, event.timestampMs.toDouble()) + touch.putDouble(POINTER_IDENTIFIER_KEY, motionEvent.getPointerId(index).toDouble()) + + touches[index] = touch + } + + return touches + } + + /** + * Generate and send touch event to RCTEventEmitter JS module associated with the given {@param * + * context} for legacy renderer. Touch event can encode multiple concurrent touches (pointers). + * + * @param rctEventEmitter Event emitter used to execute JS module call + * @param touchEvent native touch event to read pointers count and coordinates from + */ + @Suppress("DEPRECATION") + @JvmStatic + public fun sendTouchesLegacy(rctEventEmitter: RCTEventEmitter, touchEvent: TouchEvent) { + val type = touchEvent.getTouchEventType() + + val pointers = getWritableArray(/* copyObjects */ false, createPointersArray(touchEvent)) + val motionEvent = touchEvent.getMotionEvent() + + // For START and END events send only index of the pointer that is associated with that event + // For MOVE and CANCEL events 'changedIndices' array should contain all the pointers indices + val changedIndices = Arguments.createArray() + if (type == TouchEventType.MOVE || type == TouchEventType.CANCEL) { + for (i in 0 until motionEvent.pointerCount) { + changedIndices.pushInt(i) + } + } else if (type == TouchEventType.START || type == TouchEventType.END) { + changedIndices.pushInt(motionEvent.actionIndex) + } else { + throw RuntimeException("Unknown touch type: $type") + } + + @Suppress("DEPRECATION") + rctEventEmitter.receiveTouches(getJSEventName(type), pointers, changedIndices) + } + + /** + * Generate touch event data to match JS expectations. Combines logic in [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 + */ + @JvmStatic + public fun sendTouchEvent(eventEmitter: RCTModernEventEmitter, event: TouchEvent) { + Systrace.beginSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + "TouchesHelper.sentTouchEventModern(" + event.eventName + ")") + try { + val type = event.getTouchEventType() + val motionEvent = event.getMotionEvent() + var touches = createPointersArray(event) + var changedTouches: Array? = null + + when (type) { + TouchEventType.START -> { + val newPointerIndex = motionEvent.actionIndex + + changedTouches = arrayOf(touches[newPointerIndex]?.copy()) + } + TouchEventType.END -> { + val finishedPointerIndex = motionEvent.actionIndex + /* + * 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". + */ + val finishedPointer = touches[finishedPointerIndex] + touches[finishedPointerIndex] = null + + changedTouches = arrayOf(finishedPointer) + } + TouchEventType.MOVE -> { + changedTouches = arrayOfNulls(touches.size) + var i = 0 + while (i < touches.size) { + changedTouches[i] = touches[i]?.copy() + i++ + } + } + TouchEventType.CANCEL -> { + changedTouches = touches + touches = arrayOfNulls(0) + } + } + + for (touchData in changedTouches) { + val eventData = + touchData?.let { td -> + val ed = td.copy() + val changedTouchesArray = getWritableArray(/* copyObjects */ true, changedTouches) + val touchesArray = getWritableArray(/* copyObjects */ true, touches) + ed.putArray(CHANGED_TOUCHES_KEY, changedTouchesArray) + ed.putArray(TOUCHES_KEY, touchesArray) + ed + } + + eventEmitter.receiveEvent( + event.surfaceId, + event.viewTag, + event.eventName, + event.canCoalesce(), + 0, + eventData, + event.eventCategory) + } + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE) + } + } + + private fun getWritableArray(copyObjects: Boolean, objects: Array): WritableArray { + val result = Arguments.createArray() + for (obj in objects) { + if (obj != null) { + result.pushMap(if (copyObjects) obj.copy() else obj) + } + } + return result + } +}