diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index e92289a5f1d..ebe012492b2 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -185,14 +185,7 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot { @Override public void onChildStartedNativeGesture(MotionEvent ev) { - 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 (mJSTouchDispatcher == null) { - FLog.w(TAG, "Unable to dispatch touch to JS before the dispatcher is available"); + if (!isDispatcherReady()) { return; } ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); @@ -209,6 +202,35 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot { onChildStartedNativeGesture(ev); } + @Override + public void onChildEndedNativeGesture(View childView, MotionEvent ev) { + if (!isDispatcherReady()) { + return; + } + ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); + UIManager uiManager = UIManagerHelper.getUIManager(reactContext, getUIManagerType()); + + if (uiManager != null) { + EventDispatcher eventDispatcher = uiManager.getEventDispatcher(); + mJSTouchDispatcher.onChildEndedNativeGesture(ev, eventDispatcher); + } + } + + private boolean isDispatcherReady() { + 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 false; + } + if (mJSTouchDispatcher == null) { + FLog.w(TAG, "Unable to dispatch touch to JS before the dispatcher is available"); + return false; + } + + return true; + } + @Override public boolean onInterceptTouchEvent(MotionEvent ev) { dispatchJSTouchEvent(ev); diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSTouchDispatcher.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSTouchDispatcher.java index d3b8043d655..49674e7cf17 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSTouchDispatcher.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSTouchDispatcher.java @@ -50,6 +50,11 @@ public class JSTouchDispatcher { mTargetTag = -1; } + public void onChildEndedNativeGesture(MotionEvent androidEvent, EventDispatcher eventDispatcher) { + // There should be only one child gesture at any given time. We can safely turn off the flag. + mChildIsHandlingNativeGesture = false; + } + /** * 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 diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootView.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootView.java index 6188a241962..ebdb54487ca 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootView.java @@ -22,5 +22,11 @@ public interface RootView { /** @deprecated */ void onChildStartedNativeGesture(MotionEvent ev); + /** + * Called when a child ends a native gesture. Should be called from the child's onTouchIntercepted + * implementation. + */ + void onChildEndedNativeGesture(View childView, MotionEvent ev); + void handleException(Throwable t); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/NativeGestureUtil.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/NativeGestureUtil.java index 7b79013960b..abbfbc67de9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/NativeGestureUtil.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/NativeGestureUtil.java @@ -25,4 +25,16 @@ public class NativeGestureUtil { public static void notifyNativeGestureStarted(View view, MotionEvent event) { RootViewUtil.getRootView(view).onChildStartedNativeGesture(view, event); } + + /** + * Helper method that should be called when a native view ends a native gesture (e.g. a native + * ScrollView takes control of a gesture stream and starts scrolling). This will handle + * dispatching the appropriate events to JS to make sure future gesture is not blocked. + * + * @param view the View ending the native gesture + * @param event the MotionEvent that caused the gesture to be ended + */ + public static void notifyNativeGestureEnded(View view, MotionEvent event) { + RootViewUtil.getRootView(view).onChildEndedNativeGesture(view, event); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java b/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java index 7d272250bd1..b21cecfc251 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java @@ -537,6 +537,11 @@ public class ReactModalHostView extends ViewGroup mJSTouchDispatcher.onChildStartedNativeGesture(ev, mEventDispatcher); } + @Override + public void onChildEndedNativeGesture(View childView, MotionEvent ev) { + mJSTouchDispatcher.onChildEndedNativeGesture(ev, mEventDispatcher); + } + @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { // No-op - override in order to still receive events to onInterceptTouchEvent