Installation
Install React Native Gesture Handler:npm install react-native-gesture-handler
Basic Gesture Example
import { Canvas, Circle } from "@shopify/react-native-skia";
import { GestureDetector, Gesture } from "react-native-gesture-handler";
import { useSharedValue } from "react-native-reanimated";
const DraggableCircle = () => {
const translateX = useSharedValue(128);
const translateY = useSharedValue(128);
const gesture = Gesture.Pan()
.onChange((e) => {
translateX.value += e.changeX;
translateY.value += e.changeY;
});
return (
<GestureDetector gesture={gesture}>
<Canvas style={{ width: 256, height: 256 }}>
<Circle cx={translateX} cy={translateY} r={40} color="blue" />
</Canvas>
</GestureDetector>
);
};
Pan Gesture
Drag and move elements:import { GestureDetector, Gesture } from "react-native-gesture-handler";
import { useSharedValue, withDecay } from "react-native-reanimated";
const PanDemo = () => {
const x = useSharedValue(100);
const y = useSharedValue(100);
const pan = Gesture.Pan()
.onChange((e) => {
x.value += e.changeX;
y.value += e.changeY;
})
.onEnd((e) => {
// Add momentum with decay
x.value = withDecay({
velocity: e.velocityX,
clamp: [40, 216],
});
y.value = withDecay({
velocity: e.velocityY,
clamp: [40, 216],
});
});
return (
<GestureDetector gesture={pan}>
<Canvas style={{ width: 256, height: 256 }}>
<Circle cx={x} cy={y} r={40} color="purple" />
</Canvas>
</GestureDetector>
);
};
Tap Gesture
Handle taps and touches:import { Gesture } from "react-native-gesture-handler";
import { withSpring } from "react-native-reanimated";
const TapDemo = () => {
const scale = useSharedValue(1);
const tap = Gesture.Tap()
.onStart(() => {
scale.value = withSpring(1.3);
})
.onEnd(() => {
scale.value = withSpring(1);
});
return (
<GestureDetector gesture={tap}>
<Canvas style={{ width: 256, height: 256 }}>
<Group transform={[{ scale }]}>
<Circle cx={128} cy={128} r={40} color="red" />
</Group>
</Canvas>
</GestureDetector>
);
};
Pinch Gesture
Scale elements with two fingers:import { Gesture } from "react-native-gesture-handler";
import { useSharedValue } from "react-native-reanimated";
const PinchDemo = () => {
const scale = useSharedValue(1);
const savedScale = useSharedValue(1);
const pinch = Gesture.Pinch()
.onUpdate((e) => {
scale.value = savedScale.value * e.scale;
})
.onEnd(() => {
savedScale.value = scale.value;
});
return (
<GestureDetector gesture={pinch}>
<Canvas style={{ width: 256, height: 256 }}>
<Group transform={[{ scale }]}>
<Image image={myImage} x={0} y={0} width={256} height={256} />
</Group>
</Canvas>
</GestureDetector>
);
};
Rotation Gesture
Rotate elements with two fingers:import { Gesture } from "react-native-gesture-handler";
const RotationDemo = () => {
const rotation = useSharedValue(0);
const savedRotation = useSharedValue(0);
const rotate = Gesture.Rotation()
.onUpdate((e) => {
rotation.value = savedRotation.value + e.rotation;
})
.onEnd(() => {
savedRotation.value = rotation.value;
});
return (
<GestureDetector gesture={rotate}>
<Canvas style={{ width: 256, height: 256 }}>
<Group transform={[{ rotate: rotation }]}>
<RoundedRect
x={64}
y={64}
width={128}
height={128}
r={20}
color="green"
/>
</Group>
</Canvas>
</GestureDetector>
);
};
Combining Gestures
UseGesture.Simultaneous to enable multiple gestures at once:
import { Gesture } from "react-native-gesture-handler";
const CombinedGesturesDemo = () => {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const scale = useSharedValue(1);
const rotation = useSharedValue(0);
const savedScale = useSharedValue(1);
const savedRotation = useSharedValue(0);
const pan = Gesture.Pan()
.onChange((e) => {
translateX.value += e.changeX;
translateY.value += e.changeY;
});
const pinch = Gesture.Pinch()
.onUpdate((e) => {
scale.value = savedScale.value * e.scale;
})
.onEnd(() => {
savedScale.value = scale.value;
});
const rotate = Gesture.Rotation()
.onUpdate((e) => {
rotation.value = savedRotation.value + e.rotation;
})
.onEnd(() => {
savedRotation.value = rotation.value;
});
const composed = Gesture.Simultaneous(pan, pinch, rotate);
return (
<GestureDetector gesture={composed}>
<Canvas style={{ width: 256, height: 256 }}>
<Group
transform={[
{ translateX },
{ translateY },
{ scale },
{ rotate: rotation },
]}
>
<Image image={myImage} x={0} y={0} width={256} height={256} />
</Group>
</Canvas>
</GestureDetector>
);
};
Interactive Drawing
Create a drawing app:import { Canvas, Path, Skia } from "@shopify/react-native-skia";
import { Gesture } from "react-native-gesture-handler";
import { useState } from "react";
const DrawingDemo = () => {
const [paths, setPaths] = useState<SkPath[]>([]);
const [currentPath, setCurrentPath] = useState<SkPath | null>(null);
const pan = Gesture.Pan()
.onStart((e) => {
const path = Skia.Path.Make();
path.moveTo(e.x, e.y);
setCurrentPath(path);
})
.onChange((e) => {
if (currentPath) {
currentPath.lineTo(e.x, e.y);
setCurrentPath(currentPath.copy());
}
})
.onEnd(() => {
if (currentPath) {
setPaths([...paths, currentPath]);
setCurrentPath(null);
}
});
return (
<GestureDetector gesture={pan}>
<Canvas style={{ flex: 1, backgroundColor: "white" }}>
{paths.map((path, index) => (
<Path
key={index}
path={path}
color="black"
style="stroke"
strokeWidth={3}
strokeCap="round"
strokeJoin="round"
/>
))}
{currentPath && (
<Path
path={currentPath}
color="black"
style="stroke"
strokeWidth={3}
strokeCap="round"
strokeJoin="round"
/>
)}
</Canvas>
</GestureDetector>
);
};
Velocity-Based Animations
Use gesture velocity for natural animations:import { Gesture } from "react-native-gesture-handler";
import { withDecay } from "react-native-reanimated";
const VelocityDemo = () => {
const translateX = useSharedValue(0);
const pan = Gesture.Pan()
.onChange((e) => {
translateX.value += e.changeX;
})
.onEnd((e) => {
translateX.value = withDecay({
velocity: e.velocityX,
clamp: [0, 200],
});
});
return (
<GestureDetector gesture={pan}>
<Canvas style={{ width: 256, height: 256 }}>
<Circle cx={translateX} cy={128} r={30} color="orange" />
</Canvas>
</GestureDetector>
);
};
Gesture State
React to gesture state changes:import { Gesture, State } from "react-native-gesture-handler";
import { withTiming } from "react-native-reanimated";
const GestureStateDemo = () => {
const opacity = useSharedValue(1);
const scale = useSharedValue(1);
const pan = Gesture.Pan()
.onBegin(() => {
opacity.value = withTiming(0.6);
scale.value = withTiming(1.1);
})
.onChange((e) => {
// Handle pan...
})
.onFinalize(() => {
opacity.value = withTiming(1);
scale.value = withTiming(1);
});
return (
<GestureDetector gesture={pan}>
<Canvas style={{ width: 256, height: 256 }}>
<Group opacity={opacity} transform={[{ scale }]}>
<Circle cx={128} cy={128} r={50} color="cyan" />
</Group>
</Canvas>
</GestureDetector>
);
};
Snapping
Snap to specific positions:import { Gesture } from "react-native-gesture-handler";
import { withTiming } from "react-native-reanimated";
const SnapDemo = () => {
const translateX = useSharedValue(64);
const snapPoints = [64, 128, 192];
const findNearestSnap = (value: number) => {
'worklet';
return snapPoints.reduce((prev, curr) =>
Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev
);
};
const pan = Gesture.Pan()
.onChange((e) => {
translateX.value += e.changeX;
})
.onEnd(() => {
const nearest = findNearestSnap(translateX.value);
translateX.value = withTiming(nearest, { duration: 200 });
});
return (
<GestureDetector gesture={pan}>
<Canvas style={{ width: 256, height: 256 }}>
{/* Snap guides */}
{snapPoints.map((x, i) => (
<Circle key={i} cx={x} cy={128} r={4} color="lightgray" />
))}
{/* Draggable element */}
<Circle cx={translateX} cy={128} r={20} color="blue" />
</Canvas>
</GestureDetector>
);
};
Hit Testing
Detect if a touch is within a specific shape:const isPointInCircle = (
px: number,
py: number,
cx: number,
cy: number,
r: number
) => {
'worklet';
const dx = px - cx;
const dy = py - cy;
return dx * dx + dy * dy <= r * r;
};
const tap = Gesture.Tap()
.onStart((e) => {
if (isPointInCircle(e.x, e.y, 128, 128, 50)) {
// Handle tap on circle
scale.value = withSpring(1.2);
}
});
Long Press
Handle long press gestures:import { Gesture } from "react-native-gesture-handler";
import { withSequence, withTiming } from "react-native-reanimated";
const LongPressDemo = () => {
const scale = useSharedValue(1);
const longPress = Gesture.LongPress()
.minDuration(500)
.onStart(() => {
scale.value = withSequence(
withTiming(1.3, { duration: 100 }),
withTiming(1, { duration: 100 })
);
});
return (
<GestureDetector gesture={longPress}>
<Canvas style={{ width: 256, height: 256 }}>
<Group transform={[{ scale }]}>
<Circle cx={128} cy={128} r={50} color="indigo" />
</Group>
</Canvas>
</GestureDetector>
);
};
Gesture Boundaries
Constrain gestures to specific bounds:const clamp = (value: number, min: number, max: number) => {
'worklet';
return Math.min(Math.max(value, min), max);
};
const pan = Gesture.Pan()
.onChange((e) => {
translateX.value = clamp(translateX.value + e.changeX, 0, 200);
translateY.value = clamp(translateY.value + e.changeY, 0, 200);
});
Performance Tips
- Use
onChangeinstead ofonUpdatefor better performance - Minimize state updates - use shared values instead
- Use
runOnJSsparingly to call JS functions from gestures - Batch related value updates together
- Use
withDecayfor natural momentum scrolling
Related
- Animations Overview - Animation fundamentals
- Reanimated Integration - Shared values and worklets
- Hooks - Animation utilities
- Gesture Handler Docs - Official documentation