From 84cd63bcd9e04cae6a8a3d6c32a70f9aec651f9e Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 28 Apr 2026 11:04:56 +0200 Subject: [PATCH 1/5] Cancel button gesture when other handlers are extracted before --- .../swmansion/gesturehandler/core/GestureHandler.kt | 2 ++ .../core/GestureHandlerOrchestrator.kt | 4 ++++ .../gesturehandler/core/NativeViewGestureHandler.kt | 12 ++++++++++++ .../react/RNGestureHandlerButtonViewManager.kt | 5 +++++ 4 files changed, 23 insertions(+) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt index 995146da5a..218b8fc70f 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt @@ -693,6 +693,8 @@ open class GestureHandler { return interactionController?.shouldHandlerBeCancelledBy(this, handler) ?: false } + open fun shouldBeginWithRecorded(recorded: List): Boolean = true + fun isWithinBounds(view: View?, posX: Float, posY: Float): Boolean { if (RNSVGHitTester.isSvgElement(view!!)) { return RNSVGHitTester.hitTest(view, posX, posY) 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 8778e7bcab..d7d718c3d1 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 @@ -453,6 +453,10 @@ class GestureHandlerOrchestrator( handler.isAwaiting = false handler.activationIndex = Int.MAX_VALUE handler.prepare(view, this) + + if (!handler.shouldBeginWithRecorded(gestureHandlers)) { + handler.cancel() + } } private fun isViewOverflowingParent(view: View): Boolean { 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 73e0262c08..c69d7a0f6a 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 @@ -83,6 +83,9 @@ class NativeViewGestureHandler : GestureHandler() { override fun shouldBeCancelledBy(handler: GestureHandler): Boolean = !disallowInterruption + override fun shouldBeginWithRecorded(recorded: List): Boolean = + hook.shouldBeginWithRecorded(recorded, this) + override fun onPrepare() { when (val view = view) { is NativeViewGestureHandlerHook -> this.hook = view @@ -271,6 +274,15 @@ class NativeViewGestureHandler : GestureHandler() { */ fun shouldCancelRootViewGestureHandlerIfNecessary() = false + /** + * Called when the handler is being recorded by the orchestrator, before any pointer events + * are delivered. Returning `false` cancels the handler immediately. + * + * @param recorded handlers already recorded for the current touch + * @param handler the handler being recorded + */ + fun shouldBeginWithRecorded(recorded: List, handler: NativeViewGestureHandler): Boolean = true + /** * Passes the event down to the underlying view using the correct 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 39c59da6bc..cb5291af9b 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 @@ -42,6 +42,8 @@ import com.facebook.react.uimanager.style.BorderStyle import com.facebook.react.uimanager.style.LogicalEdge import com.facebook.react.viewmanagers.RNGestureHandlerButtonManagerDelegate import com.facebook.react.viewmanagers.RNGestureHandlerButtonManagerInterface +import com.swmansion.gesturehandler.core.GestureHandler +import com.swmansion.gesturehandler.core.HoverGestureHandler import com.swmansion.gesturehandler.core.NativeViewGestureHandler import com.swmansion.gesturehandler.react.RNGestureHandlerButtonViewManager.ButtonViewGroup @@ -738,6 +740,9 @@ class RNGestureHandlerButtonViewManager : isTouched = false } + override fun shouldBeginWithRecorded(recorded: List, handler: NativeViewGestureHandler): Boolean = + recorded.all { it.shouldRecognizeSimultaneously(handler) || it.view == this || it is HoverGestureHandler } + private fun tryFreeingResponder() { if (touchResponder === this) { touchResponder = null From 4ad34e16865c4cc71bc1949fdea37d5e63fc1e0b Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 28 Apr 2026 11:08:24 +0200 Subject: [PATCH 2/5] Update example Co-authored-by: Copilot --- .../src/new_api/tests/nestedTouchables/index.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/common-app/src/new_api/tests/nestedTouchables/index.tsx b/apps/common-app/src/new_api/tests/nestedTouchables/index.tsx index dd3258ae54..72da4e2c31 100644 --- a/apps/common-app/src/new_api/tests/nestedTouchables/index.tsx +++ b/apps/common-app/src/new_api/tests/nestedTouchables/index.tsx @@ -12,6 +12,7 @@ export default function NestedTouchablesExample() { const [log, setLog] = useState([]); const pushLog = (message: string) => { + console.log(message); setLog((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${message}`].slice(-6) ); @@ -20,11 +21,13 @@ export default function NestedTouchablesExample() { const outerTap = useTapGesture({ runOnJS: true, onActivate: () => pushLog('outer tap gesture'), + testID: 'outer-tap', }); const innerTap = useTapGesture({ runOnJS: true, onActivate: () => pushLog('inner tap gesture'), + testID: 'inner-tap', }); return ( @@ -40,6 +43,11 @@ export default function NestedTouchablesExample() { pushLog('outer press in')} + onPressOut={() => pushLog('outer press out')} + onLongPress={() => pushLog('outer long press')} onPress={() => pushLog('outer Touchable')}> Outer Touchable @@ -49,6 +57,11 @@ export default function NestedTouchablesExample() { pushLog('inner press in')} + onPressOut={() => pushLog('inner press out')} + onLongPress={() => pushLog('inner long press')} onPress={() => pushLog('inner Touchable')}> Inner Touchable From 65ecbe5b0ddf476c2bb8a367b797a39864c1b7f4 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 28 Apr 2026 11:57:28 +0200 Subject: [PATCH 3/5] Make simultaneous check symmetric --- .../react/RNGestureHandlerButtonViewManager.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 cb5291af9b..cb95aec3b8 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 @@ -741,7 +741,12 @@ class RNGestureHandlerButtonViewManager : } override fun shouldBeginWithRecorded(recorded: List, handler: NativeViewGestureHandler): Boolean = - recorded.all { it.shouldRecognizeSimultaneously(handler) || it.view == this || it is HoverGestureHandler } + recorded.all { + it.shouldRecognizeSimultaneously(handler) || + handler.shouldRecognizeSimultaneously(it) || + it.view == this || + it is HoverGestureHandler + } private fun tryFreeingResponder() { if (touchResponder === this) { From a39de09c89091999973723d2cb515e38baca0edd Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 28 Apr 2026 11:57:54 +0200 Subject: [PATCH 4/5] Add new handler to list after `shouldBeginWithRecorded` call --- .../gesturehandler/core/GestureHandlerOrchestrator.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 d7d718c3d1..7dd4466bcc 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 @@ -448,7 +448,6 @@ class GestureHandlerOrchestrator( return } - gestureHandlers.add(handler) handler.isActive = false handler.isAwaiting = false handler.activationIndex = Int.MAX_VALUE @@ -457,6 +456,8 @@ class GestureHandlerOrchestrator( if (!handler.shouldBeginWithRecorded(gestureHandlers)) { handler.cancel() } + + gestureHandlers.add(handler) } private fun isViewOverflowingParent(view: View): Boolean { From 5d1211b0a82853acf91d5ea9b14a81067924ff5a Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 29 Apr 2026 09:27:46 +0200 Subject: [PATCH 5/5] Rename method --- .../gesturehandler/core/GestureHandler.kt | 2 +- .../core/GestureHandlerOrchestrator.kt | 2 +- .../core/NativeViewGestureHandler.kt | 7 ++++--- .../react/RNGestureHandlerButtonViewManager.kt | 16 +++++++++------- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt index 218b8fc70f..2042529e2d 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt @@ -693,7 +693,7 @@ open class GestureHandler { return interactionController?.shouldHandlerBeCancelledBy(this, handler) ?: false } - open fun shouldBeginWithRecorded(recorded: List): Boolean = true + open fun shouldBeginWithRecordedHandlers(recorded: List): Boolean = true fun isWithinBounds(view: View?, posX: Float, posY: Float): Boolean { if (RNSVGHitTester.isSvgElement(view!!)) { 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 7dd4466bcc..21fe9d94fb 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 @@ -453,7 +453,7 @@ class GestureHandlerOrchestrator( handler.activationIndex = Int.MAX_VALUE handler.prepare(view, this) - if (!handler.shouldBeginWithRecorded(gestureHandlers)) { + if (!handler.shouldBeginWithRecordedHandlers(gestureHandlers)) { handler.cancel() } 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 c69d7a0f6a..39cef22bf9 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 @@ -83,8 +83,8 @@ class NativeViewGestureHandler : GestureHandler() { override fun shouldBeCancelledBy(handler: GestureHandler): Boolean = !disallowInterruption - override fun shouldBeginWithRecorded(recorded: List): Boolean = - hook.shouldBeginWithRecorded(recorded, this) + override fun shouldBeginWithRecordedHandlers(recorded: List): Boolean = + hook.shouldBeginWithRecordedHandlers(recorded, this) override fun onPrepare() { when (val view = view) { @@ -281,7 +281,8 @@ class NativeViewGestureHandler : GestureHandler() { * @param recorded handlers already recorded for the current touch * @param handler the handler being recorded */ - fun shouldBeginWithRecorded(recorded: List, handler: NativeViewGestureHandler): Boolean = true + fun shouldBeginWithRecordedHandlers(recorded: List, handler: NativeViewGestureHandler): Boolean = + true /** * Passes the event down to the underlying view using the correct 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 cb95aec3b8..1145ddf3b3 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 @@ -740,13 +740,15 @@ class RNGestureHandlerButtonViewManager : isTouched = false } - override fun shouldBeginWithRecorded(recorded: List, handler: NativeViewGestureHandler): Boolean = - recorded.all { - it.shouldRecognizeSimultaneously(handler) || - handler.shouldRecognizeSimultaneously(it) || - it.view == this || - it is HoverGestureHandler - } + override fun shouldBeginWithRecordedHandlers( + recorded: List, + handler: NativeViewGestureHandler, + ): Boolean = recorded.all { + it.shouldRecognizeSimultaneously(handler) || + handler.shouldRecognizeSimultaneously(it) || + it.view == this || + it is HoverGestureHandler + } private fun tryFreeingResponder() { if (touchResponder === this) {