diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index dc0828689db..a7970346275 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -700,6 +700,16 @@ public abstract class BaseViewManager> mLastHitPathByPointerId; private Map mLastEventCoordinatesByPointerId; + private Map> mCurrentlyDownPointerIdsToHitPath; private Set mHoveringPointerIds = new HashSet<>(); private int mChildHandlingNativeGesture = -1; @@ -53,6 +54,7 @@ public class JSPointerDispatcher { public JSPointerDispatcher(ViewGroup viewGroup) { mRootViewGroup = viewGroup; + mCurrentlyDownPointerIdsToHitPath = new HashMap<>(); } public void onChildStartedNativeGesture( @@ -87,6 +89,30 @@ public class JSPointerDispatcher { mChildHandlingNativeGesture = -1; } + // returns the section of the hit path shared by both lists, or an empty list if there's no such + // section + private static List findHitPathIntersection( + final List hitsA, final List hitsB) { + if (hitsA.isEmpty()) { + return new ArrayList<>(); + } + if (hitsB.isEmpty()) { + return new ArrayList<>(); + } + + Set inA = new HashSet<>(hitsA); + + List intersection = new ArrayList<>(); + + for (final ViewTarget vt : hitsB) { + if (inA.contains(vt)) { + intersection.add(vt); + } + } + + return intersection; + } + private void onUp( int activeTargetTag, PointerEventState eventState, @@ -127,6 +153,18 @@ public class JSPointerDispatcher { eventDispatcher); } + List hitPathDown = mCurrentlyDownPointerIdsToHitPath.remove(activePointerId); + if (hitPathDown != null + && isAnyoneListeningForBubblingEvent(activeHitPath, EVENT.CLICK, EVENT.CLICK_CAPTURE)) { + List hitPathForClick = findHitPathIntersection(hitPathDown, activeHitPath); + if (!hitPathForClick.isEmpty()) { + final ViewTarget clickTarget = hitPathForClick.get(0); + eventDispatcher.dispatchEvent( + PointerEvent.obtain( + PointerEventHelper.CLICK, clickTarget.getViewId(), eventState, motionEvent)); + } + } + if (motionEvent.getActionMasked() == MotionEvent.ACTION_UP) { mPrimaryPointerId = UNSET_POINTER_ID; } @@ -175,6 +213,12 @@ public class JSPointerDispatcher { eventDispatcher); } + // store some information if we might need to emit a click later on + if (isAnyoneListeningForBubblingEvent(activeHitPath, EVENT.CLICK, EVENT.CLICK_CAPTURE)) { + mCurrentlyDownPointerIdsToHitPath.put( + eventState.getActivePointerId(), new ArrayList<>(activeHitPath)); + } + boolean listeningForDown = isAnyoneListeningForBubblingEvent(activeHitPath, EVENT.DOWN, EVENT.DOWN_CAPTURE); if (listeningForDown) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEvent.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEvent.java index 0e298829a55..02b99115070 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEvent.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEvent.java @@ -91,6 +91,10 @@ public class PointerEvent extends Event { return mEventName; } + private boolean isClickEvent() { + return mEventName.equals(PointerEventHelper.CLICK); + } + @Override public void dispatch(RCTEventEmitter rctEventEmitter) { if (mMotionEvent == null) { @@ -190,7 +194,8 @@ public class PointerEvent extends Event { pointerEvent.putString("pointerType", pointerType); boolean isPrimary = - mEventState.supportsHover(pointerId) || pointerId == mEventState.mPrimaryPointerId; + !isClickEvent() // compatibility click events should not be considered primary + && (mEventState.supportsHover(pointerId) || pointerId == mEventState.mPrimaryPointerId); pointerEvent.putBoolean("isPrimary", isPrimary); // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent @@ -223,8 +228,8 @@ public class PointerEvent extends Event { pointerEvent.putDouble("tiltY", 0); pointerEvent.putInt("twist", 0); - - if (pointerType.equals(PointerEventHelper.POINTER_TYPE_MOUSE)) { + // note: click events should have width = height = 1 + if (pointerType.equals(PointerEventHelper.POINTER_TYPE_MOUSE) || isClickEvent()) { pointerEvent.putDouble("width", 1); pointerEvent.putDouble("height", 1); } else { @@ -241,8 +246,12 @@ public class PointerEvent extends Event { pointerEvent.putInt( "buttons", PointerEventHelper.getButtons(mEventName, pointerType, buttonState)); - pointerEvent.putDouble( - "pressure", PointerEventHelper.getPressure(pointerEvent.getInt("buttons"), mEventName)); + final double pressure = + isClickEvent() // click events need pressure=0 + ? 0 + : PointerEventHelper.getPressure(pointerEvent.getInt("buttons"), mEventName); + + pointerEvent.putDouble("pressure", pressure); pointerEvent.putDouble("tangentialPressure", 0.0); return pointerEvent; @@ -264,6 +273,7 @@ public class PointerEvent extends Event { case PointerEventHelper.POINTER_LEAVE: case PointerEventHelper.POINTER_OUT: case PointerEventHelper.POINTER_OVER: + case PointerEventHelper.CLICK: pointersEventData = Arrays.asList(createW3CPointerEvent(activePointerIndex)); break; } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEventHelper.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEventHelper.java index 757d5f5dd4e..787c9ed117c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEventHelper.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEventHelper.java @@ -25,6 +25,8 @@ public class PointerEventHelper { public static enum EVENT { CANCEL, CANCEL_CAPTURE, + CLICK, + CLICK_CAPTURE, DOWN, DOWN_CAPTURE, ENTER, @@ -49,6 +51,7 @@ public class PointerEventHelper { public static final String POINTER_UP = "topPointerUp"; public static final String POINTER_OVER = "topPointerOver"; public static final String POINTER_OUT = "topPointerOut"; + public static final String CLICK = "topClick"; // https://w3c.github.io/pointerevents/#the-buttons-property public static int getButtons(String eventName, String pointerType, int buttonState) { @@ -117,6 +120,8 @@ public class PointerEventHelper { case UP_CAPTURE: case CANCEL: case CANCEL_CAPTURE: + case CLICK: + case CLICK_CAPTURE: return true; }