mirror of
https://github.com/status-im/react-native.git
synced 2025-02-26 08:05:34 +00:00
Refactor JS touch dispatch from ReactRootView
Reviewed By: andreicoman11 Differential Revision: D3035589 fb-gh-sync-id: a4af7b0be89e5ea13367481697ff4b3dda8536b1 shipit-source-id: a4af7b0be89e5ea13367481697ff4b3dda8536b1
This commit is contained in:
parent
e674e45c2e
commit
39fdce259d
@ -27,28 +27,26 @@ import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.UiThreadUtil;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
import com.facebook.react.common.SystemClock;
|
||||
import com.facebook.react.common.annotations.VisibleForTesting;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
import com.facebook.react.uimanager.DisplayMetricsHolder;
|
||||
import com.facebook.react.uimanager.PixelUtil;
|
||||
import com.facebook.react.uimanager.RootView;
|
||||
import com.facebook.react.uimanager.SizeMonitoringFrameLayout;
|
||||
import com.facebook.react.uimanager.TouchTargetHelper;
|
||||
import com.facebook.react.uimanager.JSTouchDispatcher;
|
||||
import com.facebook.react.uimanager.UIManagerModule;
|
||||
import com.facebook.react.uimanager.events.EventDispatcher;
|
||||
import com.facebook.react.uimanager.events.TouchEvent;
|
||||
import com.facebook.react.uimanager.events.TouchEventType;
|
||||
|
||||
/**
|
||||
* Default root view for catalyst apps. Provides the ability to listen for size changes so that a UI
|
||||
* manager can re-layout its elements.
|
||||
* It is also responsible for handling touch events passed to any of it's child view's and sending
|
||||
* those events to JS via RCTEventEmitter module. This view is overriding
|
||||
* {@link ViewGroup#onInterceptTouchEvent} method in order to be notified about the events for all
|
||||
* of it's children and it's also overriding {@link ViewGroup#requestDisallowInterceptTouchEvent}
|
||||
* to make sure that {@link ViewGroup#onInterceptTouchEvent} will get events even when some child
|
||||
* view start intercepting it. In case when no child view is interested in handling some particular
|
||||
* It delegates handling touch events for itself and child views and sending those events to JS by
|
||||
* using JSTouchDispatcher.
|
||||
* This view is overriding {@link ViewGroup#onInterceptTouchEvent} method in order to be notified
|
||||
* about the events for all of it's children and it's also overriding
|
||||
* {@link ViewGroup#requestDisallowInterceptTouchEvent} to make sure that
|
||||
* {@link ViewGroup#onInterceptTouchEvent} will get events even when some child view start
|
||||
* intercepting it. In case when no child view is interested in handling some particular
|
||||
* touch event this view's {@link View#onTouchEvent} will still return true in order to be notified
|
||||
* about all subsequent touch events related to that gesture (in case when JS code want to handle
|
||||
* that gesture).
|
||||
@ -59,13 +57,11 @@ public class ReactRootView extends SizeMonitoringFrameLayout implements RootView
|
||||
private @Nullable String mJSModuleName;
|
||||
private @Nullable Bundle mLaunchOptions;
|
||||
private @Nullable KeyboardListener mKeyboardListener;
|
||||
private int mTargetTag = -1;
|
||||
private final float[] mTargetCoordinates = new float[2];
|
||||
private boolean mChildIsHandlingNativeGesture = false;
|
||||
private boolean mWasMeasured = false;
|
||||
private boolean mAttachScheduled = false;
|
||||
private boolean mIsAttachedToWindow = false;
|
||||
private boolean mIsAttachedToInstance = false;
|
||||
private final JSTouchDispatcher mJSTouchDispatcher = new JSTouchDispatcher(this);
|
||||
|
||||
public ReactRootView(Context context) {
|
||||
super(context);
|
||||
@ -112,171 +108,50 @@ public class ReactRootView extends SizeMonitoringFrameLayout implements RootView
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main catalyst view is responsible for collecting and sending touch events to JS. This method
|
||||
* reacts for an incoming android native touch events ({@link MotionEvent}) and calls into
|
||||
* {@link com.facebook.react.uimanager.events.EventDispatcher} when appropriate.
|
||||
* It uses {@link com.facebook.react.uimanager.TouchTargetManagerHelper#findTouchTargetView}
|
||||
* helper method for figuring out a react view ID in the case of ACTION_DOWN
|
||||
* event (when the gesture starts).
|
||||
*/
|
||||
private void handleTouchEvent(MotionEvent ev) {
|
||||
if (mReactInstanceManager == null || !mIsAttachedToInstance ||
|
||||
mReactInstanceManager.getCurrentReactContext() == null) {
|
||||
FLog.w(
|
||||
ReactConstants.TAG,
|
||||
"Unable to handle touch in JS as the catalyst instance has not been attached");
|
||||
return;
|
||||
}
|
||||
int action = ev.getAction() & MotionEvent.ACTION_MASK;
|
||||
ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
|
||||
EventDispatcher eventDispatcher = reactContext.getNativeModule(UIManagerModule.class)
|
||||
.getEventDispatcher();
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
if (mTargetTag != -1) {
|
||||
FLog.e(
|
||||
ReactConstants.TAG,
|
||||
"Got DOWN touch before receiving UP or CANCEL from last gesture");
|
||||
}
|
||||
|
||||
// First event for this gesture. We expect tag to be set to -1, and we use helper method
|
||||
// {@link #findTargetTagForTouch} to find react view ID that will be responsible for handling
|
||||
// this gesture
|
||||
mChildIsHandlingNativeGesture = false;
|
||||
mTargetTag = TouchTargetHelper.findTargetTagAndCoordinatesForTouch(
|
||||
ev.getX(),
|
||||
ev.getY(),
|
||||
this,
|
||||
mTargetCoordinates);
|
||||
eventDispatcher.dispatchEvent(
|
||||
TouchEvent.obtain(
|
||||
mTargetTag,
|
||||
SystemClock.nanoTime(),
|
||||
TouchEventType.START,
|
||||
ev,
|
||||
mTargetCoordinates[0],
|
||||
mTargetCoordinates[1]));
|
||||
} 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.
|
||||
return;
|
||||
} else if (mTargetTag == -1) {
|
||||
// All the subsequent action types are expected to be called after ACTION_DOWN thus target
|
||||
// is supposed to be set for them.
|
||||
FLog.e(
|
||||
ReactConstants.TAG,
|
||||
"Unexpected state: received touch event but didn't get starting ACTION_DOWN for this " +
|
||||
"gesture before");
|
||||
} else if (action == MotionEvent.ACTION_UP) {
|
||||
// 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.nanoTime(),
|
||||
TouchEventType.END,
|
||||
ev,
|
||||
mTargetCoordinates[0],
|
||||
mTargetCoordinates[1]));
|
||||
mTargetTag = -1;
|
||||
} else if (action == MotionEvent.ACTION_MOVE) {
|
||||
// Update pointer position for current gesture
|
||||
eventDispatcher.dispatchEvent(
|
||||
TouchEvent.obtain(
|
||||
mTargetTag,
|
||||
SystemClock.nanoTime(),
|
||||
TouchEventType.MOVE,
|
||||
ev,
|
||||
mTargetCoordinates[0],
|
||||
mTargetCoordinates[1]));
|
||||
} 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.nanoTime(),
|
||||
TouchEventType.START,
|
||||
ev,
|
||||
mTargetCoordinates[0],
|
||||
mTargetCoordinates[1]));
|
||||
} else if (action == MotionEvent.ACTION_POINTER_UP) {
|
||||
// Exactly onw of the pointers goes up
|
||||
eventDispatcher.dispatchEvent(
|
||||
TouchEvent.obtain(
|
||||
mTargetTag,
|
||||
SystemClock.nanoTime(),
|
||||
TouchEventType.END,
|
||||
ev,
|
||||
mTargetCoordinates[0],
|
||||
mTargetCoordinates[1]));
|
||||
} else if (action == MotionEvent.ACTION_CANCEL) {
|
||||
dispatchCancelEvent(ev);
|
||||
mTargetTag = -1;
|
||||
} else {
|
||||
FLog.w(
|
||||
ReactConstants.TAG,
|
||||
"Warning : touch event was ignored. Action=" + action + " Target=" + mTargetTag);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildStartedNativeGesture(MotionEvent androidEvent) {
|
||||
if (mChildIsHandlingNativeGesture) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
dispatchCancelEvent(androidEvent);
|
||||
mChildIsHandlingNativeGesture = true;
|
||||
mTargetTag = -1;
|
||||
}
|
||||
|
||||
private void dispatchCancelEvent(MotionEvent androidEvent) {
|
||||
// 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.
|
||||
if (mTargetTag == -1) {
|
||||
if (mReactInstanceManager == null || !mIsAttachedToInstance ||
|
||||
mReactInstanceManager.getCurrentReactContext() == null) {
|
||||
FLog.w(
|
||||
ReactConstants.TAG,
|
||||
"Can't cancel already finished gesture. Is a child View trying to start a gesture from " +
|
||||
"an UP/CANCEL event?");
|
||||
ReactConstants.TAG,
|
||||
"Unable to dispatch touch to JS as the catalyst instance has not been attached");
|
||||
return;
|
||||
}
|
||||
|
||||
EventDispatcher eventDispatcher = mReactInstanceManager.getCurrentReactContext()
|
||||
.getNativeModule(UIManagerModule.class)
|
||||
.getEventDispatcher();
|
||||
|
||||
Assertions.assertCondition(
|
||||
!mChildIsHandlingNativeGesture,
|
||||
"Expected to not have already sent a cancel for this gesture");
|
||||
Assertions.assertNotNull(eventDispatcher).dispatchEvent(
|
||||
TouchEvent.obtain(
|
||||
mTargetTag,
|
||||
SystemClock.nanoTime(),
|
||||
TouchEventType.CANCEL,
|
||||
androidEvent,
|
||||
mTargetCoordinates[0],
|
||||
mTargetCoordinates[1]));
|
||||
ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
|
||||
EventDispatcher eventDispatcher = reactContext.getNativeModule(UIManagerModule.class)
|
||||
.getEventDispatcher();
|
||||
mJSTouchDispatcher.onChildStartedNativeGesture(androidEvent, eventDispatcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
||||
handleTouchEvent(ev);
|
||||
dispatchJSTouchEvent(ev);
|
||||
return super.onInterceptTouchEvent(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent ev) {
|
||||
handleTouchEvent(ev);
|
||||
dispatchJSTouchEvent(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
|
||||
return true;
|
||||
}
|
||||
|
||||
private void dispatchJSTouchEvent(MotionEvent event) {
|
||||
if (mReactInstanceManager == null || !mIsAttachedToInstance ||
|
||||
mReactInstanceManager.getCurrentReactContext() == null) {
|
||||
FLog.w(
|
||||
ReactConstants.TAG,
|
||||
"Unable to dispatch touch to JS as the catalyst instance has not been attached");
|
||||
return;
|
||||
}
|
||||
ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
|
||||
EventDispatcher eventDispatcher = reactContext.getNativeModule(UIManagerModule.class)
|
||||
.getEventDispatcher();
|
||||
mJSTouchDispatcher.handleTouchEvent(event, eventDispatcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
|
||||
// No-op - override in order to still receive events to onInterceptTouchEvent
|
||||
|
@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
package com.facebook.react.uimanager;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
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.common.SystemClock;
|
||||
import com.facebook.react.uimanager.events.EventDispatcher;
|
||||
import com.facebook.react.uimanager.events.TouchEvent;
|
||||
import com.facebook.react.uimanager.events.TouchEventType;
|
||||
|
||||
/**
|
||||
* JSTouchDispatcher handles dispatching touches to JS from RootViews. If you implement RootView
|
||||
* you need to call handleTouchEvent from onTouchEvent and onInterceptTouchEvent. It will correctly
|
||||
* find the right view to handle the touch and also dispatch the appropriate event to JS
|
||||
*/
|
||||
public class JSTouchDispatcher {
|
||||
|
||||
private int mTargetTag = -1;
|
||||
private final float[] mTargetCoordinates = new float[2];
|
||||
private boolean mChildIsHandlingNativeGesture = false;
|
||||
private final ViewGroup mRootViewGroup;
|
||||
|
||||
public JSTouchDispatcher(ViewGroup viewGroup) {
|
||||
mRootViewGroup = viewGroup;
|
||||
}
|
||||
|
||||
public void onChildStartedNativeGesture(MotionEvent androidEvent, EventDispatcher eventDispatcher) {
|
||||
if (mChildIsHandlingNativeGesture) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
dispatchCancelEvent(androidEvent, eventDispatcher);
|
||||
mChildIsHandlingNativeGesture = true;
|
||||
mTargetTag = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main catalyst view is responsible for collecting and sending touch events to JS. This method
|
||||
* reacts for an incoming android native touch events ({@link MotionEvent}) and calls into
|
||||
* {@link com.facebook.react.uimanager.events.EventDispatcher} when appropriate.
|
||||
* It uses {@link com.facebook.react.uimanager.TouchTargetHelper#findTouchTargetView}
|
||||
* helper method for figuring out a react view ID in the case of ACTION_DOWN
|
||||
* event (when the gesture starts).
|
||||
*/
|
||||
public void handleTouchEvent(MotionEvent ev, EventDispatcher eventDispatcher) {
|
||||
int action = ev.getAction() & MotionEvent.ACTION_MASK;
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
if (mTargetTag != -1) {
|
||||
FLog.e(
|
||||
ReactConstants.TAG,
|
||||
"Got DOWN touch before receiving UP or CANCEL from last gesture");
|
||||
}
|
||||
|
||||
// First event for this gesture. We expect tag to be set to -1, and we use helper method
|
||||
// {@link #findTargetTagForTouch} to find react view ID that will be responsible for handling
|
||||
// this gesture
|
||||
mChildIsHandlingNativeGesture = false;
|
||||
mTargetTag = TouchTargetHelper.findTargetTagAndCoordinatesForTouch(
|
||||
ev.getX(),
|
||||
ev.getY(),
|
||||
mRootViewGroup,
|
||||
mTargetCoordinates);
|
||||
eventDispatcher.dispatchEvent(
|
||||
TouchEvent.obtain(
|
||||
mTargetTag,
|
||||
SystemClock.nanoTime(),
|
||||
TouchEventType.START,
|
||||
ev,
|
||||
mTargetCoordinates[0],
|
||||
mTargetCoordinates[1]));
|
||||
} 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.
|
||||
return;
|
||||
} else if (mTargetTag == -1) {
|
||||
// All the subsequent action types are expected to be called after ACTION_DOWN thus target
|
||||
// is supposed to be set for them.
|
||||
FLog.e(
|
||||
ReactConstants.TAG,
|
||||
"Unexpected state: received touch event but didn't get starting ACTION_DOWN for this " +
|
||||
"gesture before");
|
||||
} else if (action == MotionEvent.ACTION_UP) {
|
||||
// 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.nanoTime(),
|
||||
TouchEventType.END,
|
||||
ev,
|
||||
mTargetCoordinates[0],
|
||||
mTargetCoordinates[1]));
|
||||
mTargetTag = -1;
|
||||
} else if (action == MotionEvent.ACTION_MOVE) {
|
||||
// Update pointer position for current gesture
|
||||
eventDispatcher.dispatchEvent(
|
||||
TouchEvent.obtain(
|
||||
mTargetTag,
|
||||
SystemClock.nanoTime(),
|
||||
TouchEventType.MOVE,
|
||||
ev,
|
||||
mTargetCoordinates[0],
|
||||
mTargetCoordinates[1]));
|
||||
} 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.nanoTime(),
|
||||
TouchEventType.START,
|
||||
ev,
|
||||
mTargetCoordinates[0],
|
||||
mTargetCoordinates[1]));
|
||||
} else if (action == MotionEvent.ACTION_POINTER_UP) {
|
||||
// Exactly onw of the pointers goes up
|
||||
eventDispatcher.dispatchEvent(
|
||||
TouchEvent.obtain(
|
||||
mTargetTag,
|
||||
SystemClock.nanoTime(),
|
||||
TouchEventType.END,
|
||||
ev,
|
||||
mTargetCoordinates[0],
|
||||
mTargetCoordinates[1]));
|
||||
} else if (action == MotionEvent.ACTION_CANCEL) {
|
||||
dispatchCancelEvent(ev, eventDispatcher);
|
||||
mTargetTag = -1;
|
||||
} else {
|
||||
FLog.w(
|
||||
ReactConstants.TAG,
|
||||
"Warning : touch event was ignored. Action=" + action + " Target=" + mTargetTag);
|
||||
}
|
||||
}
|
||||
|
||||
private void dispatchCancelEvent(MotionEvent androidEvent, 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.
|
||||
if (mTargetTag == -1) {
|
||||
FLog.w(
|
||||
ReactConstants.TAG,
|
||||
"Can't cancel already finished gesture. Is a child View trying to start a gesture from " +
|
||||
"an UP/CANCEL event?");
|
||||
return;
|
||||
}
|
||||
|
||||
Assertions.assertCondition(
|
||||
!mChildIsHandlingNativeGesture,
|
||||
"Expected to not have already sent a cancel for this gesture");
|
||||
Assertions.assertNotNull(eventDispatcher).dispatchEvent(
|
||||
TouchEvent.obtain(
|
||||
mTargetTag,
|
||||
SystemClock.nanoTime(),
|
||||
TouchEventType.CANCEL,
|
||||
androidEvent,
|
||||
mTargetCoordinates[0],
|
||||
mTargetCoordinates[1]));
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user