Description
On Android, Pressable fires onPress when the touch was only meant to stop a fling.
Put a finger down on a list row while the list is still decelerating, then lift it without moving: the row under the finger receives a press. In a shop list that means opening a product page the user never tapped.
This is a regression introduced with the v3 Pressable. Using LegacyPressable, or passing any relation prop (simultaneousWith / requireToFail / block, which routes the component to StatefulPressable), makes the phantom press go away.
Root cause
Pressable without relation props renders PressableWithTouchable, which handles the press natively through ButtonViewGroup. The NativeViewGestureHandler the button manages for itself is attached with ACTION_TYPE_NONE:
https://github.com/software-mansion/react-native-gesture-handler/blob/main/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt#L484
When a native view grabs the touch lock — Android's ScrollView intercepts the ACTION_DOWN while its scroller is still running and calls requestDisallowInterceptTouchEvent(true) up the tree — RNGestureHandlerRootHelper.requestDisallowInterceptTouchEvent() reacts with GestureHandlerOrchestrator.cancelAllLegacyHandlers(), which only cancels handlers whose action type is ACTION_TYPE_JS_FUNCTION_OLD_API, ACTION_TYPE_JS_FUNCTION_NEW_API, ACTION_TYPE_REANIMATED_WORKLET or ACTION_TYPE_NATIVE_ANIMATED_EVENT.
The button's handler is none of those, so it survives, reaches STATE_END on the finger lift and dispatches the press. The framework's ACTION_CANCEL doesn't save it either, since RNGH delivers touches itself and ignores onInterceptTouchEvent.
The v2 Pressable was built on GestureDetector, so its handlers carried a JS action type and were cancelled by that same path — hence the regression.
LegacyPressable and StatefulPressable are unaffected for the same reason.
Steps to reproduce
- Render a list (
FlatList, FlashList or a plain ScrollView from react-native) whose rows are wrapped in Pressable from react-native-gesture-handler.
- Fling the list hard so it keeps scrolling on its own.
- While it decelerates, put a finger down on a row to stop the scroll.
- Lift the finger without moving it.
Expected: nothing happens, the touch only stopped the scroll.
Actual: onPress fires for the row under the finger.
Repro
import React, { useState } from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import { GestureHandlerRootView, Pressable } from 'react-native-gesture-handler';
const DATA = Array.from({ length: 80 }, (_, index) => `Item ${index}`);
export default function App() {
const [pressed, setPressed] = useState<string>('—');
return (
<GestureHandlerRootView style={styles.root}>
<View style={styles.banner}>
<Text style={styles.bannerText}>last onPress: {pressed}</Text>
</View>
<FlatList
data={DATA}
keyExtractor={(item) => item}
renderItem={({ item }) => (
<Pressable style={styles.row} onPress={() => setPressed(item)}>
<Text>{item}</Text>
</Pressable>
)}
/>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
root: { flex: 1 },
banner: { padding: 16, backgroundColor: '#eee' },
bannerText: { fontWeight: 'bold' },
row: { padding: 24, borderBottomWidth: 1, borderBottomColor: '#ddd' },
});
Fling the list, stop it with a finger, lift without moving: the banner shows the row that was under the finger. Swap the import for LegacyPressable and it stays at —.
Gesture Handler version
3.2.1
React Native version
0.86.2
Platforms
Android
JavaScript runtime
Hermes
Workflow
Using Expo Prebuild or an Expo development build
Architecture
New Architecture (Fabric)
Build type
Debug mode
Device
Real device
Description
On Android,
PressablefiresonPresswhen the touch was only meant to stop a fling.Put a finger down on a list row while the list is still decelerating, then lift it without moving: the row under the finger receives a press. In a shop list that means opening a product page the user never tapped.
This is a regression introduced with the v3
Pressable. UsingLegacyPressable, or passing any relation prop (simultaneousWith/requireToFail/block, which routes the component toStatefulPressable), makes the phantom press go away.Root cause
Pressablewithout relation props rendersPressableWithTouchable, which handles the press natively throughButtonViewGroup. TheNativeViewGestureHandlerthe button manages for itself is attached withACTION_TYPE_NONE:https://github.com/software-mansion/react-native-gesture-handler/blob/main/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt#L484
When a native view grabs the touch lock — Android's
ScrollViewintercepts theACTION_DOWNwhile its scroller is still running and callsrequestDisallowInterceptTouchEvent(true)up the tree —RNGestureHandlerRootHelper.requestDisallowInterceptTouchEvent()reacts withGestureHandlerOrchestrator.cancelAllLegacyHandlers(), which only cancels handlers whose action type isACTION_TYPE_JS_FUNCTION_OLD_API,ACTION_TYPE_JS_FUNCTION_NEW_API,ACTION_TYPE_REANIMATED_WORKLETorACTION_TYPE_NATIVE_ANIMATED_EVENT.The button's handler is none of those, so it survives, reaches
STATE_ENDon the finger lift and dispatches the press. The framework'sACTION_CANCELdoesn't save it either, since RNGH delivers touches itself and ignoresonInterceptTouchEvent.The v2
Pressablewas built onGestureDetector, so its handlers carried a JS action type and were cancelled by that same path — hence the regression.LegacyPressableandStatefulPressableare unaffected for the same reason.Steps to reproduce
FlatList,FlashListor a plainScrollViewfromreact-native) whose rows are wrapped inPressablefromreact-native-gesture-handler.Expected: nothing happens, the touch only stopped the scroll.
Actual:
onPressfires for the row under the finger.Repro
Fling the list, stop it with a finger, lift without moving: the banner shows the row that was under the finger. Swap the import for
LegacyPressableand it stays at—.Gesture Handler version
3.2.1
React Native version
0.86.2
Platforms
Android
JavaScript runtime
Hermes
Workflow
Using Expo Prebuild or an Expo development build
Architecture
New Architecture (Fabric)
Build type
Debug mode
Device
Real device