From 4a029fa593c9314f19fe73412a4e5baa7d8fc0de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Tue, 18 Aug 2026 08:53:58 +0200 Subject: [PATCH 1/9] Fix buttons press on scroll end --- .../core/GestureHandlerOrchestrator.kt | 28 +++++++++++++------ .../core/NativeViewGestureHandler.kt | 8 ++++++ .../RNGestureHandlerButtonViewManager.kt | 17 +++++++++++ .../react/RNGestureHandlerRootHelper.kt | 14 ++++++++++ .../react/RNGestureHandlerRootView.kt | 4 ++- 5 files changed, 61 insertions(+), 10 deletions(-) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt index 9ced951328..abc72fca77 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt @@ -365,20 +365,13 @@ class GestureHandlerOrchestrator( event.recycle() } - /** - * Cancels all handlers created using API v1 and v2 - */ - fun cancelAllLegacyHandlers() { + private inline fun cancelTrackedHandlers(predicate: (GestureHandler) -> Boolean) { val handlersToProcess = obtainHandlerList() handlersToProcess.addAll(gestureHandlers) try { handlersToProcess.forEach { - if (it.actionType == GestureHandler.ACTION_TYPE_JS_FUNCTION_OLD_API || - it.actionType == GestureHandler.ACTION_TYPE_JS_FUNCTION_NEW_API || - it.actionType == GestureHandler.ACTION_TYPE_REANIMATED_WORKLET || - it.actionType == GestureHandler.ACTION_TYPE_NATIVE_ANIMATED_EVENT - ) { + if (predicate(it)) { it.cancel() } } @@ -389,6 +382,23 @@ class GestureHandlerOrchestrator( } } + /** + * Cancels all handlers created using API v1 and v2 + */ + fun cancelAllLegacyHandlers() = cancelTrackedHandlers { + it.actionType == GestureHandler.ACTION_TYPE_JS_FUNCTION_OLD_API || + it.actionType == GestureHandler.ACTION_TYPE_JS_FUNCTION_NEW_API || + it.actionType == GestureHandler.ACTION_TYPE_REANIMATED_WORKLET || + it.actionType == GestureHandler.ACTION_TYPE_NATIVE_ANIMATED_EVENT + } + + /** + * Cancels handlers whose view opted out of surviving a native view taking over the touch stream. + */ + fun cancelHandlersOnNativeTouchGrab() = cancelTrackedHandlers { + it is NativeViewGestureHandler && it.shouldCancelOnNativeTouchGrab() + } + /** * isViewAttachedUnderWrapper checks whether all of parents for view related to handler * view are attached. Since there might be an issue rarely observed when view diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt index d53ab52192..2859db834a 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt @@ -241,6 +241,8 @@ class NativeViewGestureHandler : GestureHandler() { override fun wantsToAttachDirectlyToView() = true + fun shouldCancelOnNativeTouchGrab(): Boolean = hook.shouldCancelOnNativeTouchGrab() + data class HitSlop( val left: Float = HIT_SLOP_NONE, val top: Float = HIT_SLOP_NONE, @@ -361,6 +363,12 @@ class NativeViewGestureHandler : GestureHandler() { */ fun shouldRecognizeSimultaneously(handler: GestureHandler): Boolean? = null + /** + * Called after a native view grabbed the touch lock during a native dispatch pass. + * Return true to cancel the handler. + */ + fun shouldCancelOnNativeTouchGrab() = false + /** * shouldActivateOnStart and tryIntercept have priority over this method * diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt index 9b511049b1..94548384e6 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt @@ -717,6 +717,10 @@ class RNGestureHandlerButtonViewManager : // event). private var lastEventWasInside = false + // Whether the native dispatch delivered DOWN for the current gesture. False when a native + // ancestor intercepted it — the orchestrator still delivers events then. + private var receivedNativeDown = false + override fun onHandlerUpdate(handler: NativeViewGestureHandler) { if (managedHandlerTag == null || handler.isWithinBounds == lastEventWasInside) { return @@ -744,6 +748,9 @@ class RNGestureHandlerButtonViewManager : val localLastEventWasInside = lastEventWasInside if (newState == GestureHandler.STATE_BEGAN) { + // Reset for the new gesture - BEGAN precedes the native dispatch of DOWN, which + // sets the flag. + receivedNativeDown = false dispatchJSEvent(EventType.PressIn, handler) longPressDetected = false @@ -815,6 +822,16 @@ class RNGestureHandlerButtonViewManager : } } + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + receivedNativeDown = true + } + + return super.dispatchTouchEvent(event) + } + + override fun shouldCancelOnNativeTouchGrab() = !receivedNativeDown + override fun onInterceptTouchEvent(event: MotionEvent): Boolean { if (super.onInterceptTouchEvent(event)) { return true diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt index 57979ea310..a27c5ef896 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt @@ -22,6 +22,7 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView: private var shouldIntercept = false private var wasIntercepting = false private var passingTouch = false + private var nativeTouchGrabRequested = false init { val registry = @@ -120,10 +121,23 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView: if (orchestrator != null && !passingTouch) { // if we are in the process of delivering touch events via GH orchestrator, we don't want to // treat it as a native gesture capturing the lock + nativeTouchGrabRequested = true orchestrator.cancelAllLegacyHandlers() } } + /** + * A disallow-intercept request may mean a real interception, but it may also be just a + * defensive call from a view that lets the event through (e.g. a nested pager). The two can only + * be told apart after the native dispatch completes, so cancellation runs here, not at request time. + */ + fun onNativeDispatchEnd() { + if (nativeTouchGrabRequested) { + nativeTouchGrabRequested = false + orchestrator?.cancelHandlersOnNativeTouchGrab() + } + } + fun dispatchTouchEvent(event: MotionEvent): Boolean { // We mark `mPassingTouch` before we get into `mOrchestrator.onTouchEvent` so that we can tell // if `requestDisallow` has been called as a result of a normal gesture handling process or diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt index 06c01bb8b5..402754d64e 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt @@ -62,7 +62,9 @@ class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) { return if (rootViewEnabled && rootHelper!!.dispatchTouchEvent(event)) { true } else { - super.dispatchTouchEvent(event) + val handled = super.dispatchTouchEvent(event) + rootHelper?.onNativeDispatchEnd() + handled } } From b2ee903a5c7c199037485140674a3c748ae51f99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Tue, 18 Aug 2026 09:41:06 +0200 Subject: [PATCH 2/9] Remove comment --- .../gesturehandler/core/GestureHandlerOrchestrator.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt index abc72fca77..1d5cdd8f2a 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt @@ -382,9 +382,6 @@ class GestureHandlerOrchestrator( } } - /** - * Cancels all handlers created using API v1 and v2 - */ fun cancelAllLegacyHandlers() = cancelTrackedHandlers { it.actionType == GestureHandler.ACTION_TYPE_JS_FUNCTION_OLD_API || it.actionType == GestureHandler.ACTION_TYPE_JS_FUNCTION_NEW_API || From ae28247be46ddce560b6a03f7f408f5da3902913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Tue, 18 Aug 2026 10:15:00 +0200 Subject: [PATCH 3/9] SectionList long press --- .../gesturehandler/core/GestureHandlerOrchestrator.kt | 4 ++-- .../gesturehandler/core/NativeViewGestureHandler.kt | 9 +++++---- .../react/RNGestureHandlerButtonViewManager.kt | 5 ++--- .../gesturehandler/react/RNGestureHandlerRootHelper.kt | 4 ++-- .../gesturehandler/react/RNGestureHandlerRootView.kt | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt index 1d5cdd8f2a..be898c3713 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt @@ -392,8 +392,8 @@ class GestureHandlerOrchestrator( /** * Cancels handlers whose view opted out of surviving a native view taking over the touch stream. */ - fun cancelHandlersOnNativeTouchGrab() = cancelTrackedHandlers { - it is NativeViewGestureHandler && it.shouldCancelOnNativeTouchGrab() + fun cancelHandlersOnNativeTouchGrab(grabbedMidGesture: Boolean) = cancelTrackedHandlers { + it is NativeViewGestureHandler && it.shouldCancelOnNativeTouchGrab(grabbedMidGesture) } /** diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt index 2859db834a..44501ece78 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt @@ -241,7 +241,8 @@ class NativeViewGestureHandler : GestureHandler() { override fun wantsToAttachDirectlyToView() = true - fun shouldCancelOnNativeTouchGrab(): Boolean = hook.shouldCancelOnNativeTouchGrab() + fun shouldCancelOnNativeTouchGrab(grabbedMidGesture: Boolean): Boolean = + hook.shouldCancelOnNativeTouchGrab(grabbedMidGesture) data class HitSlop( val left: Float = HIT_SLOP_NONE, @@ -364,10 +365,10 @@ class NativeViewGestureHandler : GestureHandler() { fun shouldRecognizeSimultaneously(handler: GestureHandler): Boolean? = null /** - * Called after a native view grabbed the touch lock during a native dispatch pass. - * Return true to cancel the handler. + * Called after a native view grabbed the touch lock; return true to cancel the handler. + * [grabbedMidGesture] distinguishes a real take-over from a defensive DOWN-pass disallow. */ - fun shouldCancelOnNativeTouchGrab() = false + fun shouldCancelOnNativeTouchGrab(grabbedMidGesture: Boolean) = false /** * shouldActivateOnStart and tryIntercept have priority over this method diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt index 94548384e6..1115667659 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt @@ -748,8 +748,7 @@ class RNGestureHandlerButtonViewManager : val localLastEventWasInside = lastEventWasInside if (newState == GestureHandler.STATE_BEGAN) { - // Reset for the new gesture - BEGAN precedes the native dispatch of DOWN, which - // sets the flag. + // Reset for the new gesture - BEGAN precedes the native dispatch of DOWN that sets the flag. receivedNativeDown = false dispatchJSEvent(EventType.PressIn, handler) longPressDetected = false @@ -830,7 +829,7 @@ class RNGestureHandlerButtonViewManager : return super.dispatchTouchEvent(event) } - override fun shouldCancelOnNativeTouchGrab() = !receivedNativeDown + override fun shouldCancelOnNativeTouchGrab(grabbedMidGesture: Boolean) = grabbedMidGesture || !receivedNativeDown override fun onInterceptTouchEvent(event: MotionEvent): Boolean { if (super.onInterceptTouchEvent(event)) { diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt index a27c5ef896..596c26ef83 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt @@ -131,10 +131,10 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView: * defensive call from a view that lets the event through (e.g. a nested pager). The two can only * be told apart after the native dispatch completes, so cancellation runs here, not at request time. */ - fun onNativeDispatchEnd() { + fun onNativeDispatchEnd(event: MotionEvent) { if (nativeTouchGrabRequested) { nativeTouchGrabRequested = false - orchestrator?.cancelHandlersOnNativeTouchGrab() + orchestrator?.cancelHandlersOnNativeTouchGrab(event.actionMasked != MotionEvent.ACTION_DOWN) } } diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt index 402754d64e..616bf09b3f 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt @@ -63,7 +63,7 @@ class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) { true } else { val handled = super.dispatchTouchEvent(event) - rootHelper?.onNativeDispatchEnd() + rootHelper?.onNativeDispatchEnd(event) handled } } From 706cfa36a3e21bc1a9116404b60f874cade527f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Tue, 18 Aug 2026 10:40:16 +0200 Subject: [PATCH 4/9] Comment --- .../swmansion/gesturehandler/core/NativeViewGestureHandler.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt index 44501ece78..a3d36d95f1 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt @@ -366,7 +366,6 @@ class NativeViewGestureHandler : GestureHandler() { /** * Called after a native view grabbed the touch lock; return true to cancel the handler. - * [grabbedMidGesture] distinguishes a real take-over from a defensive DOWN-pass disallow. */ fun shouldCancelOnNativeTouchGrab(grabbedMidGesture: Boolean) = false From 9dfc9a2483ffb94204490333719f3e26595dac39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Tue, 18 Aug 2026 10:51:56 +0200 Subject: [PATCH 5/9] Handle also pointer down --- .../gesturehandler/react/RNGestureHandlerRootHelper.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt index 596c26ef83..db81539298 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt @@ -134,7 +134,11 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView: fun onNativeDispatchEnd(event: MotionEvent) { if (nativeTouchGrabRequested) { nativeTouchGrabRequested = false - orchestrator?.cancelHandlersOnNativeTouchGrab(event.actionMasked != MotionEvent.ACTION_DOWN) + + val grabbedMidGesture = event.actionMasked != MotionEvent.ACTION_DOWN && + event.actionMasked != MotionEvent.ACTION_POINTER_DOWN + + orchestrator?.cancelHandlersOnNativeTouchGrab(grabbedMidGesture) } } From f7c0be9a56f7faf10df858738af238910d6889ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Fri, 21 Aug 2026 10:26:28 +0200 Subject: [PATCH 6/9] Copilot comment --- .../gesturehandler/react/RNGestureHandlerRootHelper.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt index db81539298..de80bfb108 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt @@ -117,7 +117,8 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView: fun requestDisallowInterceptTouchEvent() { // If this method gets called it means that some native view is attempting to grab lock for - // touch event delivery. In that case we cancel all gesture recognizers + // touch event delivery. Legacy handlers are cancelled right away; handlers opting into + // native-touch-grab cancellation are deferred to `onNativeDispatchEnd`. if (orchestrator != null && !passingTouch) { // if we are in the process of delivering touch events via GH orchestrator, we don't want to // treat it as a native gesture capturing the lock From d6fe536c4775bb96ab948f1ecbf7238a5f7e4aac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Fri, 21 Aug 2026 10:27:42 +0200 Subject: [PATCH 7/9] Consider requests from outside dispatch pass --- .../react/RNGestureHandlerRootHelper.kt | 13 ++++++++++++- .../react/RNGestureHandlerRootView.kt | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt index de80bfb108..6af3461eb1 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt @@ -22,6 +22,7 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView: private var shouldIntercept = false private var wasIntercepting = false private var passingTouch = false + private var passingNativeTouch = false private var nativeTouchGrabRequested = false init { @@ -122,17 +123,27 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView: if (orchestrator != null && !passingTouch) { // if we are in the process of delivering touch events via GH orchestrator, we don't want to // treat it as a native gesture capturing the lock - nativeTouchGrabRequested = true + if (passingNativeTouch) { + // Requests may also arrive outside any dispatch pass (e.g. RN's JS responder). Those have + // no pass to classify against and must not arm the sweep for a future gesture. + nativeTouchGrabRequested = true + } orchestrator.cancelAllLegacyHandlers() } } + fun onNativeDispatchStart() { + passingNativeTouch = true + } + /** * A disallow-intercept request may mean a real interception, but it may also be just a * defensive call from a view that lets the event through (e.g. a nested pager). The two can only * be told apart after the native dispatch completes, so cancellation runs here, not at request time. */ fun onNativeDispatchEnd(event: MotionEvent) { + passingNativeTouch = false + if (nativeTouchGrabRequested) { nativeTouchGrabRequested = false diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt index 616bf09b3f..c896945554 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt @@ -62,6 +62,7 @@ class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) { return if (rootViewEnabled && rootHelper!!.dispatchTouchEvent(event)) { true } else { + rootHelper?.onNativeDispatchStart() val handled = super.dispatchTouchEvent(event) rootHelper?.onNativeDispatchEnd(event) handled From 1e89b3215d5eb024fa4792ffecb35eddb1008fdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Fri, 21 Aug 2026 10:40:36 +0200 Subject: [PATCH 8/9] Rename --- .../gesturehandler/core/GestureHandlerOrchestrator.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt index be898c3713..95bc9a10bd 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt @@ -365,7 +365,7 @@ class GestureHandlerOrchestrator( event.recycle() } - private inline fun cancelTrackedHandlers(predicate: (GestureHandler) -> Boolean) { + private inline fun cancelHandlersMatching(predicate: (GestureHandler) -> Boolean) { val handlersToProcess = obtainHandlerList() handlersToProcess.addAll(gestureHandlers) @@ -382,7 +382,7 @@ class GestureHandlerOrchestrator( } } - fun cancelAllLegacyHandlers() = cancelTrackedHandlers { + fun cancelAllLegacyHandlers() = cancelHandlersMatching { it.actionType == GestureHandler.ACTION_TYPE_JS_FUNCTION_OLD_API || it.actionType == GestureHandler.ACTION_TYPE_JS_FUNCTION_NEW_API || it.actionType == GestureHandler.ACTION_TYPE_REANIMATED_WORKLET || @@ -392,7 +392,7 @@ class GestureHandlerOrchestrator( /** * Cancels handlers whose view opted out of surviving a native view taking over the touch stream. */ - fun cancelHandlersOnNativeTouchGrab(grabbedMidGesture: Boolean) = cancelTrackedHandlers { + fun cancelHandlersOnNativeTouchGrab(grabbedMidGesture: Boolean) = cancelHandlersMatching { it is NativeViewGestureHandler && it.shouldCancelOnNativeTouchGrab(grabbedMidGesture) } From e9feb1f912b1efa3e7eb4a2c1cb757400df66aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Fri, 21 Aug 2026 11:51:25 +0200 Subject: [PATCH 9/9] Rephrase comment --- .../gesturehandler/react/RNGestureHandlerRootHelper.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt index 6af3461eb1..63001c1799 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt @@ -137,9 +137,13 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView: } /** - * A disallow-intercept request may mean a real interception, but it may also be just a - * defensive call from a view that lets the event through (e.g. a nested pager). The two can only - * be told apart after the native dispatch completes, so cancellation runs here, not at request time. + * Deferred handling of a disallow-intercept request recorded during this dispatch pass. The + * request alone doesn't say what the caller did with the event: a scrollable calls it when it + * takes over the touch, but e.g. a nested pager calls it already on DOWN, just to keep its + * ancestors from stealing a swipe it may recognize later, and the event still reaches the + * button - at request time both calls look identical. They only become + * distinguishable once the native dispatch completes (did the button receive the DOWN?), which + * is why cancellation runs here instead of in `requestDisallowInterceptTouchEvent`. */ fun onNativeDispatchEnd(event: MotionEvent) { passingNativeTouch = false