PanResponder is React Native 36,878 's built-in, JavaScript-side gesture API. It sits on the gesture responder system: on each touch, React Native asks the views under the finger, innermost first, whether they want to become the responder (onStartShouldSetPanResponder, or onMoveShouldSetPanResponder once the finger moves); the winner receives onPanResponderGrant, Move and Release, and can lose the touch to another view through onPanResponderTerminate. Every callback gets the native event and a gestureState with x0/y0 (where the grant happened), dx/dy (the distance since), vx/vy (velocity), moveX/moveY and numberActiveTouches. The classic use is dragging with an Animated.ValueXY:
const pan = useRef(new Animated.ValueXY()).current;
const responder = useRef(PanResponder.create({
onStartShouldSetPanResponder: () => true, // claim the touch on press
onPanResponderGrant: (_, g) => console.log('grant at', r(g.x0), r(g.y0)),
onPanResponderMove: Animated.event([null, { dx: pan.x, dy: pan.y }],
{ useNativeDriver: false }), // gestureState.dx/dy -> pan.x/y
onPanResponderRelease: (_, g) => {
console.log(`release dx=${r(g.dx)} dy=${r(g.dy)} vx=${g.vx.toFixed(2)}`,
`moveX=${r(g.moveX)} touches=${g.numberActiveTouches}`);
Animated.spring(pan, { toValue: { x: 0, y: 0 }, useNativeDriver: false }).start();
},
onPanResponderTerminate: () => console.log('another responder took over'),
})).current;
// ...spread responder.panHandlers on an Animated.View moved by pan.getTranslateTransform()One adb shell input swipe, logged through adb logcat -s ReactNativeJS:
ReactNativeJS: 'grant at', 206, 152 ReactNativeJS: 'release dx=97 dy=112 vx=0.20', 'moveX=303 touches=0'
Coordinates are in dp, and velocities in dp per millisecond (0.20 here, not 200). Create the responder once, with useRef() or useMemo(); created on every render, it loses track of a gesture in progress. useNativeDriver must be false, because Animated.event() is fed here by a JavaScript callback, not by a native event.