mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Emit non-hover pointer events on Android (#33526)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/33526 Changelog: [Internal] Experimental pointer event dispatching on Android: touch pointer support only, hover events to come later. Reviewed By: vincentriemer Differential Revision: D34132667 fbshipit-source-id: 6506d43b4ad16e11b10c3cd23e0231428209411f
This commit is contained in:
committed by
Facebook GitHub Bot
parent
ceae48c0f7
commit
c50e6b52fe
@@ -53,6 +53,7 @@ import com.facebook.react.modules.deviceinfo.DeviceInfoModule;
|
||||
import com.facebook.react.surface.ReactStage;
|
||||
import com.facebook.react.uimanager.DisplayMetricsHolder;
|
||||
import com.facebook.react.uimanager.IllegalViewOperationException;
|
||||
import com.facebook.react.uimanager.JSPointerDispatcher;
|
||||
import com.facebook.react.uimanager.JSTouchDispatcher;
|
||||
import com.facebook.react.uimanager.PixelUtil;
|
||||
import com.facebook.react.uimanager.ReactClippingProhibitedView;
|
||||
@@ -97,6 +98,7 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot {
|
||||
private boolean mIsAttachedToInstance;
|
||||
private boolean mShouldLogContentAppeared;
|
||||
private @Nullable JSTouchDispatcher mJSTouchDispatcher;
|
||||
private @Nullable JSPointerDispatcher mJSPointerDispatcher;
|
||||
private final ReactAndroidHWInputDeviceHelper mAndroidHWInputDeviceHelper =
|
||||
new ReactAndroidHWInputDeviceHelper(this);
|
||||
private boolean mWasMeasured = false;
|
||||
@@ -188,6 +190,11 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot {
|
||||
|
||||
@Override
|
||||
public void onChildStartedNativeGesture(MotionEvent ev) {
|
||||
onChildStartedNativeGesture(null, ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildStartedNativeGesture(View childView, MotionEvent ev) {
|
||||
if (!isDispatcherReady()) {
|
||||
return;
|
||||
}
|
||||
@@ -197,14 +204,12 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot {
|
||||
if (uiManager != null) {
|
||||
EventDispatcher eventDispatcher = uiManager.getEventDispatcher();
|
||||
mJSTouchDispatcher.onChildStartedNativeGesture(ev, eventDispatcher);
|
||||
if (childView != null && mJSPointerDispatcher != null) {
|
||||
mJSPointerDispatcher.onChildStartedNativeGesture(childView, ev, eventDispatcher);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildStartedNativeGesture(View childView, MotionEvent ev) {
|
||||
onChildStartedNativeGesture(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildEndedNativeGesture(View childView, MotionEvent ev) {
|
||||
if (!isDispatcherReady()) {
|
||||
@@ -216,6 +221,9 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot {
|
||||
if (uiManager != null) {
|
||||
EventDispatcher eventDispatcher = uiManager.getEventDispatcher();
|
||||
mJSTouchDispatcher.onChildEndedNativeGesture(ev, eventDispatcher);
|
||||
if (mJSPointerDispatcher != null) {
|
||||
mJSPointerDispatcher.onChildEndedNativeGesture();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +238,10 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot {
|
||||
FLog.w(TAG, "Unable to dispatch touch to JS before the dispatcher is available");
|
||||
return false;
|
||||
}
|
||||
if (ReactFeatureFlags.dispatchPointerEvents && mJSPointerDispatcher == null) {
|
||||
FLog.w(TAG, "Unable to dispatch pointer events to JS before the dispatcher is available");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -245,6 +257,7 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot {
|
||||
if (shouldDispatchJSTouchEvent(ev)) {
|
||||
dispatchJSTouchEvent(ev);
|
||||
}
|
||||
dispatchJSPointerEvent(ev);
|
||||
return super.onInterceptTouchEvent(ev);
|
||||
}
|
||||
|
||||
@@ -253,6 +266,7 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot {
|
||||
if (shouldDispatchJSTouchEvent(ev)) {
|
||||
dispatchJSTouchEvent(ev);
|
||||
}
|
||||
dispatchJSPointerEvent(ev);
|
||||
super.onTouchEvent(ev);
|
||||
// In case when there is no children interested in handling touch event, we return true from
|
||||
// the root view in order to receive subsequent events related to that gesture
|
||||
@@ -312,6 +326,29 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot {
|
||||
super.requestChildFocus(child, focused);
|
||||
}
|
||||
|
||||
private void dispatchJSPointerEvent(MotionEvent event) {
|
||||
if (mReactInstanceManager == null
|
||||
|| !mIsAttachedToInstance
|
||||
|| mReactInstanceManager.getCurrentReactContext() == null) {
|
||||
FLog.w(TAG, "Unable to dispatch touch to JS as the catalyst instance has not been attached");
|
||||
return;
|
||||
}
|
||||
if (mJSPointerDispatcher == null) {
|
||||
if (!ReactFeatureFlags.dispatchPointerEvents) {
|
||||
return;
|
||||
}
|
||||
FLog.w(TAG, "Unable to dispatch pointer events to JS before the dispatcher is available");
|
||||
return;
|
||||
}
|
||||
ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
|
||||
UIManager uiManager = UIManagerHelper.getUIManager(reactContext, getUIManagerType());
|
||||
|
||||
if (uiManager != null) {
|
||||
EventDispatcher eventDispatcher = uiManager.getEventDispatcher();
|
||||
mJSPointerDispatcher.handleMotionEvent(event, eventDispatcher);
|
||||
}
|
||||
}
|
||||
|
||||
private void dispatchJSTouchEvent(MotionEvent event) {
|
||||
if (mReactInstanceManager == null
|
||||
|| !mIsAttachedToInstance
|
||||
@@ -629,6 +666,11 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot {
|
||||
// them. Otherwise, these events might break the states expected by JS.
|
||||
// Note that this callback was invoked from within the UI thread.
|
||||
mJSTouchDispatcher = new JSTouchDispatcher(this);
|
||||
|
||||
if (ReactFeatureFlags.dispatchPointerEvents) {
|
||||
mJSPointerDispatcher = new JSPointerDispatcher(this);
|
||||
}
|
||||
|
||||
if (mRootViewEventListener != null) {
|
||||
mRootViewEventListener.onAttachedToReactInstance(this);
|
||||
}
|
||||
@@ -709,6 +751,9 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot {
|
||||
/* package */ void simulateAttachForTesting() {
|
||||
mIsAttachedToInstance = true;
|
||||
mJSTouchDispatcher = new JSTouchDispatcher(this);
|
||||
if (ReactFeatureFlags.dispatchPointerEvents) {
|
||||
mJSPointerDispatcher = new JSPointerDispatcher(this);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
||||
@@ -112,4 +112,6 @@ public class ReactFeatureFlags {
|
||||
|
||||
/** TODO: T113245006 Delete this flag. Enables caching of spannables for text */
|
||||
public static boolean enableSpannableCache = false;
|
||||
|
||||
public static boolean dispatchPointerEvents = false;
|
||||
}
|
||||
|
||||
@@ -443,6 +443,61 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
|
||||
updateViewAccessibility(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
|
||||
Map<String, Object> baseEventTypeConstants = super.getExportedCustomDirectEventTypeConstants();
|
||||
Map<String, Object> eventTypeConstants =
|
||||
baseEventTypeConstants == null ? new HashMap<String, Object>() : baseEventTypeConstants;
|
||||
eventTypeConstants.putAll(
|
||||
MapBuilder.<String, Object>builder()
|
||||
.put(
|
||||
"topPointerCancel",
|
||||
MapBuilder.of(
|
||||
"phasedRegistrationNames",
|
||||
MapBuilder.of(
|
||||
"bubbled", "onPointerCancel", "captured", "onPointerCancelCapture")))
|
||||
.put(
|
||||
"topPointerDown",
|
||||
MapBuilder.of(
|
||||
"phasedRegistrationNames",
|
||||
MapBuilder.of("bubbled", "onPointerDown", "captured", "onPointerDownCapture")))
|
||||
.put(
|
||||
"topPointerEnter2",
|
||||
MapBuilder.of(
|
||||
"phasedRegistrationNames",
|
||||
MapBuilder.of(
|
||||
"bubbled",
|
||||
"onPointerEnter2",
|
||||
"captured",
|
||||
"onPointerEnter2Capture",
|
||||
"skipBubbling",
|
||||
true)))
|
||||
.put(
|
||||
"topPointerLeave2",
|
||||
MapBuilder.of(
|
||||
"phasedRegistrationNames",
|
||||
MapBuilder.of(
|
||||
"bubbled",
|
||||
"onPointerLeave2",
|
||||
"captured",
|
||||
"onPointerLeave2Capture",
|
||||
"skipBubbling",
|
||||
true)))
|
||||
.put(
|
||||
"topPointerMove2",
|
||||
MapBuilder.of(
|
||||
"phasedRegistrationNames",
|
||||
MapBuilder.of(
|
||||
"bubbled", "onPointerMove2", "captured", "onPointerMove2Capture")))
|
||||
.put(
|
||||
"topPointerUp",
|
||||
MapBuilder.of(
|
||||
"phasedRegistrationNames",
|
||||
MapBuilder.of("bubbled", "onPointerUp", "captured", "onPointerUpCapture")))
|
||||
.build());
|
||||
return eventTypeConstants;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Map<String, Object> getExportedCustomDirectEventTypeConstants() {
|
||||
@Nullable
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
import com.facebook.react.uimanager.events.EventDispatcher;
|
||||
import com.facebook.react.uimanager.events.PointerEvent;
|
||||
import com.facebook.react.uimanager.events.PointerEventHelper;
|
||||
import com.facebook.react.uimanager.events.TouchEvent;
|
||||
import com.facebook.react.uimanager.events.TouchEventCoalescingKeyHelper;
|
||||
|
||||
/**
|
||||
* JSPointerDispatcher handles dispatching pointer events to JS from RootViews. If you implement
|
||||
* RootView you need to call handleMotionEvent from onTouchEvent, onInterceptTouchEvent,
|
||||
* onHoverEvent, onInterceptHoverEvent. It will correctly find the right view to handle the touch
|
||||
* and also dispatch the appropriate event to JS
|
||||
*/
|
||||
public class JSPointerDispatcher {
|
||||
|
||||
private final float[] mTargetCoordinates = new float[2];
|
||||
private int mChildHandlingNativeGesture = -1;
|
||||
private long mDownStartTime = TouchEvent.UNSET;
|
||||
private final ViewGroup mRootViewGroup;
|
||||
private final TouchEventCoalescingKeyHelper mTouchEventCoalescingKeyHelper =
|
||||
new TouchEventCoalescingKeyHelper();
|
||||
|
||||
public JSPointerDispatcher(ViewGroup viewGroup) {
|
||||
mRootViewGroup = viewGroup;
|
||||
}
|
||||
|
||||
public void onChildStartedNativeGesture(
|
||||
View childView, MotionEvent motionEvent, EventDispatcher eventDispatcher) {
|
||||
if (mChildHandlingNativeGesture != -1 || childView == null) {
|
||||
// This means we previously had another child start handling this native gesture and now a
|
||||
// different native parent of that child has decided to intercept the touch stream and handle
|
||||
// the gesture itself. Example where this can happen: HorizontalScrollView in a ScrollView.
|
||||
return;
|
||||
}
|
||||
|
||||
int targetTag = findTargetTagAndSetCoordinates(motionEvent);
|
||||
dispatchCancelEvent(targetTag, motionEvent, eventDispatcher);
|
||||
mChildHandlingNativeGesture = childView.getId();
|
||||
}
|
||||
|
||||
public void onChildEndedNativeGesture() {
|
||||
// There should be only one child gesture at any given time. We can safely turn off the flag.
|
||||
mChildHandlingNativeGesture = -1;
|
||||
}
|
||||
|
||||
public void handleMotionEvent(MotionEvent motionEvent, EventDispatcher eventDispatcher) {
|
||||
|
||||
// Ignore if child is handling native gesture
|
||||
if (mChildHandlingNativeGesture != -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean supportsHover =
|
||||
PointerEventHelper.supportsHover(motionEvent.getToolType(motionEvent.getActionIndex()));
|
||||
|
||||
int surfaceId = UIManagerHelper.getSurfaceId(mRootViewGroup);
|
||||
int action = motionEvent.getActionMasked();
|
||||
int targetTag = findTargetTagAndSetCoordinates(motionEvent);
|
||||
|
||||
// First down pointer
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
|
||||
// Start a "down" coalescing key
|
||||
mDownStartTime = motionEvent.getEventTime();
|
||||
mTouchEventCoalescingKeyHelper.addCoalescingKey(mDownStartTime);
|
||||
|
||||
if (!supportsHover) {
|
||||
eventDispatcher.dispatchEvent(
|
||||
PointerEvent.obtain(
|
||||
PointerEventHelper.POINTER_ENTER, surfaceId, targetTag, motionEvent));
|
||||
}
|
||||
eventDispatcher.dispatchEvent(
|
||||
PointerEvent.obtain(PointerEventHelper.POINTER_DOWN, surfaceId, targetTag, motionEvent));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// New pointer goes down, this can only happen after ACTION_DOWN is sent for the first pointer
|
||||
if (action == MotionEvent.ACTION_POINTER_DOWN) {
|
||||
mTouchEventCoalescingKeyHelper.incrementCoalescingKey(mDownStartTime);
|
||||
|
||||
if (!supportsHover) {
|
||||
eventDispatcher.dispatchEvent(
|
||||
PointerEvent.obtain(
|
||||
PointerEventHelper.POINTER_ENTER, surfaceId, targetTag, motionEvent));
|
||||
}
|
||||
eventDispatcher.dispatchEvent(
|
||||
PointerEvent.obtain(PointerEventHelper.POINTER_DOWN, surfaceId, targetTag, motionEvent));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (action == MotionEvent.ACTION_MOVE) {
|
||||
int coalescingKey = mTouchEventCoalescingKeyHelper.getCoalescingKey(mDownStartTime);
|
||||
eventDispatcher.dispatchEvent(
|
||||
PointerEvent.obtain(
|
||||
PointerEventHelper.POINTER_MOVE, surfaceId, targetTag, motionEvent, coalescingKey));
|
||||
return;
|
||||
}
|
||||
|
||||
// Exactly one of the pointers goes up, not the last one
|
||||
if (action == MotionEvent.ACTION_POINTER_UP) {
|
||||
mTouchEventCoalescingKeyHelper.incrementCoalescingKey(mDownStartTime);
|
||||
eventDispatcher.dispatchEvent(
|
||||
PointerEvent.obtain(PointerEventHelper.POINTER_UP, surfaceId, targetTag, motionEvent));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Last pointer comes up
|
||||
if (action == MotionEvent.ACTION_UP) {
|
||||
|
||||
// End of a "down" coalescing key
|
||||
mTouchEventCoalescingKeyHelper.removeCoalescingKey(mDownStartTime);
|
||||
mDownStartTime = TouchEvent.UNSET;
|
||||
|
||||
eventDispatcher.dispatchEvent(
|
||||
PointerEvent.obtain(PointerEventHelper.POINTER_UP, surfaceId, targetTag, motionEvent));
|
||||
|
||||
if (!supportsHover) {
|
||||
eventDispatcher.dispatchEvent(
|
||||
PointerEvent.obtain(
|
||||
PointerEventHelper.POINTER_LEAVE, surfaceId, targetTag, motionEvent));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (action == MotionEvent.ACTION_CANCEL) {
|
||||
dispatchCancelEvent(targetTag, motionEvent, eventDispatcher);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
FLog.w(
|
||||
ReactConstants.TAG,
|
||||
"Warning : Motion Event was ignored. Action="
|
||||
+ action
|
||||
+ " Target="
|
||||
+ targetTag
|
||||
+ " Supports Hover="
|
||||
+ supportsHover);
|
||||
}
|
||||
|
||||
private int findTargetTagAndSetCoordinates(MotionEvent ev) {
|
||||
// This method updates `mTargetCoordinates` with coordinates for the motion event.
|
||||
return TouchTargetHelper.findTargetTagAndCoordinatesForTouch(
|
||||
ev.getX(), ev.getY(), mRootViewGroup, mTargetCoordinates, null);
|
||||
}
|
||||
|
||||
private void dispatchCancelEvent(
|
||||
int targetTag, MotionEvent motionEvent, EventDispatcher eventDispatcher) {
|
||||
// This means the gesture has already ended, via some other CANCEL or UP event. This is not
|
||||
// expected to happen very often as it would mean some child View has decided to intercept the
|
||||
// touch stream and start a native gesture only upon receiving the UP/CANCEL event.
|
||||
|
||||
Assertions.assertCondition(
|
||||
mChildHandlingNativeGesture == -1,
|
||||
"Expected to not have already sent a cancel for this gesture");
|
||||
int surfaceId = UIManagerHelper.getSurfaceId(mRootViewGroup);
|
||||
|
||||
Assertions.assertNotNull(eventDispatcher)
|
||||
.dispatchEvent(
|
||||
PointerEvent.obtain(
|
||||
PointerEventHelper.POINTER_CANCEL, surfaceId, targetTag, motionEvent));
|
||||
|
||||
eventDispatcher.dispatchEvent(
|
||||
PointerEvent.obtain(PointerEventHelper.POINTER_LEAVE, surfaceId, targetTag, motionEvent));
|
||||
|
||||
mTouchEventCoalescingKeyHelper.removeCoalescingKey(mDownStartTime);
|
||||
mDownStartTime = TouchEvent.UNSET;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* 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 androidx.annotation.Nullable;
|
||||
import androidx.core.util.Pools;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactSoftExceptionLogger;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class PointerEvent extends Event<PointerEvent> {
|
||||
private static final String TAG = PointerEvent.class.getSimpleName();
|
||||
private static final int POINTER_EVENTS_POOL_SIZE = 6;
|
||||
private static final Pools.SynchronizedPool<PointerEvent> EVENTS_POOL =
|
||||
new Pools.SynchronizedPool<>(POINTER_EVENTS_POOL_SIZE);
|
||||
private static final int UNSET_COALESCING_KEY = -1;
|
||||
|
||||
public static PointerEvent obtain(
|
||||
String eventName, int surfaceId, int viewTag, MotionEvent motionEventToCopy) {
|
||||
PointerEvent event = EVENTS_POOL.acquire();
|
||||
if (event == null) {
|
||||
event = new PointerEvent();
|
||||
}
|
||||
event.init(eventName, surfaceId, viewTag, Assertions.assertNotNull(motionEventToCopy), 0);
|
||||
return event;
|
||||
}
|
||||
|
||||
public static PointerEvent obtain(
|
||||
String eventName,
|
||||
int surfaceId,
|
||||
int viewTag,
|
||||
MotionEvent motionEventToCopy,
|
||||
int coalescingKey) {
|
||||
PointerEvent event = EVENTS_POOL.acquire();
|
||||
if (event == null) {
|
||||
event = new PointerEvent();
|
||||
}
|
||||
event.init(
|
||||
eventName, surfaceId, viewTag, Assertions.assertNotNull(motionEventToCopy), coalescingKey);
|
||||
return event;
|
||||
}
|
||||
|
||||
private @Nullable MotionEvent mMotionEvent;
|
||||
private @Nullable String mEventName;
|
||||
private int mCoalescingKey = UNSET_COALESCING_KEY;
|
||||
|
||||
private void init(
|
||||
String eventName,
|
||||
int surfaceId,
|
||||
int viewTag,
|
||||
MotionEvent motionEventToCopy,
|
||||
int coalescingKey) {
|
||||
super.init(surfaceId, viewTag, motionEventToCopy.getEventTime());
|
||||
mEventName = eventName;
|
||||
mMotionEvent = MotionEvent.obtain(motionEventToCopy);
|
||||
mCoalescingKey = coalescingKey;
|
||||
}
|
||||
|
||||
private PointerEvent() {}
|
||||
|
||||
@Override
|
||||
public String getEventName() {
|
||||
return mEventName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispatch(RCTEventEmitter rctEventEmitter) {
|
||||
// Skip legacy stuff for now?
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDispose() {
|
||||
MotionEvent motionEvent = mMotionEvent;
|
||||
mMotionEvent = null;
|
||||
if (motionEvent != null) {
|
||||
motionEvent.recycle();
|
||||
}
|
||||
|
||||
// Either `this` is in the event pool, or motionEvent
|
||||
// is null. It is in theory not possible for a PointerEvent to
|
||||
// be in the EVENTS_POOL but for motionEvent to be null. However,
|
||||
// out of an abundance of caution and to avoid memory leaks or
|
||||
// other crashes at all costs, we attempt to release here and log
|
||||
// a soft exception here if release throws an IllegalStateException
|
||||
// due to `this` being over-released. This may indicate that there is
|
||||
// a logic error in our events system or pooling mechanism.
|
||||
try {
|
||||
EVENTS_POOL.release(this);
|
||||
} catch (IllegalStateException e) {
|
||||
ReactSoftExceptionLogger.logSoftException(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
private ArrayList<WritableMap> createPointerEvents() {
|
||||
MotionEvent motionEvent = mMotionEvent;
|
||||
ArrayList<WritableMap> pointerEvents = new ArrayList<>();
|
||||
|
||||
for (int index = 0; index < motionEvent.getPointerCount(); index++) {
|
||||
pointerEvents.add(this.createPointerEvent(index));
|
||||
}
|
||||
|
||||
return pointerEvents;
|
||||
}
|
||||
|
||||
private WritableMap createPointerEvent(int index) {
|
||||
WritableMap pointerEvent = Arguments.createMap();
|
||||
|
||||
pointerEvent.putDouble("pointerId", mMotionEvent.getPointerId(index));
|
||||
pointerEvent.putDouble("pressure", mMotionEvent.getPressure(index));
|
||||
pointerEvent.putString(
|
||||
"pointerType", PointerEventHelper.getW3CPointerType(mMotionEvent.getToolType(index)));
|
||||
|
||||
// Client refers to upper left edge of the content area (viewport)
|
||||
// We define the viewport to be ReactRootView
|
||||
pointerEvent.putDouble("clientX", mMotionEvent.getX(index));
|
||||
pointerEvent.putDouble("clientY", mMotionEvent.getY(index));
|
||||
|
||||
pointerEvent.putInt("target", this.getViewTag());
|
||||
pointerEvent.putDouble("timestamp", this.getTimestampMs());
|
||||
return pointerEvent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispatchModern(RCTModernEventEmitter rctEventEmitter) {
|
||||
if (mMotionEvent == null) {
|
||||
ReactSoftExceptionLogger.logSoftException(
|
||||
TAG,
|
||||
new IllegalStateException(
|
||||
"Cannot dispatch a Pointer that has no MotionEvent; the PointerEvehas been recycled"));
|
||||
return;
|
||||
}
|
||||
|
||||
List<WritableMap> relevantPointerEventData = null;
|
||||
|
||||
int activePointerIndex = mMotionEvent.getActionIndex();
|
||||
switch (mEventName) {
|
||||
// Cases where all pointer info is relevant
|
||||
case PointerEventHelper.POINTER_MOVE:
|
||||
case PointerEventHelper.POINTER_CANCEL:
|
||||
relevantPointerEventData = createPointerEvents();
|
||||
break;
|
||||
// Cases where only the "active" pointer info is relevant
|
||||
case PointerEventHelper.POINTER_ENTER:
|
||||
case PointerEventHelper.POINTER_DOWN:
|
||||
case PointerEventHelper.POINTER_UP:
|
||||
case PointerEventHelper.POINTER_LEAVE:
|
||||
relevantPointerEventData = Arrays.asList(createPointerEvent(activePointerIndex));
|
||||
break;
|
||||
}
|
||||
|
||||
if (relevantPointerEventData == null) {
|
||||
// No relevant MotionEvent to dispatch
|
||||
return;
|
||||
}
|
||||
|
||||
boolean shouldCopy = relevantPointerEventData.size() > 1;
|
||||
for (WritableMap pointerEventData : relevantPointerEventData) {
|
||||
WritableMap eventData = shouldCopy ? pointerEventData.copy() : pointerEventData;
|
||||
rctEventEmitter.receiveEvent(
|
||||
this.getSurfaceId(),
|
||||
this.getViewTag(),
|
||||
mEventName,
|
||||
mCoalescingKey != UNSET_COALESCING_KEY,
|
||||
mCoalescingKey,
|
||||
eventData,
|
||||
PointerEventHelper.getEventCategory(mEventName));
|
||||
}
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/** Class responsible for generating catalyst touch events based on android {@link MotionEvent}. */
|
||||
public class PointerEventHelper {
|
||||
|
||||
public static final String POINTER_TYPE_TOUCH = "touch";
|
||||
public static final String POINTER_TYPE_PEN = "pen";
|
||||
public static final String POINTER_TYPE_MOUSE = "mouse";
|
||||
public static final String POINTER_TYPE_UNKNOWN = "";
|
||||
|
||||
public static final String POINTER_CANCEL = "topPointerCancel";
|
||||
public static final String POINTER_DOWN = "topPointerDown";
|
||||
public static final String POINTER_ENTER = "topPointerEnter2";
|
||||
public static final String POINTER_LEAVE = "topPointerLeave2";
|
||||
public static final String POINTER_MOVE = "topPointerMove2";
|
||||
public static final String POINTER_UP = "topPointerUp";
|
||||
|
||||
public static String getW3CPointerType(final int toolType) {
|
||||
// https://www.w3.org/TR/pointerevents3/#dom-pointerevent-pointertype
|
||||
switch (toolType) {
|
||||
case MotionEvent.TOOL_TYPE_FINGER:
|
||||
return POINTER_TYPE_TOUCH;
|
||||
|
||||
case MotionEvent.TOOL_TYPE_STYLUS:
|
||||
return POINTER_TYPE_PEN;
|
||||
|
||||
case MotionEvent.TOOL_TYPE_MOUSE:
|
||||
return POINTER_TYPE_MOUSE;
|
||||
}
|
||||
return POINTER_TYPE_UNKNOWN;
|
||||
}
|
||||
|
||||
public static int getEventCategory(String pointerEventType) {
|
||||
if (pointerEventType == null) {
|
||||
return EventCategoryDef.UNSPECIFIED;
|
||||
}
|
||||
// Following:
|
||||
// https://github.com/facebook/react/blob/main/packages/react-dom/src/events/ReactDOMEventListener.js#L435-L437
|
||||
switch (pointerEventType) {
|
||||
case POINTER_DOWN:
|
||||
case POINTER_CANCEL:
|
||||
case POINTER_UP:
|
||||
return EventCategoryDef.DISCRETE;
|
||||
case POINTER_MOVE:
|
||||
case POINTER_ENTER:
|
||||
case POINTER_LEAVE:
|
||||
return EventCategoryDef.CONTINUOUS;
|
||||
}
|
||||
|
||||
return EventCategoryDef.UNSPECIFIED;
|
||||
}
|
||||
|
||||
public static boolean supportsHover(final int toolType) {
|
||||
String pointerType = getW3CPointerType(toolType);
|
||||
|
||||
if (pointerType.equals(POINTER_TYPE_MOUSE)) {
|
||||
return true;
|
||||
} else if (pointerType.equals(POINTER_TYPE_PEN)) {
|
||||
return true; // true?
|
||||
} else if (pointerType.equals(POINTER_TYPE_TOUCH)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ rn_android_library(
|
||||
react_native_root_target(":generated_components_java-FBReactNativeComponentSpec"),
|
||||
react_native_target("java/com/facebook/react/bridge:bridge"),
|
||||
react_native_target("java/com/facebook/react/common:common"),
|
||||
react_native_target("java/com/facebook/react/config:config"),
|
||||
react_native_target("java/com/facebook/react/module/annotations:annotations"),
|
||||
react_native_target("java/com/facebook/react/touch:touch"),
|
||||
react_native_target("java/com/facebook/react/uimanager:uimanager"),
|
||||
|
||||
@@ -34,7 +34,9 @@ import com.facebook.react.bridge.UiThreadUtil;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableNativeMap;
|
||||
import com.facebook.react.common.annotations.VisibleForTesting;
|
||||
import com.facebook.react.config.ReactFeatureFlags;
|
||||
import com.facebook.react.uimanager.FabricViewStateManager;
|
||||
import com.facebook.react.uimanager.JSPointerDispatcher;
|
||||
import com.facebook.react.uimanager.JSTouchDispatcher;
|
||||
import com.facebook.react.uimanager.PixelUtil;
|
||||
import com.facebook.react.uimanager.RootView;
|
||||
@@ -413,9 +415,13 @@ public class ReactModalHostView extends ViewGroup
|
||||
private final FabricViewStateManager mFabricViewStateManager = new FabricViewStateManager();
|
||||
|
||||
private final JSTouchDispatcher mJSTouchDispatcher = new JSTouchDispatcher(this);
|
||||
@Nullable private JSPointerDispatcher mJSPointerDispatcher;
|
||||
|
||||
public DialogRootViewGroup(Context context) {
|
||||
super(context);
|
||||
if (ReactFeatureFlags.dispatchPointerEvents) {
|
||||
mJSPointerDispatcher = new JSPointerDispatcher(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void setEventDispatcher(EventDispatcher eventDispatcher) {
|
||||
@@ -515,12 +521,18 @@ public class ReactModalHostView extends ViewGroup
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent event) {
|
||||
mJSTouchDispatcher.handleTouchEvent(event, mEventDispatcher);
|
||||
if (mJSPointerDispatcher != null) {
|
||||
mJSPointerDispatcher.handleMotionEvent(event, mEventDispatcher);
|
||||
}
|
||||
return super.onInterceptTouchEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
mJSTouchDispatcher.handleTouchEvent(event, mEventDispatcher);
|
||||
if (mJSPointerDispatcher != null) {
|
||||
mJSPointerDispatcher.handleMotionEvent(event, mEventDispatcher);
|
||||
}
|
||||
super.onTouchEvent(event);
|
||||
// In case when there is no children interested in handling touch event, we return true from
|
||||
// the root view in order to receive subsequent events related to that gesture
|
||||
@@ -529,17 +541,23 @@ public class ReactModalHostView extends ViewGroup
|
||||
|
||||
@Override
|
||||
public void onChildStartedNativeGesture(MotionEvent ev) {
|
||||
mJSTouchDispatcher.onChildStartedNativeGesture(ev, mEventDispatcher);
|
||||
this.onChildStartedNativeGesture(null, ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildStartedNativeGesture(View childView, MotionEvent ev) {
|
||||
mJSTouchDispatcher.onChildStartedNativeGesture(ev, mEventDispatcher);
|
||||
if (mJSPointerDispatcher != null) {
|
||||
mJSPointerDispatcher.onChildStartedNativeGesture(childView, ev, mEventDispatcher);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildEndedNativeGesture(View childView, MotionEvent ev) {
|
||||
mJSTouchDispatcher.onChildEndedNativeGesture(ev, mEventDispatcher);
|
||||
if (mJSPointerDispatcher != null) {
|
||||
mJSPointerDispatcher.onChildEndedNativeGesture();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user