From 37fc93e837e13fa61ad8d20716fb30fad123f1ba Mon Sep 17 00:00:00 2001 From: huamurui Date: Sat, 24 Oct 2026 08:49:05 +0800 Subject: [PATCH 1/2] fix(android): cancel native touches before terminal cleanup --- .../gesturehandler/core/GestureHandlerOrchestrator.kt | 9 +++++++-- .../gesturehandler/react/RNGestureHandlerRootHelper.kt | 10 +++++++--- 2 files changed, 14 insertions(+), 5 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 71ace5fd8b..8d308e8bfa 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 @@ -21,6 +21,7 @@ class GestureHandlerOrchestrator( private val viewConfigHelper: ViewConfigurationHelper, private val rootView: ViewGroup, private val onJSResponderCancelListener: OnJSResponderCancelListener, + private val onTouchEventProcessed: (MotionEvent) -> Unit = {}, ) { /** * Minimum alpha (value from 0 to 1) that should be set to a view so that it can be treated as a @@ -74,6 +75,12 @@ class GestureHandlerOrchestrator( cancelAll() } deliverEventToGestureHandlers(event) + // A discrete handler can start interception on UP. Cancel native touches before losing + // their DOWN positions or resetting the native handlers that still own their views. + onTouchEventProcessed(event) + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + pointerDownPoints.clear() + } isHandlingTouch = false if (finishedHandlersCleanupScheduled && handlingChangeSemaphore == 0) { cleanupFinishedHandlers() @@ -765,8 +772,6 @@ class GestureHandlerOrchestrator( pointerDownPoints.put(event.getPointerId(index), PointF(event.getX(index), event.getY(index))) MotionEvent.ACTION_POINTER_UP -> pointerDownPoints.remove(event.getPointerId(index)) - MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> - pointerDownPoints.clear() } } 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 baba360c10..03cbc98229 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 @@ -56,6 +56,7 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView: RNViewConfigurationHelper(), rootView, onJSResponderCancelListener, + ::onTouchEventProcessed, ).apply { minimumAlphaForTraversal = MIN_ALPHA_FOR_TOUCH } @@ -185,14 +186,17 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView: orchestrator!!.onTouchEvent(event) passingTouch = false + return shouldIntercept + } + + private fun onTouchEventProcessed(event: MotionEvent) { // On the transition into interception, cancel the native views the pointers landed on - the - // framework's ACTION_CANCEL never reaches them since RNGH ignores `onInterceptTouchEvent` + // framework's ACTION_CANCEL never reaches them since RNGH ignores `onInterceptTouchEvent`. + // Keep this inside the orchestrator's dispatch so its own CANCEL is not a native touch grab. if (shouldIntercept && !wasIntercepting) { orchestrator!!.cancelTouchesInInterceptedViews(event) } wasIntercepting = shouldIntercept - - return shouldIntercept } fun activateNativeHandlers(view: View) { From 0edff422a597c6f03586d3c917b4c8f407138fd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Tue, 27 Oct 2026 11:05:53 +0100 Subject: [PATCH 2/2] Schedule intercepted views cancel instead of passing a callback --- .../core/GestureHandlerOrchestrator.kt | 33 ++++++++++++------- .../react/RNGestureHandlerRootHelper.kt | 17 +++------- 2 files changed, 26 insertions(+), 24 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 8d308e8bfa..af1f97c856 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 @@ -21,7 +21,6 @@ class GestureHandlerOrchestrator( private val viewConfigHelper: ViewConfigurationHelper, private val rootView: ViewGroup, private val onJSResponderCancelListener: OnJSResponderCancelListener, - private val onTouchEventProcessed: (MotionEvent) -> Unit = {}, ) { /** * Minimum alpha (value from 0 to 1) that should be set to a view so that it can be treated as a @@ -57,6 +56,7 @@ class GestureHandlerOrchestrator( private set private var handlingChangeSemaphore = 0 private var finishedHandlersCleanupScheduled = false + private var interceptedViewsCancelScheduled = false private var activationIndex = 0 /** @@ -75,12 +75,11 @@ class GestureHandlerOrchestrator( cancelAll() } deliverEventToGestureHandlers(event) - // A discrete handler can start interception on UP. Cancel native touches before losing - // their DOWN positions or resetting the native handlers that still own their views. - onTouchEventProcessed(event) - if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { - pointerDownPoints.clear() + if (interceptedViewsCancelScheduled) { + interceptedViewsCancelScheduled = false + cancelTouchesInInterceptedViews(event) } + releasePointerDownPoints(event) isHandlingTouch = false if (finishedHandlersCleanupScheduled && handlingChangeSemaphore == 0) { cleanupFinishedHandlers() @@ -766,16 +765,26 @@ class GestureHandlerOrchestrator( } private fun trackPointerDownPoints(event: MotionEvent) { - val index = event.actionIndex + val action = event.actionMasked + if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) { + val index = event.actionIndex + pointerDownPoints.put(event.getPointerId(index), PointF(event.getX(index), event.getY(index))) + } + } + + // Runs after the event is handled, so a sweep started by a lifting pointer still sees it. + private fun releasePointerDownPoints(event: MotionEvent) { when (event.actionMasked) { - MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> - pointerDownPoints.put(event.getPointerId(index), PointF(event.getX(index), event.getY(index))) - MotionEvent.ACTION_POINTER_UP -> - pointerDownPoints.remove(event.getPointerId(index)) + MotionEvent.ACTION_POINTER_UP -> pointerDownPoints.remove(event.getPointerId(event.actionIndex)) + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> pointerDownPoints.clear() } } - fun cancelTouchesInInterceptedViews(event: MotionEvent) { + fun scheduleInterceptedViewsCancel() { + interceptedViewsCancelScheduled = true + } + + private fun cancelTouchesInInterceptedViews(event: MotionEvent) { viewsToCancel.clear() for (i in 0 until pointerDownPoints.size()) { val point = pointerDownPoints.valueAt(i) 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 03cbc98229..0467c2ef47 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 @@ -20,7 +20,6 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView: private val jsGestureHandler: GestureHandler? val rootView: ViewGroup private var shouldIntercept = false - private var wasIntercepting = false private var passingTouch = false private var passingNativeTouch = false private var nativeTouchGrabRequested = false @@ -56,7 +55,6 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView: RNViewConfigurationHelper(), rootView, onJSResponderCancelListener, - ::onTouchEventProcessed, ).apply { minimumAlphaForTraversal = MIN_ALPHA_FOR_TOUCH } @@ -116,6 +114,11 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView: override fun onHandleHover(event: MotionEvent, sourceEvent: MotionEvent) = handleEvent(event) override fun onCancel() { + // On the transition into interception, cancel the native views the pointers landed on - the + // framework's ACTION_CANCEL never reaches them since RNGH ignores `onInterceptTouchEvent`. + if (!shouldIntercept) { + orchestrator?.scheduleInterceptedViewsCancel() + } shouldIntercept = true } } @@ -189,16 +192,6 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView: return shouldIntercept } - private fun onTouchEventProcessed(event: MotionEvent) { - // On the transition into interception, cancel the native views the pointers landed on - the - // framework's ACTION_CANCEL never reaches them since RNGH ignores `onInterceptTouchEvent`. - // Keep this inside the orchestrator's dispatch so its own CANCEL is not a native touch grab. - if (shouldIntercept && !wasIntercepting) { - orchestrator!!.cancelTouchesInInterceptedViews(event) - } - wasIntercepting = shouldIntercept - } - fun activateNativeHandlers(view: View) { orchestrator?.activateNativeHandlersForView(view) }