From 728cf49285027bec0b224d0ae5749604f1801dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Tue, 6 Oct 2026 18:56:16 +0200 Subject: [PATCH 1/3] [Android] Dispatch onPressIn when the native press starts --- .../RNGestureHandlerButtonViewManager.kt | 54 +++++++++++++++---- 1 file changed, 43 insertions(+), 11 deletions(-) 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 659473ba8d..7d056f5347 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 @@ -735,6 +735,10 @@ class RNGestureHandlerButtonViewManager : // container) instead of pressing right away. private var pressDeferredByPlatform = false + // Uptime of the DOWN that began the current press sequence; the long press timer is measured from it. + private var pressDownTime = 0L + private var pressStarted = false + override fun onHandlerUpdate(handler: NativeViewGestureHandler) { if (managedHandlerTag == null || handler.isWithinBounds == lastEventWasInside) { return @@ -768,18 +772,10 @@ class RNGestureHandlerButtonViewManager : // native dispatch of the same DOWN and the sweep that reads the flag. receivedNativeDown = false flushPendingPressOut() - dispatchJSEvent(EventType.PressIn, handler) + // PressIn and the long press timer follow `setPressed`, when the press feedback starts. + pressDownTime = SystemClock.uptimeMillis() + pressStarted = false longPressDetected = false - - if (hasLongPressHandler && longPressDuration >= 0) { - val runnable = Runnable { - pendingLongPress = null - longPressDetected = true - dispatchJSEvent(EventType.LongPress, handler) - } - pendingLongPress = runnable - this.handler?.postDelayed(runnable, longPressDuration.toLong()) - } } if (newState == GestureHandler.STATE_END || @@ -808,6 +804,41 @@ class RNGestureHandlerButtonViewManager : } } + // The framework defers the pressed state inside scrolling containers and drops it when a parent + // intercepts the DOWN (e.g. a fling catch), so PressIn and the long press timer follow the pressed + // state, not BEGAN. Like `View.CheckForTap`, long press is still measured from the DOWN. + private fun onPressedStateStarted() { + val moduleId = moduleId ?: return + val handlerTag = managedHandlerTag ?: return + val handler = + RNGestureHandlerModule.registries[moduleId]?.getHandler(handlerTag) as? NativeViewGestureHandler ?: return + + if (handler.state != GestureHandler.STATE_BEGAN && handler.state != GestureHandler.STATE_ACTIVE) { + return + } + + if (!lastEventWasInside) { + dispatchJSEvent(EventType.PressIn, handler) + } + + // Re-entry after leaving (cancelOnLeave off) reports PressIn again but never re-arms the timer. + if (pressStarted) { + return + } + pressStarted = true + + if (hasLongPressHandler && longPressDuration >= 0) { + val elapsed = SystemClock.uptimeMillis() - pressDownTime + val runnable = Runnable { + pendingLongPress = null + longPressDetected = true + dispatchJSEvent(EventType.LongPress, handler) + } + pendingLongPress = runnable + this.handler?.postDelayed(runnable, (longPressDuration - elapsed).coerceAtLeast(0)) + } + } + private fun dispatchJSEvent(type: EventType, handler: NativeViewGestureHandler) { dispatchEvent(RNGestureHandlerButtonEvent.obtain(this, handler, type)) @@ -1579,6 +1610,7 @@ class RNGestureHandlerButtonViewManager : if (pressed) { animatePressIn() + onPressedStateStarted() } else { animatePressOut() } From 3f987cbf5328d8c92993f2236938d170b9d7fec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Tue, 6 Oct 2026 19:07:38 +0200 Subject: [PATCH 2/3] Newline --- .../gesturehandler/react/RNGestureHandlerButtonViewManager.kt | 1 + 1 file changed, 1 insertion(+) 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 7d056f5347..7a53326344 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 @@ -825,6 +825,7 @@ class RNGestureHandlerButtonViewManager : if (pressStarted) { return } + pressStarted = true if (hasLongPressHandler && longPressDuration >= 0) { From cfd5ba9af0edf887c2d14ee4becfdd39a68efb50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Mon, 12 Oct 2026 10:43:39 +0200 Subject: [PATCH 3/3] TalkBack --- .../react/RNGestureHandlerButtonViewManager.kt | 9 ++++++++- 1 file changed, 8 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 7d2febd2c3..1a0cca04d3 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 @@ -774,10 +774,17 @@ class RNGestureHandlerButtonViewManager : // native dispatch of the same DOWN and the sweep that reads the flag. receivedNativeDown = false flushPendingPressOut() - // PressIn and the long press timer follow `setPressed`, when the press feedback starts. pressDownTime = SystemClock.uptimeMillis() pressStarted = false longPressDetected = false + + // Touch-driven presses report PressIn and arm the long press timer from `setPressed`, when the + // press feedback starts. A synthetic activation (TalkBack, keyboard) has no touch and no + // pressed state to follow, so it reports PressIn right away. Nothing deferred it either. + if (!handler.hasTrackedPointers) { + pressDeferredByPlatform = false + dispatchJSEvent(EventType.PressIn, handler) + } } if (newState == GestureHandler.STATE_END ||