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/5] 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/5] 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/5] 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/5] 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/5] 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) } }