Skip to content

Commit 6b0ca7b

Browse files
authored
Don't dispatch orphaned touch events (#4341)
## Description Follow-up to #4316, fixing the root cause of the malformed touch events on the native side. `GestureHandler` on Android keeps two parallel pointer structures with different lifecycles: - `trackedPointerIDs` — which pointers belong to the gesture. Always maintained (`startTrackingPointer` runs unconditionally) and consulted by `wantsEvent`. - `trackedPointers` — per-pointer position data used to build touch events. Only maintained while `needsPointerData` is true, i.e. while touch callbacks are attached. When touch callbacks are attached **mid-gesture** — e.g. a callback attached conditionally, where the attaching re-render is triggered from `onBegin` — `needsPointerData` flips from `false` to `true` while a pointer is already down. That pointer's DOWN was never recorded into `trackedPointers`, but the UP still passes `wantsEvent`, so `dispatchTouchUpEvent()`: 1. built `allTouches` from an empty map — the `null` payload that used to reach JS without the `allTouches` key and crash the v3 update pipeline (fixed defensively in #4316), 2. reported a pointer in `changedTouches` that the JS side has never seen go down, 3. decremented `trackedPointersCount` that was never incremented for that pointer, serializing `numberOfTouches: -1` and corrupting the counter that `moveToState` uses to decide whether to dispatch `TOUCH_CANCEL` (with multi-touch this can suppress a legitimate cancel event). iOS had the same asymmetry with a different failure mode: `RNGestureHandlerPointerTracker` guards the counter (`unregisterTouch` returns `-1`, no underflow), but still dispatched the orphaned event with `id: -1` in `changedTouches`. Same for moves of an unregistered touch in `touchesMoved`. Web is not affected: its `PointerTracker` is populated unconditionally and only *sending* is gated on `needsPointerData`, so mid-gesture flips always produce well-formed events there. ## Test plan <details> <summary>Tested on the following code:</summary> ```tsx import React, { useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { GestureDetector, usePanGesture } from 'react-native-gesture-handler'; // Repro / verification screen for the orphaned-touch-event fix (follow-up to // #4316). // // Purple box ("flip"): touch callbacks are attached conditionally, so // `needsPointerData` flips false -> true mid-gesture (the onBegin re-render // attaches onTouchesUp). Press, hold ~300ms, release: // - without the fix: an orphaned onTouchesUp fires for a pointer that never // reported a down (Android: numberOfTouches -1, iOS: changed touch id -1; // on pre-#4316 builds this crashed the pan change calculator instead) // - with the fix: the orphaned up event is skipped - counter stays 0 // // Green box ("static"): touch callbacks attached from the start - the normal // path. Each tap must count down+up (counter += 2) with and without the fix. export default function OrphanedTouchRepro() { const [flipTouches, setFlipTouches] = useState(0); const [staticTouches, setStaticTouches] = useState(0); const [lastOrphan, setLastOrphan] = useState('none'); const [pressed, setPressed] = useState(false); const flipPan = usePanGesture({ onBegin: () => setPressed(true), onFinalize: () => setPressed(false), onUpdate: () => {}, runOnJS: true, onTouchesUp: pressed ? (e) => { setFlipTouches((c) => c + 1); setLastOrphan( `numberOfTouches: ${e.numberOfTouches}, ` + `changed id: ${e.changedTouches[0]?.id ?? 'none'}` ); } : undefined, }); const staticPan = usePanGesture({ onTouchesDown: () => setStaticTouches((c) => c + 1), onTouchesUp: () => setStaticTouches((c) => c + 1), onUpdate: () => {}, runOnJS: true, }); return ( <View style={styles.container}> <Text style={styles.counter}> flip: {flipTouches} static: {staticTouches} </Text> <Text style={styles.orphanInfo}>last orphaned up: {lastOrphan}</Text> <GestureDetector gesture={flipPan}> <View style={[styles.box, styles.flipBox]} testID="flipBox"> <Text style={styles.boxLabel}>flip: hold ~300ms (expect 0)</Text> </View> </GestureDetector> <GestureDetector gesture={staticPan}> <View style={[styles.box, styles.staticBox]} testID="staticBox"> <Text style={styles.boxLabel}>static (expect +2 per tap)</Text> </View> </GestureDetector> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', }, counter: { fontSize: 16, marginBottom: 8, }, orphanInfo: { fontSize: 13, opacity: 0.6, marginBottom: 24, }, box: { width: 260, height: 150, borderRadius: 16, justifyContent: 'center', alignItems: 'center', marginVertical: 12, }, flipBox: { backgroundColor: 'mediumpurple', }, staticBox: { backgroundColor: 'mediumseagreen', }, boxLabel: { color: 'white', fontSize: 16, }, }); ``` </details>
1 parent 13d0070 commit 6b0ca7b

2 files changed

Lines changed: 40 additions & 11 deletions

File tree

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,10 +477,15 @@ open class GestureHandler {
477477
}
478478

479479
private fun dispatchTouchUpEvent(event: MotionEvent, sourceEvent: MotionEvent) {
480+
val pointerId = event.getPointerId(event.actionIndex)
481+
482+
if (trackedPointers[pointerId] == null) {
483+
return
484+
}
485+
480486
extractAllPointersData()
481487
changedTouchesPayload = null
482488
touchEventType = RNGestureHandlerTouchEvent.EVENT_TOUCH_UP
483-
val pointerId = event.getPointerId(event.actionIndex)
484489
val offsetX = sourceEvent.rawX - sourceEvent.x
485490
val offsetY = sourceEvent.rawY - sourceEvent.y
486491

packages/react-native-gesture-handler/apple/RNGestureHandlerPointerTracker.mm

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -116,18 +116,25 @@ - (void)touchesBegan:(NSSet<RNGHUITouch *> *)touches withEvent:(UIEvent *)event
116116
_eventType = RNGHTouchEventTypePointerDown;
117117

118118
NSDictionary *data[touches.count];
119+
int changedCount = 0;
119120

120121
for (int i = 0; i < [touches count]; i++) {
121122
RNGHUITouch *touch = [[touches allObjects] objectAtIndex:i];
122123
int index = [self registerTouch:touch];
123-
if (index >= 0) {
124-
_trackedPointersCount++;
124+
125+
if (index < 0) {
126+
continue;
125127
}
126128

127-
data[i] = [self extractPointerData:index forTouch:touch];
129+
_trackedPointersCount++;
130+
data[changedCount++] = [self extractPointerData:index forTouch:touch];
131+
}
132+
133+
if (changedCount == 0) {
134+
return;
128135
}
129136

130-
_changedPointersData = [[NSArray alloc] initWithObjects:data count:[touches count]];
137+
_changedPointersData = [[NSArray alloc] initWithObjects:data count:changedCount];
131138
// extract all touches last to include the ones that were just added
132139
[self extractAllTouches];
133140
[self sendEvent];
@@ -142,14 +149,24 @@ - (void)touchesMoved:(NSSet<RNGHUITouch *> *)touches withEvent:(UIEvent *)event
142149
_eventType = RNGHTouchEventTypePointerMove;
143150

144151
NSDictionary *data[touches.count];
152+
int changedCount = 0;
145153

146154
for (int i = 0; i < [touches count]; i++) {
147155
RNGHUITouch *touch = [[touches allObjects] objectAtIndex:i];
148156
int index = [self findTouchIndex:touch];
149-
data[i] = [self extractPointerData:index forTouch:touch];
157+
158+
if (index < 0) {
159+
continue;
160+
}
161+
162+
data[changedCount++] = [self extractPointerData:index forTouch:touch];
150163
}
151164

152-
_changedPointersData = [[NSArray alloc] initWithObjects:data count:[touches count]];
165+
if (changedCount == 0) {
166+
return;
167+
}
168+
169+
_changedPointersData = [[NSArray alloc] initWithObjects:data count:changedCount];
153170
[self extractAllTouches];
154171
[self sendEvent];
155172
}
@@ -166,18 +183,25 @@ - (void)touchesEnded:(NSSet<RNGHUITouch *> *)touches withEvent:(UIEvent *)event
166183
_eventType = RNGHTouchEventTypePointerUp;
167184

168185
NSDictionary *data[touches.count];
186+
int changedCount = 0;
169187

170188
for (int i = 0; i < [touches count]; i++) {
171189
RNGHUITouch *touch = [[touches allObjects] objectAtIndex:i];
172190
int index = [self unregisterTouch:touch];
173-
if (index >= 0) {
174-
_trackedPointersCount--;
191+
192+
if (index < 0) {
193+
continue;
175194
}
176195

177-
data[i] = [self extractPointerData:index forTouch:touch];
196+
_trackedPointersCount--;
197+
data[changedCount++] = [self extractPointerData:index forTouch:touch];
198+
}
199+
200+
if (changedCount == 0) {
201+
return;
178202
}
179203

180-
_changedPointersData = [[NSArray alloc] initWithObjects:data count:[touches count]];
204+
_changedPointersData = [[NSArray alloc] initWithObjects:data count:changedCount];
181205
[self sendEvent];
182206
}
183207

0 commit comments

Comments
 (0)