Skip to content

Commit 653d519

Browse files
authored
[Android] Keep the press alive while a Pressable waits for another gesture (#4639)
## Description On Android, a `Pressable` with `requireToFail` dropped the whole press on a quick tap: `onPressIn` fired, `onPressOut` and `onPress` never did. When the awaited gesture won, the press was left without `onPressOut` too. iOS was fine. The long press gesture had an Android-only branch in `onTouchesUp` that reset the state machine and finalized the press as soon as the finger lifted. That assumes the button gesture has already finished by then. With `requireToFail` the button is still awaiting the other gesture when the finger lifts, so the reset ran first and the button's later FINALIZE or CANCEL met an empty state machine. The reset dates from #3571 and never acted on a working path: on a plain tap, drag-out, scroll steal or parent pan the button finishes first and `handlePressOut` has already finalized. On Android the native handler always ends or cancels on pointer up, so the press is always closed without it. This PR removes the branch, which also removes the dependency on the orchestrator's delivery order. The cancel branch added in #4633 loses its iOS gate, so a button cancelled while the press is held delivers `onPressOut` on every native platform. ## Test plan Tested on a Pixel 9 Pro emulator and an iPhone 17 simulator with the screen below. Tap each box once. Before, on Android, the second box logs only `onPressIn`. After, both boxes log `onPressIn`, `onPressOut`, `onPress`. A double tap on the second box logs `onPressIn`, `onPressOut` and the double tap. ```tsx import React, { useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { GestureDetector, GestureHandlerRootView, Pressable, useTapGesture, } from 'react-native-gesture-handler'; export default function Repro() { const [lines, setLines] = useState<string[]>([]); const log = (line: string) => setLines((current) => [...current.slice(-7), line]); const doubleTap = useTapGesture({ numberOfTaps: 2, runOnJS: true, onActivate: () => log('doubleTap activated'), }); return ( <GestureHandlerRootView style={styles.container}> <Pressable onPressIn={() => log('plain: onPressIn')} onPressOut={() => log('plain: onPressOut')} onPress={() => log('plain: onPress')}> <View style={styles.box}> <Text>Plain Pressable</Text> </View> </Pressable> <GestureDetector gesture={doubleTap}> <View> <Pressable requireToFail={doubleTap} onPressIn={() => log('requireToFail: onPressIn')} onPressOut={() => log('requireToFail: onPressOut')} onPress={() => log('requireToFail: onPress')}> <View style={styles.box}> <Text>Pressable with requireToFail</Text> </View> </Pressable> </View> </GestureDetector> <Text style={styles.log}>{lines.join('\n')}</Text> </GestureHandlerRootView> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', paddingTop: 40, gap: 24 }, box: { width: 300, height: 80, backgroundColor: '#b58df1', alignItems: 'center', justifyContent: 'center', }, log: { width: 300, fontSize: 14 }, }); ``` Also checked on both platforms that a plain tap, a hold, a drag out of the box, a disable under the finger, a drag inside an RN and an RNGH `ScrollView`, a parent pan, a nested `Pressable`, `hitSlop` and `unstable_pressDelay` behave as before.
1 parent 089f0af commit 653d519

1 file changed

Lines changed: 3 additions & 14 deletions

File tree

‎packages/react-native-gesture-handler/src/v3/components/StatefulPressable.tsx‎

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -311,13 +311,6 @@ const StatefulPressable = (props: PressableProps) => {
311311
pressableEvent
312312
);
313313
},
314-
onTouchesUp: () => {
315-
if (Platform.OS === 'android' && !isScreenReaderEnabled) {
316-
// Prevents potential soft-locks
317-
stateMachine.reset();
318-
handleFinalize();
319-
}
320-
},
321314
onTouchesCancel: (event) => {
322315
const pressableEvent = gestureTouchToPressableEvent(event);
323316
stateMachine.reset();
@@ -402,13 +395,9 @@ const StatefulPressable = (props: PressableProps) => {
402395
event.canceled ? StateMachineEvent.CANCEL : StateMachineEvent.FINALIZE
403396
);
404397

405-
// On iOS the button's cancel can land before the long press's touch
406-
// cancel, or without one, so deliver the press-out here
407-
if (
408-
Platform.OS === 'ios' &&
409-
event.canceled &&
410-
isCurrentlyPressed.current
411-
) {
398+
// The button's cancel can land before the long press's touch cancel,
399+
// or without one, so deliver the press-out here
400+
if (event.canceled && isCurrentlyPressed.current) {
412401
handlePressOut(
413402
pressInEventRef.current ??
414403
viewCenterToPressableEvent(dimensions.current),

0 commit comments

Comments
 (0)