Repository navigation
Commit bc605eb
authored
[macOS] Fix touch events never being delivered (#4390)
## Description
`onTouchesMove` and `onTouchesUp` callbacks never fired on macOS — a
gesture produced a single `onTouchesDown` and then a cancel sweep when
the recognizer reset, never a move or up event. Found while testing the
`manualActivation` fix (#4389), which this bug masks entirely in its
real-world form (activating from `onTouchesMove`).
`RNGestureHandlerPointerTracker` tracks pointers by storing the touch
object at registration and matching subsequent events by object identity
(`_trackedPointers[index] == touch`). That's correct on iOS, where a
`UITouch` is one stable object for the whole lifetime of a touch. On
macOS the recognizers forward `NSEvent`s, and every event in a mouse
sequence is a fresh instance — so `findTouchIndex:` / `unregisterTouch:`
never matched, move/up events were dropped with `changedCount == 0`, and
the registered pointer leaked until the tracker's `reset` swept it out
via `cancelPointers` (which is why JS saw down → cancel instead of down
→ moves → up).
Since macOS has exactly one mouse pointer, the tracker now matches the
tracked slot itself on macOS instead of comparing object identity, and
`touchesMoved` replaces the stored event with the latest one so
`extractAllTouches` reports the pointer's current position rather than
where the sequence started.
## Test plan
<details>
<summary>Tested on the following code:</summary>
```tsx
import React, { useEffect } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import {
GestureDetector,
GestureStateManager,
usePanGesture,
} from 'react-native-gesture-handler';
import { useSharedValue } from 'react-native-reanimated';
export default function EmptyExample() {
// Box A: manualActivation, JS never calls activate().
// Expected: down -> move stream -> up, onFinalize canceled=true, never onActivate.
const neverActivatePan = usePanGesture({
manualActivation: true,
onBegin: () => console.log('[never-activate] onBegin'),
onActivate: () => console.log('[never-activate] onActivate (SHOULD NOT HAPPEN)'),
onTouchesDown: () => console.log('[never-activate] onTouchesDown'),
onTouchesMove: () => console.log('[never-activate] onTouchesMove'),
onTouchesUp: () => console.log('[never-activate] onTouchesUp'),
onDeactivate: () => console.log('[never-activate] onDeactivate'),
onFinalize: (e) => console.log(`[never-activate] onFinalize canceled=${e.canceled}`),
});
// Box B: manualActivation, activates from onTouchesMove — the real-world pattern.
// Expected: onActivate on first movement (tx ~0), onUpdate while dragging,
// onTouchesUp + onDeactivate + onFinalize canceled=false on release.
const selfTag = useSharedValue(-1);
const selfActivatePan = usePanGesture({
manualActivation: true,
onTouchesDown: () => console.log(`[self-activate] onTouchesDown tag=${selfTag.value}`),
onTouchesMove: () => {
console.log(`[self-activate] onTouchesMove tag=${selfTag.value}`);
if (selfTag.value !== -1) {
GestureStateManager.activate(selfTag.value);
}
},
onTouchesUp: () => console.log('[self-activate] onTouchesUp'),
onBegin: () => console.log('[self-activate] onBegin'),
onActivate: (e) =>
console.log(`[self-activate] onActivate tx=${e.translationX.toFixed(1)} ty=${e.translationY.toFixed(1)}`),
onUpdate: (e) =>
console.log(`[self-activate] onUpdate tx=${e.translationX.toFixed(1)} ty=${e.translationY.toFixed(1)}`),
onDeactivate: () => console.log('[self-activate] onDeactivate'),
onFinalize: (e) => console.log(`[self-activate] onFinalize canceled=${e.canceled}`),
});
useEffect(() => {
selfTag.value = selfActivatePan.handlerTag;
}, [selfActivatePan.handlerTag, selfTag]);
return (
<View style={styles.container}>
<Text style={styles.label}>A: manualActivation, never activated — click & drag, release</Text>
<GestureDetector gesture={neverActivatePan}>
<View style={[styles.box, { backgroundColor: 'darkorange' }]} />
</GestureDetector>
<Text style={styles.label}>B: manualActivation, self-activates on touch move — drag</Text>
<GestureDetector gesture={selfActivatePan}>
<View style={[styles.box, { backgroundColor: 'mediumpurple' }]} />
</GestureDetector>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 8 },
label: { marginTop: 16, fontSize: 15, opacity: 0.6 },
box: { width: 150, height: 150, borderRadius: 12 },
});
```
</details>1 parent 787f165 commit bc605eb
1 file changed
Lines changed: 30 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
42 | 48 | | |
| 49 | + | |
43 | 50 | | |
44 | 51 | | |
45 | 52 | | |
| |||
51 | 58 | | |
52 | 59 | | |
53 | 60 | | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
54 | 65 | | |
| 66 | + | |
55 | 67 | | |
56 | 68 | | |
57 | 69 | | |
| |||
159 | 171 | | |
160 | 172 | | |
161 | 173 | | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
162 | 180 | | |
163 | 181 | | |
164 | 182 | | |
| |||
177 | 195 | | |
178 | 196 | | |
179 | 197 | | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
180 | 210 | | |
181 | 211 | | |
182 | 212 | | |
| |||
0 commit comments