From 31524a35e4b66fdac459b5d1127dfe387cb42743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Wed, 10 Jun 2026 17:18:36 +0200 Subject: [PATCH 1/3] Account for transform --- .../core/GestureHandlerOrchestrator.kt | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 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 5c17b1d34d..598d21b51d 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 @@ -546,13 +546,19 @@ class GestureHandlerOrchestrator( coords: FloatArray, pointerId: Int, event: MotionEvent, + // View and coordinates used for the `isWithinBounds` check. Defaults to `view`/`coords`, but the + // native detector path overrides them with its child's transform-aware bounds so the detector's + // interactive area follows child transforms (see `recordDetectorHandlersForPointer`). + boundsView: View = view, + boundsX: Float = coords[0], + boundsY: Float = coords[1], ): Boolean { var found = false handlerRegistry.getHandlersForView(view)?.let { synchronized(it) { for (handler in it) { // skip disabled and out-of-bounds handlers - if (!handler.isEnabled || !handler.isWithinBounds(view, coords[0], coords[1])) { + if (!handler.isEnabled || !handler.isWithinBounds(boundsView, boundsX, boundsY)) { continue } @@ -606,6 +612,37 @@ class GestureHandlerOrchestrator( return found } + /** + * Records the handlers attached to a native detector when no child consumed the touch. + * + * Delegates to [recordViewHandlersForPointer], but evaluates each handler's bounds in the + * coordinate space of the detector's child, so the detector's interactive area follows the child's + * transforms (e.g. `translateX`) instead of relying on the detector view's own (transform-agnostic) + * frame. For an identity transform the single child fills the detector, so this matches checking + * the detector's frame and `hitSlop` expansion (#4049) keeps working; for a translated child the + * interactive area follows the content, which fixes presses being stolen over revealed siblings in + * an open `ReanimatedSwipeable` (#4250). + * + * Multi-child detectors fall back to the detector's own frame, as there is no single child whose + * transform represents the content. + */ + private fun recordDetectorHandlersForPointer( + detector: RNGestureHandlerDetectorView, + coords: FloatArray, + pointerId: Int, + event: MotionEvent, + ): Boolean { + if (detector.childCount != 1) { + return recordViewHandlersForPointer(detector, coords, pointerId, event) + } + + val child = detector.getChildAt(0) + val childPoint = PointF() + transformPointToChildViewCoords(coords[0], coords[1], detector, child, childPoint) + + return recordViewHandlersForPointer(detector, coords, pointerId, event, child, childPoint.x, childPoint.y) + } + private fun extractGestureHandlers(event: MotionEvent) { val actionIndex = event.actionIndex val pointerId = event.getPointerId(actionIndex) @@ -690,8 +727,17 @@ class GestureHandlerOrchestrator( is ViewGroup -> { extractGestureHandlers(view, coords, pointerId, event).also { found -> // A child view is handling touch, also extract handlers attached to this view - if (found || view is RNGestureHandlerDetectorView) { + if (found) { recordViewHandlersForPointer(view, coords, pointerId, event) + } else if (view is RNGestureHandlerDetectorView) { + // No child consumed the touch, but we still record the detector's own handlers so + // that `hitSlop` expansion keeps working (see #4049). The detector's frame is the + // bounding box of its children's *layout* frames and does not follow child + // transforms (e.g. `translateX`), so checking it directly would let the detector + // steal presses over areas its content has been moved away from - this is what + // breaks buttons in an open ReanimatedSwipeable on Android (see #4250). Evaluate + // the handlers in the child's transform-aware coordinate space instead. + recordDetectorHandlersForPointer(view, coords, pointerId, event) } } } From c7bfc5f4695626daa76a58c9e52a507d25fc15ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Wed, 10 Jun 2026 17:40:46 +0200 Subject: [PATCH 2/3] Simplify --- .../core/GestureHandlerOrchestrator.kt | 70 +++++++------------ 1 file changed, 26 insertions(+), 44 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 598d21b51d..67d59cdbca 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 @@ -546,13 +546,27 @@ class GestureHandlerOrchestrator( coords: FloatArray, pointerId: Int, event: MotionEvent, - // View and coordinates used for the `isWithinBounds` check. Defaults to `view`/`coords`, but the - // native detector path overrides them with its child's transform-aware bounds so the detector's - // interactive area follows child transforms (see `recordDetectorHandlersForPointer`). - boundsView: View = view, - boundsX: Float = coords[0], - boundsY: Float = coords[1], + useChildBounds: Boolean = false, ): Boolean { + var boundsView = view + var boundsX = coords[0] + var boundsY = coords[1] + + // When `useChildBounds` is set, handler bounds are checked in the coordinate space of the view's + // single child instead of against the view itself. The caller only sets this for a native detector + // with exactly one child, so its interactive area follows the child's transforms (e.g. `translateX`) + // rather than the detector's transform-agnostic frame. For an identity transform the child fills the + // detector, so this matches the detector's own frame and `hitSlop` expansion (#4049) keeps working; + // for a translated child the area follows the content. + if (useChildBounds) { + val child = (view as RNGestureHandlerDetectorView).getChildAt(0) + val childPoint = PointF() + transformPointToChildViewCoords(coords[0], coords[1], view, child, childPoint) + boundsView = child + boundsX = childPoint.x + boundsY = childPoint.y + } + var found = false handlerRegistry.getHandlersForView(view)?.let { synchronized(it) { @@ -612,37 +626,6 @@ class GestureHandlerOrchestrator( return found } - /** - * Records the handlers attached to a native detector when no child consumed the touch. - * - * Delegates to [recordViewHandlersForPointer], but evaluates each handler's bounds in the - * coordinate space of the detector's child, so the detector's interactive area follows the child's - * transforms (e.g. `translateX`) instead of relying on the detector view's own (transform-agnostic) - * frame. For an identity transform the single child fills the detector, so this matches checking - * the detector's frame and `hitSlop` expansion (#4049) keeps working; for a translated child the - * interactive area follows the content, which fixes presses being stolen over revealed siblings in - * an open `ReanimatedSwipeable` (#4250). - * - * Multi-child detectors fall back to the detector's own frame, as there is no single child whose - * transform represents the content. - */ - private fun recordDetectorHandlersForPointer( - detector: RNGestureHandlerDetectorView, - coords: FloatArray, - pointerId: Int, - event: MotionEvent, - ): Boolean { - if (detector.childCount != 1) { - return recordViewHandlersForPointer(detector, coords, pointerId, event) - } - - val child = detector.getChildAt(0) - val childPoint = PointF() - transformPointToChildViewCoords(coords[0], coords[1], detector, child, childPoint) - - return recordViewHandlersForPointer(detector, coords, pointerId, event, child, childPoint.x, childPoint.y) - } - private fun extractGestureHandlers(event: MotionEvent) { val actionIndex = event.actionIndex val pointerId = event.getPointerId(actionIndex) @@ -731,13 +714,12 @@ class GestureHandlerOrchestrator( recordViewHandlersForPointer(view, coords, pointerId, event) } else if (view is RNGestureHandlerDetectorView) { // No child consumed the touch, but we still record the detector's own handlers so - // that `hitSlop` expansion keeps working (see #4049). The detector's frame is the - // bounding box of its children's *layout* frames and does not follow child - // transforms (e.g. `translateX`), so checking it directly would let the detector - // steal presses over areas its content has been moved away from - this is what - // breaks buttons in an open ReanimatedSwipeable on Android (see #4250). Evaluate - // the handlers in the child's transform-aware coordinate space instead. - recordDetectorHandlersForPointer(view, coords, pointerId, event) + // that `hitSlop` expansion keeps working. The detector's frame ignores + // child transforms, so for a single-child detector we check bounds in the child's + // transform-aware coordinate space - otherwise the detector would steal presses over + // areas its content has been moved away from. + val useChildBounds = view.childCount == 1 + recordViewHandlersForPointer(view, coords, pointerId, event, useChildBounds) } } } From 6d31d18c489c6467d9a446c98fabd7d56f1800f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Wed, 10 Jun 2026 17:52:05 +0200 Subject: [PATCH 3/3] Use tempPoint --- .../swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt | 2 +- 1 file changed, 1 insertion(+), 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 67d59cdbca..3d371e0076 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 @@ -560,7 +560,7 @@ class GestureHandlerOrchestrator( // for a translated child the area follows the content. if (useChildBounds) { val child = (view as RNGestureHandlerDetectorView).getChildAt(0) - val childPoint = PointF() + val childPoint = tempPoint transformPointToChildViewCoords(coords[0], coords[1], view, child, childPoint) boundsView = child boundsX = childPoint.x