Skip to content

Commit b886eef

Browse files
authored
[Android] Check only the handlers extracted for the same pointer (#4632)
## Description On Android, while one finger rests on an empty part of the screen, a tap with a second finger on a `RectButton`, `Touchable` or `Pressable` does nothing: no `onBegin`, `onPressIn` or `onPress`. The same tap with no other finger down presses the button. The press right after the swallowed one is lost as well. Since #4127 the orchestrator asks a button, when it is recorded, whether it may begin given the handlers already recorded, and the button refuses any handler that is not simultaneous with it, not on its own view and not a hover. The list passed is the orchestrator's whole `gestureHandlers`, which also holds the handlers recorded by other pointers. A resting finger keeps the root view's own handler recorded, so the second finger's button is cancelled before it begins. With one finger the root handler is recorded after the button, which is why the check only broke with two. `recordHandlerIfNotPresent` now takes the pointer being extracted and passes only the handlers that already track it. Extraction is bottom-up and every handler starts tracking the pointer right after it is recorded, so those are exactly the handlers under the button on this touch, which is what #4127 meant by "already extracted". Handlers from other pointers no longer take part and keep being arbitrated at activation time. Call sites without a pointer (the TalkBack and keyboard synthetic click, manual state set from JS) pass an empty list, since nothing is under a touch that does not exist. Fixes #4627. ## Test plan - Repro from the issue in the empty example: with a finger resting on a plain view, a second finger presses `RectButton`, `Touchable` and `Pressable`; single-finger presses afterwards still work. Verified on a Pixel 9 Pro emulator (API 36) and a Galaxy S24. - `argent flow run nested-touchables-test` (the #4127 behaviour) passes. <details> <summary>Test code:</summary> ```tsx // Repro for #4627: on Android, a second finger's tap on a button does nothing // while another finger rests on the screen. The resting finger records the // root view's handler; the button's `shouldBeginWithRecordedHandlers` (#4127) // then refuses it and the button is cancelled before it begins. import React, { useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { GestureDetector, GestureHandlerRootView, Pressable, RectButton, Touchable, useManualGesture, } from 'react-native-gesture-handler'; // 1. Rest one finger on the grey area. // 2. Tap each row with a second finger: expected a log + counter bump, got nothing. // 3. Lift both, tap each row with one finger: it works. export default function SecondFingerBesideRestingFinger() { const [counts, setCounts] = useState({ rect: 0, touchable: 0, pressable: 0, tap: 0, }); const bump = (key: keyof typeof counts) => { console.log(`[${key}] press`); setCounts((c) => ({ ...c, [key]: c[key] + 1 })); }; // Control: a manual gesture is not a button, so #4127's check does not apply // to it. It counts touch-downs, so it ignores how many pointers are on screen. const tap = useManualGesture({ runOnJS: true, onTouchesDown: (e) => { console.log('[control] touchesDown', e.numberOfTouches); bump('tap'); }, onTouchesUp: (e) => console.log('[control] touchesUp', e.numberOfTouches), }); return ( <GestureHandlerRootView style={styles.container}> <View testID="resting-area" style={styles.restingArea}> <Text>rest a finger here</Text> </View> <RectButton testID="rect-button" style={styles.button} onBegin={() => console.log('[rect] onBegin')} onFinalize={() => console.log('[rect] onFinalize')} onTouchesDown={(e) => console.log('[rect] touchesDown', e.numberOfTouches) } onTouchesUp={(e) => console.log('[rect] touchesUp', e.numberOfTouches)} onTouchesCancel={() => console.log('[rect] touchesCancel')} onPress={() => bump('rect')}> <Text>RectButton: {counts.rect}</Text> </RectButton> <Touchable testID="touchable" style={styles.button} onPressIn={() => console.log('[touchable] onPressIn')} onPress={() => bump('touchable')}> <Text>Touchable: {counts.touchable}</Text> </Touchable> <Pressable testID="pressable" style={styles.button} onPressIn={() => console.log('[pressable] onPressIn')} onPress={() => bump('pressable')}> <Text>Pressable: {counts.pressable}</Text> </Pressable> <GestureDetector gesture={tap}> <View testID="tap-detector" style={[styles.button, styles.control]}> <Text>Manual gesture (control): {counts.tap}</Text> </View> </GestureDetector> </GestureHandlerRootView> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 24, padding: 16, }, restingArea: { width: 240, height: 80, backgroundColor: '#e0e0e0', alignItems: 'center', justifyContent: 'center', }, button: { width: 240, height: 48, backgroundColor: '#b58df1', alignItems: 'center', justifyContent: 'center', }, control: { backgroundColor: '#8fd3b5', }, }); ``` </details>
1 parent 4e18d5f commit b886eef

2 files changed

Lines changed: 8 additions & 6 deletions

File tree

‎packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ open class GestureHandler {
258258
trackedPointersIDsCount--
259259
}
260260

261-
private fun isTrackingPointer(pointerId: Int) = trackedPointerIDs[pointerId] != -1
261+
fun isTrackingPointer(pointerId: Int) = trackedPointerIDs[pointerId] != -1
262262

263263
private fun needAdapt(event: MotionEvent): Boolean {
264264
if (event.pointerCount != trackedPointersIDsCount) {

‎packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ class GestureHandlerOrchestrator(
539539
}
540540
}
541541

542-
fun recordHandlerIfNotPresent(handler: GestureHandler, view: View?) {
542+
fun recordHandlerIfNotPresent(handler: GestureHandler, view: View?, pointerId: Int = -1) {
543543
if (gestureHandlers.contains(handler)) {
544544
return
545545
}
@@ -549,7 +549,9 @@ class GestureHandlerOrchestrator(
549549
handler.activationIndex = Int.MAX_VALUE
550550
handler.prepare(view, this)
551551

552-
if (!handler.shouldBeginWithRecordedHandlers(gestureHandlers)) {
552+
// Only handlers extracted for the same pointer (they already track it) may block this one
553+
val recorded = if (pointerId >= 0) gestureHandlers.filter { it.isTrackingPointer(pointerId) } else emptyList()
554+
if (!handler.shouldBeginWithRecordedHandlers(recorded)) {
553555
handler.cancel()
554556
}
555557

@@ -595,7 +597,7 @@ class GestureHandlerOrchestrator(
595597

596598
if (handler.isEnabled && handler.isWithinBounds(view, coords[0], coords[1])) {
597599
found = true
598-
recordHandlerIfNotPresent(handler, parentViewGroup)
600+
recordHandlerIfNotPresent(handler, parentViewGroup, pointerId)
599601
handler.startTrackingPointer(pointerId)
600602
}
601603
}
@@ -660,7 +662,7 @@ class GestureHandlerOrchestrator(
660662
continue
661663
}
662664

663-
recordHandlerIfNotPresent(handler, view)
665+
recordHandlerIfNotPresent(handler, view, pointerId)
664666
handler.startTrackingPointer(pointerId)
665667
found = true
666668
}
@@ -697,7 +699,7 @@ class GestureHandlerOrchestrator(
697699
continue
698700
}
699701

700-
recordHandlerIfNotPresent(handler, view)
702+
recordHandlerIfNotPresent(handler, view, pointerId)
701703
handler.startTrackingPointer(pointerId)
702704
found = true
703705
}

0 commit comments

Comments
 (0)