diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index 0a4eadc008621f..0f51cd86d4bb94 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -61,6 +61,9 @@ public class ReactRootView extends SizeMonitoringFrameLayout implements RootView private @Nullable String mJSModuleName; private @Nullable Bundle mLaunchOptions; private int mTargetTag = -1; + // Note mTargetCoordinates are Y,X + // TODO: t9136625 tracks moving to X,Y + private final float[] mTargetCoordinates = new float[2]; private boolean mChildIsHandlingNativeGesture = false; private boolean mWasMeasured = false; private boolean mAttachScheduled = false; @@ -143,9 +146,19 @@ private void handleTouchEvent(MotionEvent ev) { // {@link #findTargetTagForTouch} to find react view ID that will be responsible for handling // this gesture mChildIsHandlingNativeGesture = false; - mTargetTag = TouchTargetHelper.findTargetTagForTouch(ev.getY(), ev.getX(), this); + mTargetTag = TouchTargetHelper.findTargetTagAndCoordinatesForTouch( + ev.getY(), + ev.getX(), + this, + mTargetCoordinates); eventDispatcher.dispatchEvent( - TouchEvent.obtain(mTargetTag, SystemClock.uptimeMillis(), TouchEventType.START, ev)); + TouchEvent.obtain( + mTargetTag, + SystemClock.uptimeMillis(), + TouchEventType.START, + ev, + mTargetCoordinates[1], + mTargetCoordinates[0])); } else if (mChildIsHandlingNativeGesture) { // If the touch was intercepted by a child, we've already sent a cancel event to JS for this // gesture, so we shouldn't send any more touches related to it. @@ -161,20 +174,44 @@ private void handleTouchEvent(MotionEvent ev) { // End of the gesture. We reset target tag to -1 and expect no further event associated with // this gesture. eventDispatcher.dispatchEvent( - TouchEvent.obtain(mTargetTag, SystemClock.uptimeMillis(), TouchEventType.END, ev)); + TouchEvent.obtain( + mTargetTag, + SystemClock.uptimeMillis(), + TouchEventType.END, + ev, + mTargetCoordinates[1], + mTargetCoordinates[0])); mTargetTag = -1; } else if (action == MotionEvent.ACTION_MOVE) { // Update pointer position for current gesture eventDispatcher.dispatchEvent( - TouchEvent.obtain(mTargetTag, SystemClock.uptimeMillis(), TouchEventType.MOVE, ev)); + TouchEvent.obtain( + mTargetTag, + SystemClock.uptimeMillis(), + TouchEventType.MOVE, + ev, + mTargetCoordinates[1], + mTargetCoordinates[0])); } else if (action == MotionEvent.ACTION_POINTER_DOWN) { // New pointer goes down, this can only happen after ACTION_DOWN is sent for the first pointer eventDispatcher.dispatchEvent( - TouchEvent.obtain(mTargetTag, SystemClock.uptimeMillis(), TouchEventType.START, ev)); + TouchEvent.obtain( + mTargetTag, + SystemClock.uptimeMillis(), + TouchEventType.START, + ev, + mTargetCoordinates[1], + mTargetCoordinates[0])); } else if (action == MotionEvent.ACTION_POINTER_UP) { // Exactly onw of the pointers goes up eventDispatcher.dispatchEvent( - TouchEvent.obtain(mTargetTag, SystemClock.uptimeMillis(), TouchEventType.END, ev)); + TouchEvent.obtain( + mTargetTag, + SystemClock.uptimeMillis(), + TouchEventType.END, + ev, + mTargetCoordinates[1], + mTargetCoordinates[0])); } else if (action == MotionEvent.ACTION_CANCEL) { dispatchCancelEvent(ev); mTargetTag = -1; @@ -223,7 +260,9 @@ private void dispatchCancelEvent(MotionEvent androidEvent) { mTargetTag, SystemClock.uptimeMillis(), TouchEventType.CANCEL, - androidEvent)); + androidEvent, + mTargetCoordinates[1], + mTargetCoordinates[0])); } @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java index 0658f7005ccd22..409a9f91f4028c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java @@ -39,17 +39,34 @@ public static int findTargetTagForTouch( float eventY, float eventX, ViewGroup viewGroup) { + return findTargetTagAndCoordinatesForTouch(eventY, eventX, viewGroup, mEventCoords); + } + + /** + * Find touch event target view within the provided container given the coordinates provided + * via {@link MotionEvent}. + * + * @param eventY the Y screen coordinate of the touch location + * @param eventX the X screen coordinate of the touch location + * @param viewGroup the container view to traverse + * @param viewCoords an out parameter that will return the Y,X value in the target view + * @return the react tag ID of the child view that should handle the event + */ + public static int findTargetTagAndCoordinatesForTouch( + float eventY, + float eventX, + ViewGroup viewGroup, + float[] viewCoords) { UiThreadUtil.assertOnUiThread(); int targetTag = viewGroup.getId(); // Store eventCoords in array so that they are modified to be relative to the targetView found. - float[] eventCoords = mEventCoords; - eventCoords[0] = eventY; - eventCoords[1] = eventX; - View nativeTargetView = findTouchTargetView(eventCoords, viewGroup); + viewCoords[0] = eventY; + viewCoords[1] = eventX; + View nativeTargetView = findTouchTargetView(viewCoords, viewGroup); if (nativeTargetView != null) { View reactTargetView = findClosestReactAncestor(nativeTargetView); if (reactTargetView != null) { - targetTag = getTouchTargetForView(reactTargetView, eventCoords[0], eventCoords[1]); + targetTag = getTouchTargetForView(reactTargetView, viewCoords[0], viewCoords[1]); } } return targetTag; 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 407b8c69108edd..1600b74783d047 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 @@ -35,12 +35,14 @@ public static TouchEvent obtain( int viewTag, long timestampMs, TouchEventType touchEventType, - MotionEvent motionEventToCopy) { + MotionEvent motionEventToCopy, + float viewX, + float viewY) { TouchEvent event = EVENTS_POOL.acquire(); if (event == null) { event = new TouchEvent(); } - event.init(viewTag, timestampMs, touchEventType, motionEventToCopy); + event.init(viewTag, timestampMs, touchEventType, motionEventToCopy, viewX, viewY); return event; } @@ -48,6 +50,10 @@ public static TouchEvent obtain( private @Nullable TouchEventType mTouchEventType; private short mCoalescingKey; + // Coordinates in the ViewTag coordinate space + private float mViewX; + private float mViewY; + private TouchEvent() { } @@ -55,7 +61,9 @@ private void init( int viewTag, long timestampMs, TouchEventType touchEventType, - MotionEvent motionEventToCopy) { + MotionEvent motionEventToCopy, + float viewX, + float viewY) { super.init(viewTag, timestampMs); short coalescingKey = 0; @@ -84,6 +92,8 @@ private void init( mTouchEventType = touchEventType; mMotionEvent = MotionEvent.obtain(motionEventToCopy); mCoalescingKey = coalescingKey; + mViewX = viewX; + mViewY = viewY; } @Override @@ -126,6 +136,19 @@ public void dispatch(RCTEventEmitter rctEventEmitter) { rctEventEmitter, Assertions.assertNotNull(mTouchEventType), getViewTag(), - Assertions.assertNotNull(mMotionEvent)); + this); + } + + public MotionEvent getMotionEvent() { + Assertions.assertNotNull(mMotionEvent); + return mMotionEvent; + } + + public float getViewX() { + return mViewX; + } + + public float getViewY() { + return mViewY; } } 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 56c6ff0ada2f33..d312a402026be5 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 @@ -27,8 +27,6 @@ private static final String TIMESTAMP_KEY = "timeStamp"; private static final String POINTER_IDENTIFIER_KEY = "identifier"; - // TODO(7351435): remove when we standardize touchEvent payload, since iOS uses locationXYZ but - // Android uses pageXYZ. As a temporary solution, Android currently sends both. private static final String LOCATION_X_KEY = "locationX"; private static final String LOCATION_Y_KEY = "locationY"; @@ -37,23 +35,35 @@ * 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, MotionEvent event) { + private static WritableArray createsPointersArray(int reactTarget, TouchEvent event) { WritableArray touches = Arguments.createArray(); + MotionEvent motionEvent = event.getMotionEvent(); - // Calculate raw-to-relative offset as getRawX() and getRawY() can only return values for the - // pointer at index 0. We use those value to calculate "raw" coordinates for other pointers - float offsetX = event.getRawX() - event.getX(); - float offsetY = event.getRawY() - event.getY(); + // 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 < event.getPointerCount(); index++) { + for (int index = 0; index < motionEvent.getPointerCount(); index++) { WritableMap touch = Arguments.createMap(); - touch.putDouble(PAGE_X_KEY, PixelUtil.toDIPFromPixel(event.getX(index) + offsetX)); - touch.putDouble(PAGE_Y_KEY, PixelUtil.toDIPFromPixel(event.getY(index) + offsetY)); - touch.putDouble(LOCATION_X_KEY, PixelUtil.toDIPFromPixel(event.getX(index))); - touch.putDouble(LOCATION_Y_KEY, PixelUtil.toDIPFromPixel(event.getY(index))); + // 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_KEY, reactTarget); - touch.putDouble(TIMESTAMP_KEY, event.getEventTime()); - touch.putDouble(POINTER_IDENTIFIER_KEY, event.getPointerId(index)); + touch.putDouble(TIMESTAMP_KEY, motionEvent.getEventTime()); + touch.putDouble(POINTER_IDENTIFIER_KEY, motionEvent.getPointerId(index)); touches.pushMap(touch); } @@ -67,25 +77,26 @@ private static WritableArray createsPointersArray(int reactTarget, MotionEvent e * @param rctEventEmitter Event emitter used to execute JS module call * @param type type of the touch event (see {@link TouchEventType}) * @param reactTarget target view react id associated with this gesture - * @param androidMotionEvent native touch event to read pointers count and coordinates from + * @param touchEvent native touch event to read pointers count and coordinates from */ public static void sendTouchEvent( RCTEventEmitter rctEventEmitter, TouchEventType type, int reactTarget, - MotionEvent androidMotionEvent) { + TouchEvent touchEvent) { - WritableArray pointers = createsPointersArray(reactTarget, androidMotionEvent); + WritableArray pointers = createsPointersArray(reactTarget, 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 < androidMotionEvent.getPointerCount(); i++) { + for (int i = 0; i < motionEvent.getPointerCount(); i++) { changedIndices.pushInt(i); } } else if (type == TouchEventType.START || type == TouchEventType.END) { - changedIndices.pushInt(androidMotionEvent.getActionIndex()); + changedIndices.pushInt(motionEvent.getActionIndex()); } else { throw new RuntimeException("Unknown touch type: " + type); }