[Android] Properly handle requestDisallowInterceptTouchEvent for v3 - #4367
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adjusts Android’s requestDisallowInterceptTouchEvent handling to avoid canceling v3 gestures too broadly (notably inside pagers), by making cancellation more targeted across root/orchestrator and v3 detector/button views.
Changes:
- Add
GestureHandlerOrchestrator.cancelAllLegacyHandlers()and use it from the root helper instead of triggering cancellation via the internal root handler. - Override
requestDisallowInterceptTouchEventin v3 host detector and button view to cancel only relevant handlers (and skip cancellation while the orchestrator is handling touch). - Update root view lookup to prefer the nearest enabled
RNGestureHandlerRootViewand expose the orchestrator via the root view/root helper.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt | Exposes orchestrator and returns nearest enabled GH root view when searching ancestors. |
| packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt | Switches root-level cancellation to cancelAllLegacyHandlers and attaches root handler with ACTION_TYPE_NONE. |
| packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorView.kt | Adds requestDisallowInterceptTouchEvent override to cancel v3 handlers attached via the host detector. |
| packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt | Adds requestDisallowInterceptTouchEvent override to cancel the button’s managed v3 handler. |
| packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt | Introduces cancelAllLegacyHandlers() to selectively cancel v1/v2 action types. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
934af71 to
c30a19c
Compare
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
🚧 Files skipped from review as they are similar to previous changes (5)
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe Android gesture-handler implementation now exposes orchestrator state, centralizes legacy-handler cancellation, and cancels attached handlers during disallowed touch interception. Root lookup now prefers enabled gesture-handler roots. ChangesNative interception cancellation
Sequence Diagram(s)sequenceDiagram
participant ButtonViewGroup
participant RNGestureHandlerDetectorView
participant RNGestureHandlerRootView
participant GestureHandlerOrchestrator
ButtonViewGroup->>RNGestureHandlerDetectorView: requestDisallowInterceptTouchEvent(true)
RNGestureHandlerDetectorView->>RNGestureHandlerRootView: find gesture-handler root
RNGestureHandlerRootView->>GestureHandlerOrchestrator: read isHandlingTouch
RNGestureHandlerDetectorView->>GestureHandlerOrchestrator: cancel attached handlers
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
Comment |
…m/swmansion/gesturehandler/react/RNGestureHandlerDetectorView.kt Co-authored-by: Michał Bert <63123542+m-bert@users.noreply.github.com>
dd2e6bc to
dc49bcc
Compare
…867) Bumps \[react-native-gesture-handler\](https://github.com/software-mansion/react-native-gesture-handler) from 2.32.0 to 3.2.1. Release notes _Sourced from [react-native-gesture-handler's releases](https://github.com/software-mansion/react-native-gesture-handler/releases)._ > v3.2.1 > ------ > > 🐛 Bug fixes > ------------ > > * Forward press handlers as `testOnly_*` in `PressableWithTouchable` by [`@huextrat`](https://github.com/huextrat) in [software-mansion/react-native-gesture-handler#4416](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4416) > > 🔢 Miscellaneous > ---------------- > > * Update `Pressable` props by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4421](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4421) > > **Full Changelog**: [https://github.com/software-mansion/react-native-gesture-handler/compare/v3.2.0...v3.2.1](https://github.com/software-mansion/react-native-gesture-handler/compare/v3.2.0...v3.2.1) > > v3.2.0 > ------ > > ❗ Important changes > ------------------- > > * feat: Adopt AGP v9 by [`@hurali97`](https://github.com/hurali97) in [software-mansion/react-native-gesture-handler#4263](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4263) > * Implement `Pressable` based on `Touchable` by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4411](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4411) > * \[Android\] Add hover callbacks to Touchable by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4396](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4396) > * \[iOS\] Add hover callbacks to Touchable by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4397](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4397) > * \[Web\] Add hover callbacks to Touchable by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4398](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4398) > * \[Web\] Refactor `Touchable` not to rely on `GestureDetector` by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4344](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4344) > * \[iOS\] Refactor `Touchable` not to rely on `GestureDetector` by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4343](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4343) > * \[Android\] Refactor `Touchable` not to rely on `GestureDetector` by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4342](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4342) > * Fix fatal crash `Cannot read property 'translationX' of undefined` when a touch event is serialized without `allTouches` by [`@huextrat`](https://github.com/huextrat) in [software-mansion/react-native-gesture-handler#4316](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4316) > > 👍 Improvements > --------------- > > * \[Android\] Skip the underlay drawable when it can never be visible by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4359](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4359) > * \[Android\] Apply the button's managed handler config once per prop transaction by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4357](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4357) > * \[Android\] Configure the button's handler directly instead of through a `ReadableMap` by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4358](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4358) > > 🐛 Bug fixes > ------------ > > * \[Android\] Guard update events to only be dispatched in ACTIVE state by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4332](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4332) > * Pass empty callbacks to UI when `runOnJS` is `true` by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4326](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4326) > * \[Web\] Fix incorrectly calculated `timeDelta` by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4329](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4329) > * \[Web\] Fix incorrect `Tap` offset by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4330](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4330) > * Fix `minVelocity` props behavior by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4327](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4327) > * \[Android\] Fix `minDistance` being reset by partial config updates by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4347](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4347) > * Move Interceptor on `ScrollView`, not its content by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4331](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4331) > * \[iOS\] Re-sync layer opacity and transform from retained props when recycling buttons by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4360](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4360) > * \[Android\] Properly handle `requestDisallowInterceptTouchEvent` for v3 by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4367](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4367) > * Fix `Touchable` not respecting `keyboardShouldPersistTaps="handled"` by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4372](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4372) > * \[macOS\] Fix touch events never being delivered by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4390](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4390) > * fix: crash when mount listener fires after GestureDetector unmount by [`@kosmydel`](https://github.com/kosmydel) in [software-mansion/react-native-gesture-handler#4268](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4268) > * \[macOS\] Fix `Pan` activation criteria being ignored by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4387](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4387) > * \[macOS\] Fix `manualActivation` never blocking gesture activation by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4389](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4389) > * \[iOS\] Fix touch events never being delivered to `VirtualDetector` handlers by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4392](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4392) > * \[iOS\] Fix gestures attached via `VirtualGestureDetector` never recognizing continuous gestures by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4393](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4393) > * \[macOS\] Fix `Fling` not sending touch events and begin/end states consistently by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4395](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4395) > * \[Android\] Fix handlers cancelled while awaiting leaking in the orchestrator by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4402](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4402) ... (truncated) Commits * [`62f0f7d`](software-mansion/react-native-gesture-handler@62f0f7d) Release v3.2.1 * [`4716425`](software-mansion/react-native-gesture-handler@4716425) Merge branch '3.2-stable' of github.com:software-mansion/react-native-gesture... * [`f0ae48c`](software-mansion/react-native-gesture-handler@f0ae48c) Update `Pressable` props ([#4421](https://redirect.github.com/software-mansion/react-native-gesture-handler/issues/4421)) * [`5f0f0d8`](software-mansion/react-native-gesture-handler@5f0f0d8) Forward press handlers as `testOnly_*` in `PressableWithTouchable` ([#4416](https://redirect.github.com/software-mansion/react-native-gesture-handler/issues/4416)) * [`0a91db7`](software-mansion/react-native-gesture-handler@0a91db7) Release v3.2.0 * [`44046a6`](software-mansion/react-native-gesture-handler@44046a6) \[Android\] Resolve the button event dispatcher by react tag ([#4415](https://redirect.github.com/software-mansion/react-native-gesture-handler/issues/4415)) * [`2469c1d`](software-mansion/react-native-gesture-handler@2469c1d) Derive `Pressable` pressed state from `testOnly_pressed` ([#4414](https://redirect.github.com/software-mansion/react-native-gesture-handler/issues/4414)) * [`3f1bf74`](software-mansion/react-native-gesture-handler@3f1bf74) Clear pending timers on unmount in StatefulPressable ([#4413](https://redirect.github.com/software-mansion/react-native-gesture-handler/issues/4413)) * [`50ae6a1`](software-mansion/react-native-gesture-handler@50ae6a1) \[General\] Default GestureDetector moduleId to -1 ([#4412](https://redirect.github.com/software-mansion/react-native-gesture-handler/issues/4412)) * [`8b661c9`](software-mansion/react-native-gesture-handler@8b661c9) Implement `Pressable` based on `Touchable` ([#4411](https://redirect.github.com/software-mansion/react-native-gesture-handler/issues/4411)) * Additional commits viewable in [compare view](software-mansion/react-native-gesture-handler@v2.32.0...v3.2.1)
…e touch (#4441) ## Description `Pressable` without relation props presses natively through `ButtonViewGroup`, whose managed `NativeViewGestureHandler` is attached with `ACTION_TYPE_NONE`. RNGH delivers touches through the orchestrator regardless of what happens in the native dispatch, so when a native `ScrollView` takes the gesture over, nothing stops the handler - it reaches `STATE_END` on lift and fires a press. This shows up in three ways: - fling catch: the `ScrollView` intercepts `DOWN` while decelerating, the button never sees any native event, yet `onPress` fires on lift (#4432) - drag: the `ScrollView` intercepts on `MOVE` when the finger starts scrolling from a row, and `onPress` still fires on lift ([comment](#4432 (comment))) - long press while scrolling: the content moves with the finger, so the pointer never leaves the row and the long-press timer posted on `BEGAN` fires mid-scroll (same comment) In all three the `ScrollView` calls `requestDisallowInterceptTouchEvent(true)`, but the existing sweep (`cancelAllLegacyHandlers`) only cancels action-driven handlers, and the `ButtonViewGroup` override from #4367 never runs since the request only bubbles up from the `ScrollView`. Cancelling button handlers directly at request time (the #4433 approach) is not valid either: an eager disallow-intercept (`react-native-pager-view`'s `NestedScrollableHost` requests it on `DOWN` whenever it's nested inside another `ViewPager`, without intercepting anything) is indistinguishable from a real interception at that moment, so every `Pressable` inside nested pagers (e.g. material top tabs in a pager) would go dead - the regression class #4367 fixed. The two can be told apart by when the grab happened and whether the native dispatch still reached the button: - `ButtonViewGroup` tracks `receivedNativeDown` - set in `dispatchTouchEvent` (handler delivery bypasses it), reset on `BEGAN`, which the orchestrator dispatches before the native dispatch of the same `DOWN`. - `RNGestureHandlerRootHelper` records the disallow request, and once the root view finishes `super.dispatchTouchEvent` runs `cancelHandlersOnNativeTouchGrab`, cancelling handlers whose hook opts in. - The hook decides via `shouldCancelOnNativeTouchGrab(grabbedMidGesture) = grabbedMidGesture || !receivedNativeDown`: a grab on any pass after `DOWN` means actual dragging (cancel, matching what the legacy `Pressable` and RN's `Pressable` do), while a grab during the `DOWN` pass spares a button that received that `DOWN` (a defensive disallow lets the event through). Only `ButtonViewGroup` opts into the hook, so handlers attached to detectors, scrollables and text inputs are unaffected. The cost on passes without a disallow request is a single boolean check. Fixes #4432 Supersedes #4433 ## Test plan Repro below: a `SectionList` with `Pressable` rows (`onPress` + `onLongPress`), a `Pressable` and a long-press `GestureDetector` inside nested `PagerView`s (the eager-disallow setup from #2383), and an engine toggle (v3 / `LegacyPressable` / RN `Pressable`). All runs on the same emulator, main vs this PR: | scenario | main | this PR | | --- | --- | --- | | fling the list, touch a row to stop it, lift | phantom `onPress` | nothing | | put a finger on a row and drag-scroll, lift | phantom `onPress` | nothing | | hold a row while drag-scrolling past 500 ms | phantom `onLongPress` | nothing | | tap a row on a settled list | `onPress` | `onPress` | | stationary long press on a row | `onLongPress` | `onLongPress` | | tap the `Pressable` inside nested pagers | `onPress` | `onPress` | | long press the detector box inside nested pagers (#2383) | activates | activates | `LegacyPressable` behaves the same in the list scenarios; inside nested pagers it doesn't fire on main either - its handlers are cancelled on any disallow-intercept request, which is the pre-existing legacy behavior this PR doesn't change. RN's `Pressable` doesn't go through RNGH and is clean everywhere. <details> <summary>Repro</summary> ```tsx import React, { useState } from 'react'; import { Pressable as RNPressable, SectionList, StyleSheet, Text, View, } from 'react-native'; import PagerView from 'react-native-pager-view'; import { GestureDetector, LegacyPressable, Pressable, useLongPressGesture, } from 'react-native-gesture-handler'; const SECTIONS = Array.from({ length: 8 }, (_, section) => ({ title: `Section ${section}`, data: Array.from({ length: 10 }, (_, index) => `Item ${section}-${index}`), })); const ENGINES = ['Pressable (v3)', 'LegacyPressable', 'RN Pressable'] as const; const COMPONENTS = [Pressable, LegacyPressable, RNPressable] as const; function LongPressBox({ onLongPress }: { onLongPress: () => void }) { const longPress = useLongPressGesture({ runOnJS: true, onActivate: onLongPress, }); return ( <GestureDetector gesture={longPress}> <View style={styles.gestureBox} /> </GestureDetector> ); } export default function EmptyExample() { const [engine, setEngine] = useState(0); const [lastEvent, setLastEvent] = useState('none'); const [eventCount, setEventCount] = useState(0); const Row = COMPONENTS[engine] as typeof Pressable; const report = (kind: string, item: string) => { setLastEvent(`${kind} ${item}`); setEventCount((count) => count + 1); }; return ( <View style={styles.root}> <View style={styles.banner}> <Text style={styles.bannerText}>engine: {ENGINES[engine]}</Text> <Text style={styles.bannerText}> last: {lastEvent} (count: {eventCount}) </Text> <Pressable style={styles.toggle} onPress={() => { setEngine((current) => (current + 1) % ENGINES.length); setLastEvent('none'); setEventCount(0); }}> <Text style={styles.toggleText}>Toggle engine</Text> </Pressable> </View> {/* Nested pagers: the inner pager's NestedScrollableHost calls requestDisallowInterceptTouchEvent(true) on ACTION_DOWN only when it sits inside another ViewPager2 — the eager-disallow case from #4367. */} <PagerView style={styles.pager} initialPage={0}> <View key="outer-a" style={styles.page}> <PagerView style={styles.innerPager} initialPage={0}> <View key="a" style={[styles.page, styles.pageRow]}> <Row style={styles.pagerButton} onPress={() => report('press', 'pager-button')}> <Text style={styles.toggleText}>Pager button</Text> </Row> {/* The #2383 setup: a long-press gesture inside nested pagers (material top tabs are pager-view underneath). */} <LongPressBox onLongPress={() => report('gesture', 'pager-box')} /> </View> <View key="b" style={styles.page}> <Text>Page B</Text> </View> </PagerView> </View> <View key="outer-b" style={styles.page}> <Text>Outer page B</Text> </View> </PagerView> <SectionList sections={SECTIONS} keyExtractor={(item) => item} renderSectionHeader={({ section }) => ( <Text style={styles.sectionHeader}>{section.title}</Text> )} renderItem={({ item }) => ( <Row style={styles.row} onPress={() => report('press', item)} onLongPress={() => report('longPress', item)}> <Text>{item}</Text> </Row> )} /> </View> ); } const styles = StyleSheet.create({ root: { flex: 1, }, banner: { padding: 16, gap: 8, backgroundColor: '#eee', }, bannerText: { fontWeight: 'bold', }, toggle: { alignSelf: 'flex-start', paddingVertical: 8, paddingHorizontal: 16, borderRadius: 8, backgroundColor: 'steelblue', }, toggleText: { color: 'white', }, sectionHeader: { paddingHorizontal: 24, paddingVertical: 8, fontWeight: 'bold', backgroundColor: '#ddd', }, row: { padding: 24, borderBottomWidth: 1, borderBottomColor: '#ddd', }, pager: { height: 110, borderBottomWidth: 2, borderBottomColor: '#bbb', }, page: { alignItems: 'center', justifyContent: 'center', }, pageRow: { flexDirection: 'row', gap: 16, }, gestureBox: { width: 64, height: 44, borderRadius: 8, backgroundColor: 'crimson', }, innerPager: { alignSelf: 'stretch', flex: 1, }, pagerButton: { paddingVertical: 12, paddingHorizontal: 24, borderRadius: 8, backgroundColor: 'darkorange', }, }); ``` </details>
Description
On Android, when a native view calls
requestDisallowInterceptTouchEvent(e.g. a pager starting a swipe),RNGestureHandlerRootViewreacted by cancelling all handlers registered on the root.react-native-pager-viewcalls it eagerly on touch down, so any v3 gesture rendered inside a pager (e.g. inside material top tabs) was cancelled before it could activate — a long press nested in top tabs never activated at all.This PR makes the cancellation targeted:
GestureHandlerOrchestrator.cancelAllLegacyHandlers, instead of the old trick of activating the internalRootViewGestureHandler. The root handler is now attached withACTION_TYPE_NONEso it's excluded from that sweep (and no longer sends dead events to JS).requestDisallowInterceptTouchEventoverrides onRNGestureHandlerDetectorViewandButtonViewGroup. Since the request only bubbles upward from the requesting view, only handlers attached to its ancestors are cancelled — handlers below the requester (like the long press under the pager) keep working.isHandlingTouch), mirroring the existingpassingTouchguard, so disallow requests caused by RNGH's own event delivery don't cancel gestures.findGestureHandlerRootViewnow returns the nearest enabled root view so the checks above consult the orchestrator that actually manages the subtree.Test plan
Tested on reproducer from #2383
Screen.Recording.2026-07-31.at.11.38.38.mov