
Introduction
Handling User input and gestureare fundamental aspects of creating engaging mobile applications. React Native provides several ways to handle user interactions, from simple touch events to complex multi-touch gestures like pinch-to-zoom and pan-and-drag operations.
This chapter will cover:
- Understanding React Native’s gesture responder system
- Implementing basic and advanced gestures
- Using the React Native Gesture Handler library
- Optimizing gesture performance
- Handling gesture conflicts
Basic Touch Events
React Native provides several built-in touch event handlers that you can use with most components.
onPress and onPressIn/onPressOut
import React, { useState } from 'react';
import { View, Text, TouchableOpacity, Alert, StyleSheet } from 'react-native';
const BasicTouchExample = () => {
const [pressCount, setPressCount] = useState(0);
const handlePress = () => {
setPressCount(prevCount => prevCount + 1);
Alert.alert('Button Pressed', `Press count: ${pressCount + 1}`);
};
const handlePressIn = () => {
console.log('Press started');
};
const handlePressOut = () => {
console.log('Press ended');
};
return (
<View style={styles.container}>
<TouchableOpacity
style={styles.button}
onPress={handlePress}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
>
<Text style={styles.buttonText}>
Press Me ({pressCount})
</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
button: {
backgroundColor: '#007AFF',
padding: 15,
borderRadius: 8,
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
});
export default BasicTouchExample;
Different Touchable Components
import React from 'react';
import {
View,
Text,
TouchableOpacity,
TouchableHighlight,
TouchableWithoutFeedback,
Pressable,
StyleSheet
} from 'react-native';
const TouchableComponentsExample = () => {
const handlePress = (type) => {
console.log(`${type} pressed`);
};
return (
<View style={styles.container}>
<TouchableOpacity
style={styles.touchable}
onPress={() => handlePress('TouchableOpacity')}
>
<Text style={styles.text}>TouchableOpacity</Text>
</TouchableOpacity>
<TouchableHighlight
style={styles.touchable}
underlayColor="#DDDDDD"
onPress={() => handlePress('TouchableHighlight')}
>
<Text style={styles.text}>TouchableHighlight</Text>
</TouchableHighlight>
<TouchableWithoutFeedback
onPress={() => handlePress('TouchableWithoutFeedback')}
>
<View style={styles.touchable}>
<Text style={styles.text}>TouchableWithoutFeedback</Text>
</View>
</TouchableWithoutFeedback>
<Pressable
style={({ pressed }) => [
styles.touchable,
{ backgroundColor: pressed ? '#DDDDDD' : '#F0F0F0' }
]}
onPress={() => handlePress('Pressable')}
>
<Text style={styles.text}>Pressable (Recommended)</Text>
</Pressable>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
touchable: {
backgroundColor: '#F0F0F0',
padding: 15,
margin: 10,
borderRadius: 8,
width: 200,
alignItems: 'center',
},
text: {
fontSize: 16,
},
});
export default TouchableComponentsExample;
Gesture Responder System
React Native’s gesture responder system determines which component should handle touch events when multiple components are involved.
Basic Gesture Responder
import React, { useRef } from 'react';
import { View, Text, PanResponder, Animated, StyleSheet } from 'react-native';
const GestureResponderExample = () => {
const pan = useRef(new Animated.ValueXY()).current;
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
pan.setOffset({
x: pan.x._value,
y: pan.y._value,
});
},
onPanResponderMove: Animated.event(
[null, { dx: pan.x, dy: pan.y }],
{ useNativeDriver: false }
),
onPanResponderRelease: () => {
pan.flattenOffset();
},
})
).current;
return (
<View style={styles.container}>
<Text style={styles.title}>Drag the blue square!</Text>
<Animated.View
style={[
styles.box,
{
transform: [{ translateX: pan.x }, { translateY: pan.y }],
},
]}
{...panResponder.panHandlers}
>
<Text style={styles.boxText}>Drag me!</Text>
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 18,
marginBottom: 20,
},
box: {
height: 100,
width: 100,
backgroundColor: 'blue',
borderRadius: 5,
justifyContent: 'center',
alignItems: 'center',
},
boxText: {
color: 'white',
fontWeight: 'bold',
},
});
export default GestureResponderExample;
Advanced Gesture Responder with Boundaries
import React, { useRef, useState } from 'react';
import {
View,
Text,
PanResponder,
Animated,
Dimensions,
StyleSheet
} from 'react-native';
const { width, height } = Dimensions.get('window');
const BoundedDragExample = () => {
const pan = useRef(new Animated.ValueXY()).current;
const [isDragging, setIsDragging] = useState(false);
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
setIsDragging(true);
pan.setOffset({
x: pan.x._value,
y: pan.y._value,
});
},
onPanResponderMove: (event, gestureState) => {
const { dx, dy } = gestureState;
const maxX = width - 100; // Box width
const maxY = height - 100; // Box height
const newX = Math.max(0, Math.min(maxX, dx));
const newY = Math.max(0, Math.min(maxY, dy));
pan.setValue({ x: newX, y: newY });
},
onPanResponderRelease: () => {
setIsDragging(false);
pan.flattenOffset();
},
})
).current;
return (
<View style={styles.container}>
<Text style={styles.title}>
Drag within boundaries (Status: {isDragging ? 'Dragging' : 'Idle'})
</Text>
<Animated.View
style={[
styles.box,
{
transform: [{ translateX: pan.x }, { translateY: pan.y }],
backgroundColor: isDragging ? 'red' : 'blue',
},
]}
{...panResponder.panHandlers}
>
<Text style={styles.boxText}>Bounded Drag</Text>
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 50,
},
title: {
fontSize: 16,
textAlign: 'center',
marginBottom: 20,
paddingHorizontal: 20,
},
box: {
height: 100,
width: 100,
backgroundColor: 'blue',
borderRadius: 5,
justifyContent: 'center',
alignItems: 'center',
position: 'absolute',
},
boxText: {
color: 'white',
fontWeight: 'bold',
textAlign: 'center',
},
});
export default BoundedDragExample;
PanGestureHandler
For more complex gesture handling, especially when dealing with multiple gestures, React Native Gesture Handler provides better performance and more features.
Installation
npm install react-native-gesture-handler
# or
yarn add react-native-gesture-handler
Basic Pan Gesture
import React, { useRef } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
runOnJS,
} from 'react-native-reanimated';
const PanGestureExample = () => {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const onGestureEvent = useAnimatedGestureHandler({
onStart: (_, context) => {
context.startX = translateX.value;
context.startY = translateY.value;
},
onActive: (event, context) => {
translateX.value = context.startX + event.translationX;
translateY.value = context.startY + event.translationY;
},
onEnd: () => {
// Optional: Add snap-back animation
// translateX.value = withSpring(0);
// translateY.value = withSpring(0);
},
});
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
],
};
});
return (
<View style={styles.container}>
<Text style={styles.title}>Pan Gesture with Gesture Handler</Text>
<PanGestureHandler onGestureEvent={onGestureEvent}>
<Animated.View style={[styles.box, animatedStyle]}>
<Text style={styles.boxText}>Pan Me!</Text>
</Animated.View>
</PanGestureHandler>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 18,
marginBottom: 50,
},
box: {
width: 100,
height: 100,
backgroundColor: 'purple',
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',
},
boxText: {
color: 'white',
fontWeight: 'bold',
},
});
export default PanGestureExample;
Pinch and Zoom Gestures
Implementing pinch-to-zoom functionality is common in image viewers and maps.
import React, { useRef } from 'react';
import { View, Text, Image, StyleSheet } from 'react-native';
import { PinchGestureHandler, State } from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withSpring,
} from 'react-native-reanimated';
const PinchZoomExample = () => {
const scale = useSharedValue(1);
const focalX = useSharedValue(0);
const focalY = useSharedValue(0);
const pinchGestureHandler = useAnimatedGestureHandler({
onStart: (event, context) => {
context.startScale = scale.value;
},
onActive: (event, context) => {
scale.value = context.startScale * event.scale;
focalX.value = event.focalX;
focalY.value = event.focalY;
},
onEnd: () => {
if (scale.value < 1) {
scale.value = withSpring(1);
} else if (scale.value > 3) {
scale.value = withSpring(3);
}
},
});
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{ scale: scale.value },
],
};
});
return (
<View style={styles.container}>
<Text style={styles.title}>Pinch to Zoom</Text>
<PinchGestureHandler onGestureEvent={pinchGestureHandler}>
<Animated.View style={[styles.imageContainer, animatedStyle]}>
<View style={styles.imagePlaceholder}>
<Text style={styles.placeholderText}>
Pinch to zoom this content
</Text>
</View>
</Animated.View>
</PinchGestureHandler>
<Text style={styles.instructions}>
Use two fingers to pinch and zoom
</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
title: {
fontSize: 18,
marginBottom: 20,
},
imageContainer: {
width: 200,
height: 200,
marginBottom: 20,
},
imagePlaceholder: {
flex: 1,
backgroundColor: '#E0E0E0',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 10,
},
placeholderText: {
textAlign: 'center',
color: '#666',
fontSize: 16,
},
instructions: {
fontSize: 14,
color: '#666',
textAlign: 'center',
},
});
export default PinchZoomExample;
Swipe Gestures
Swipe gestures are useful for navigation, dismissing content, and triggering actions.
import React, { useState } from 'react';
import { View, Text, StyleSheet, Alert } from 'react-native';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withSpring,
runOnJS,
} from 'react-native-reanimated';
const SwipeGestureExample = () => {
const [swipeDirection, setSwipeDirection] = useState('');
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const detectSwipe = (translationX, translationY, velocityX, velocityY) => {
const minSwipeDistance = 50;
const minSwipeVelocity = 500;
if (Math.abs(translationX) > minSwipeDistance || Math.abs(velocityX) > minSwipeVelocity) {
if (translationX > 0) {
setSwipeDirection('Right');
} else {
setSwipeDirection('Left');
}
} else if (Math.abs(translationY) > minSwipeDistance || Math.abs(velocityY) > minSwipeVelocity) {
if (translationY > 0) {
setSwipeDirection('Down');
} else {
setSwipeDirection('Up');
}
}
};
const gestureHandler = useAnimatedGestureHandler({
onStart: () => {
runOnJS(setSwipeDirection)('');
},
onActive: (event) => {
translateX.value = event.translationX;
translateY.value = event.translationY;
},
onEnd: (event) => {
runOnJS(detectSwipe)(
event.translationX,
event.translationY,
event.velocityX,
event.velocityY
);
translateX.value = withSpring(0);
translateY.value = withSpring(0);
},
});
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
],
};
});
return (
<View style={styles.container}>
<Text style={styles.title}>Swipe Gesture Detection</Text>
<Text style={styles.direction}>
Last swipe: {swipeDirection || 'None'}
</Text>
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View style={[styles.swipeArea, animatedStyle]}>
<Text style={styles.swipeText}>Swipe me in any direction!</Text>
</Animated.View>
</PanGestureHandler>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
title: {
fontSize: 18,
marginBottom: 10,
},
direction: {
fontSize: 16,
marginBottom: 30,
color: '#666',
},
swipeArea: {
width: 200,
height: 200,
backgroundColor: '#4CAF50',
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',
},
swipeText: {
color: 'white',
fontSize: 16,
textAlign: 'center',
fontWeight: 'bold',
},
});
export default SwipeGestureExample;
Long Press Gestures
Long press gestures are useful for context menus and secondary actions.
import React, { useState } from 'react';
import { View, Text, StyleSheet, Alert } from 'react-native';
import { LongPressGestureHandler, State } from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withSpring,
runOnJS,
} from 'react-native-reanimated';
const LongPressExample = () => {
const [isPressed, setIsPressed] = useState(false);
const scale = useSharedValue(1);
const showContextMenu = () => {
Alert.alert(
'Context Menu',
'Long press detected!',
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Delete', style: 'destructive' },
{ text: 'Edit', style: 'default' },
]
);
};
const longPressHandler = useAnimatedGestureHandler({
onStart: () => {
runOnJS(setIsPressed)(true);
scale.value = withSpring(0.9);
},
onActive: () => {
// Long press is active
},
onEnd: () => {
runOnJS(setIsPressed)(false);
scale.value = withSpring(1);
},
onFinish: (event) => {
if (event.state === State.ACTIVE) {
runOnJS(showContextMenu)();
}
},
});
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [{ scale: scale.value }],
};
});
return (
<View style={styles.container}>
<Text style={styles.title}>Long Press Gesture</Text>
<Text style={styles.subtitle}>
Press and hold the button below
</Text>
<LongPressGestureHandler
onGestureEvent={longPressHandler}
minDurationMs={500}
>
<Animated.View style={[styles.button, animatedStyle]}>
<Text style={styles.buttonText}>
{isPressed ? 'Hold...' : 'Long Press Me'}
</Text>
</Animated.View>
</LongPressGestureHandler>
<Text style={styles.instructions}>
Hold for 500ms to trigger context menu
</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
title: {
fontSize: 18,
marginBottom: 10,
},
subtitle: {
fontSize: 14,
color: '#666',
marginBottom: 30,
},
button: {
backgroundColor: '#FF6B6B',
paddingHorizontal: 30,
paddingVertical: 15,
borderRadius: 25,
marginBottom: 20,
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
instructions: {
fontSize: 12,
color: '#666',
textAlign: 'center',
},
});
export default LongPressExample;
React Native Gesture Handler
React Native Gesture Handler provides better performance and more gesture types compared to the built-in PanResponder.
Simultaneous Gestures
import React, { useRef } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import {
PanGestureHandler,
PinchGestureHandler,
simultaneousHandlers,
} from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated';
const SimultaneousGesturesExample = () => {
const panRef = useRef();
const pinchRef = useRef();
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const scale = useSharedValue(1);
const panGestureHandler = useAnimatedGestureHandler({
onStart: (_, context) => {
context.startX = translateX.value;
context.startY = translateY.value;
},
onActive: (event, context) => {
translateX.value = context.startX + event.translationX;
translateY.value = context.startY + event.translationY;
},
});
const pinchGestureHandler = useAnimatedGestureHandler({
onStart: (_, context) => {
context.startScale = scale.value;
},
onActive: (event, context) => {
scale.value = context.startScale * event.scale;
},
});
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
{ scale: scale.value },
],
};
});
return (
<View style={styles.container}>
<Text style={styles.title}>Simultaneous Pan & Pinch</Text>
<PanGestureHandler
ref={panRef}
onGestureEvent={panGestureHandler}
simultaneousHandlers={pinchRef}
>
<Animated.View>
<PinchGestureHandler
ref={pinchRef}
onGestureEvent={pinchGestureHandler}
simultaneousHandlers={panRef}
>
<Animated.View style={[styles.box, animatedStyle]}>
<Text style={styles.boxText}>Pan & Pinch Me!</Text>
</Animated.View>
</PinchGestureHandler>
</Animated.View>
</PanGestureHandler>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 18,
marginBottom: 50,
},
box: {
width: 150,
height: 150,
backgroundColor: '#FFA726',
borderRadius: 15,
justifyContent: 'center',
alignItems: 'center',
},
boxText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
textAlign: 'center',
},
});
export default SimultaneousGesturesExample;
Custom Gesture Handling
Creating custom gesture handlers for specific use cases.
import React, { useState } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withSpring,
runOnJS,
} from 'react-native-reanimated';
const CustomDrawerGesture = () => {
const [isOpen, setIsOpen] = useState(false);
const translateX = useSharedValue(0);
const drawerWidth = 250;
const toggleDrawer = () => {
setIsOpen(!isOpen);
};
const gestureHandler = useAnimatedGestureHandler({
onStart: (_, context) => {
context.startX = translateX.value;
},
onActive: (event, context) => {
const newTranslateX = context.startX + event.translationX;
// Constrain the drawer movement
if (newTranslateX >= 0 && newTranslateX <= drawerWidth) {
translateX.value = newTranslateX;
}
},
onEnd: (event) => {
const { translationX, velocityX } = event;
const shouldOpen = translationX > drawerWidth / 2 || velocityX > 500;
if (shouldOpen) {
translateX.value = withSpring(drawerWidth);
runOnJS(setIsOpen)(true);
} else {
translateX.value = withSpring(0);
runOnJS(setIsOpen)(false);
}
},
});
const drawerStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: translateX.value - drawerWidth }],
};
});
const overlayStyle = useAnimatedStyle(() => {
return {
opacity: translateX.value / drawerWidth,
};
});
return (
<View style={styles.container}>
<Text style={styles.title}>Custom Drawer Gesture</Text>
<Text style={styles.subtitle}>
Swipe from left edge to open drawer
</Text>
{/* Overlay */}
<Animated.View style={[styles.overlay, overlayStyle]} />
{/* Drawer */}
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View style={[styles.drawer, drawerStyle]}>
<Text style={styles.drawerTitle}>Menu</Text>
<View style={styles.menuItem}>
<Text style={styles.menuText}>Home</Text>
</View>
<View style={styles.menuItem}>
<Text style={styles.menuText}>Profile</Text>
</View>
<View style={styles.menuItem}>
<Text style={styles.menuText}>Settings</Text>
</View>
</Animated.View>
</PanGestureHandler>
{/* Main content */}
<View style={styles.content}>
<Text style={styles.contentText}>
Main Content Area
</Text>
<Text style={styles.status}>
Drawer is {isOpen ? 'Open' : 'Closed'}
</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F5F5',
},
title: {
fontSize: 18,
textAlign: 'center',
marginTop: 50,
marginBottom: 10,
},
subtitle: {
fontSize: 14,
textAlign: 'center',
color: '#666',
marginBottom: 20,
},
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'black',
zIndex: 1,
},
drawer: {
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
width: 250,
backgroundColor: 'white',
zIndex: 2,
paddingTop: 100,
paddingHorizontal: 20,
shadowColor: '#000',
shadowOffset: { width: 2, height: 0 },
shadowOpacity: 0.3,
shadowRadius: 5,
elevation: 5,
},
drawerTitle: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 30,
color: '#333',
},
menuItem: {
paddingVertical: 15,
borderBottomWidth: 1,
borderBottomColor: '#E0E0E0',
},
menuText: {
fontSize: 18,
color: '#333',
},
content: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
contentText: {
fontSize: 20,
marginBottom: 20,
},
status: {
fontSize: 16,
color: '#666',
},
});
export default CustomDrawerGesture;
Gesture Conflicts and Priority
Managing gesture conflicts when multiple gesture handlers are present.
import React, { useRef } from 'react';
import { View, Text, StyleSheet, ScrollView } from 'react-native';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withSpring,
} from 'react-native-reanimated';
const GestureConflictExample = () => {
const parentPanRef = useRef();
const childPanRef = useRef();
const parentTranslateX = useSharedValue(0);
const childTranslateX = useSharedValue(0);
const parentGestureHandler = useAnimatedGestureHandler({
onStart: (_, context) => {
context.startX = parentTranslateX.value;
},
onActive: (event, context) => {
parentTranslateX.value = context.startX + event.translationX;
},
onEnd: () => {
parentTranslateX.value = withSpring(0);
},
});
const childGestureHandler = useAnimatedGestureHandler({
onStart: (_, context) => {
context.startX = childTranslateX.value;
},
onActive: (event, context) => {
childTranslateX.value = context.startX + event.translationX;
},
onEnd: () => {
childTranslateX.value = withSpring(0);
},
});
const parentStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: parentTranslateX.value }],
};
});
const childStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: childTranslateX.value }],
};
});
return (
<ScrollView style={styles.container}>
<Text style={styles.title}>Gesture Conflicts & Priority</Text>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Parent-Child Gesture Conflict</Text>
<PanGestureHandler
ref={parentPanRef}
onGestureEvent={parentGestureHandler}
>
<Animated.View style={[styles.parentBox, parentStyle]}>
<Text style={styles.parentText}>Parent (Drag me)</Text>
<PanGestureHandler
ref={childPanRef}
onGestureEvent={childGestureHandler}
shouldCancelWhenOutside
>
<Animated.View style={[styles.childBox, childStyle]}>
<Text style={styles.childText}>Child (Drag me too)</Text>
</Animated.View>
</PanGestureHandler>
</Animated.View>
</PanGestureHandler>
</View>
<View style={styles.explanation}>
<Text style={styles.explanationTitle}>Explanation:</Text>
<Text style={styles.explanationText}>
• Child gesture handler has priority over parent
</Text>
<Text style={styles.explanationText}>
• shouldCancelWhenOutside prevents parent activation
</Text>
<Text style={styles.explanationText}>
• Try dragging both the parent and child areas
</Text>
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F5F5',
padding: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 30,
marginTop: 40,
},
section: {
marginBottom: 30,
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 15,
},
parentBox: {
width: 300,
height: 200,
backgroundColor: '#4CAF50',
borderRadius: 10,
padding: 20,
alignSelf: 'center',
},
parentText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
marginBottom: 10,
},
childBox: {
width: 150,
height: 80,
backgroundColor: '#2196F3',
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
marginTop: 10,
},
childText: {
color: 'white',
fontSize: 14,
fontWeight: 'bold',
textAlign: 'center',
},
explanation: {
backgroundColor: 'white',
padding: 20,
borderRadius: 10,
marginTop: 20,
},
explanationTitle: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 10,
},
explanationText: {
fontSize: 14,
marginBottom: 5,
color: '#666',
},
});
export default GestureConflictExample;
Performance Considerations
Optimizing gesture performance for smooth user interactions.
import React, { useRef, useCallback } from 'react';
import { View, Text, StyleSheet, FlatList } from 'react-native';
import { PanGestureHandler } from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withSpring,
runOnJS,
} from 'react-native-reanimated';
const PerformanceOptimizedGestures = () => {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const scale = useSharedValue(1);
// Use useCallback to prevent unnecessary re-renders
const onGestureUpdate = useCallback((x, y) => {
console.log(`Position: ${x.toFixed(2)}, ${y.toFixed(2)}`);
}, []);
// Optimized gesture handler with native driver
const gestureHandler = useAnimatedGestureHandler({
onStart: (_, context) => {
context.startX = translateX.value;
context.startY = translateY.value;
scale.value = withSpring(1.1); // Immediate feedback
},
onActive: (event, context) => {
translateX.value = context.startX + event.translationX;
translateY.value = context.startY + event.translationY;
// Throttle JS calls for performance
if (Math.abs(event.translationX) % 10 < 1) {
runOnJS(onGestureUpdate)(translateX.value, translateY.value);
}
},
onEnd: () => {
scale.value = withSpring(1);
// Optional: Add boundary constraints
// translateX.value = withSpring(0);
// translateY.value = withSpring(0);
},
});
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
{ scale: scale.value },
],
};
}, []);
return (
<View style={styles.container}>
<Text style={styles.title}>Performance Optimized Gestures</Text>
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View style={[styles.draggableBox, animatedStyle]}>
<Text style={styles.boxText}>Optimized Drag</Text>
</Animated.View>
</PanGestureHandler>
<View style={styles.tips}>
<Text style={styles.tipsTitle}>Performance Tips:</Text>
<Text style={styles.tip}>• Use useNativeDriver for transforms</Text>
<Text style={styles.tip}>• Throttle runOnJS calls</Text>
<Text style={styles.tip}>• Use useCallback for event handlers</Text>
<Text style={styles.tip}>• Minimize state updates during gestures</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: '#F5F5F5',
},
title: {
fontSize: 20,
fontWeight: 'bold',
textAlign: 'center',
marginTop: 50,
marginBottom: 50,
},
draggableBox: {
width: 120,
height: 120,
backgroundColor: '#FF5722',
borderRadius: 15,
justifyContent: 'center',
alignItems: 'center',
alignSelf: 'center',
marginBottom: 50,
},
boxText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
tips: {
backgroundColor: 'white',
padding: 20,
borderRadius: 10,
},
tipsTitle: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 15,
},
tip: {
fontSize: 14,
marginBottom: 8,
color: '#666',
},
});
export default PerformanceOptimizedGestures;
Best Practices
Guidelines for implementing gestures effectively in React Native applications.
1. Gesture Feedback and Visual Cues
import React, { useState } from 'react';
import { View, Text, StyleSheet, Haptics } from 'react-native';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withSpring,
withTiming,
runOnJS,
} from 'react-native-reanimated';
const GestureFeedbackExample = () => {
const [gestureState, setGestureState] = useState('idle');
const translateX = useSharedValue(0);
const backgroundColor = useSharedValue(0);
const borderRadius = useSharedValue(10);
const triggerHaptic = () => {
// Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
};
const gestureHandler = useAnimatedGestureHandler({
onStart: () => {
runOnJS(setGestureState)('active');
runOnJS(triggerHaptic)();
backgroundColor.value = withTiming(1);
borderRadius.value = withSpring(20);
},
onActive: (event) => {
translateX.value = event.translationX;
},
onEnd: () => {
runOnJS(setGestureState)('idle');
translateX.value = withSpring(0);
backgroundColor.value = withTiming(0);
borderRadius.value = withSpring(10);
},
});
const animatedStyle = useAnimatedStyle(() => {
const bgColor = backgroundColor.value === 1 ? '#4CAF50' : '#2196F3';
return {
transform: [{ translateX: translateX.value }],
backgroundColor: bgColor,
borderRadius: borderRadius.value,
};
});
return (
<View style={styles.container}>
<Text style={styles.title}>Gesture Feedback Best Practices</Text>
<Text style={styles.status}>
Status: {gestureState}
</Text>
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View style={[styles.feedbackBox, animatedStyle]}>
<Text style={styles.boxText}>Drag for Feedback</Text>
</Animated.View>
</PanGestureHandler>
<View style={styles.practicesList}>
<Text style={styles.practicesTitle}>Best Practices:</Text>
<Text style={styles.practice}>✓ Provide immediate visual feedback</Text>
<Text style={styles.practice}>✓ Use haptic feedback for touch events</Text>
<Text style={styles.practice}>✓ Animate state changes smoothly</Text>
<Text style={styles.practice}>✓ Show gesture boundaries clearly</Text>
<Text style={styles.practice}>✓ Maintain 60fps during gestures</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: '#F5F5F5',
},
title: {
fontSize: 20,
fontWeight: 'bold',
textAlign: 'center',
marginTop: 50,
marginBottom: 20,
},
status: {
fontSize: 16,
textAlign: 'center',
marginBottom: 30,
color: '#666',
},
feedbackBox: {
width: 150,
height: 150,
backgroundColor: '#2196F3',
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',
alignSelf: 'center',
marginBottom: 40,
},
boxText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
textAlign: 'center',
},
practicesList: {
backgroundColor: 'white',
padding: 20,
borderRadius: 10,
},
practicesTitle: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 15,
},
practice: {
fontSize: 14,
marginBottom: 8,
color: '#333',
},
});
export default GestureFeedbackExample;
Common Patterns
Implementing common gesture patterns used in mobile applications.
Pull-to-Refresh Pattern
import React, { useState, useRef } from 'react';
import { View, Text, StyleSheet, ScrollView } from 'react-native';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withSpring,
runOnJS,
} from 'react-native-reanimated';
const PullToRefreshExample = () => {
const [isRefreshing, setIsRefreshing] = useState(false);
const [data, setData] = useState(['Item 1', 'Item 2', 'Item 3']);
const translateY = useSharedValue(0);
const refreshThreshold = 100;
const performRefresh = async () => {
setIsRefreshing(true);
// Simulate API call
setTimeout(() => {
setData(prev => [`Item ${prev.length + 1}`, ...prev]);
setIsRefreshing(false);
translateY.value = withSpring(0);
}, 2000);
};
const gestureHandler = useAnimatedGestureHandler({
onActive: (event) => {
if (event.translationY > 0) {
translateY.value = event.translationY;
}
},
onEnd: (event) => {
if (event.translationY > refreshThreshold) {
runOnJS(performRefresh)();
} else {
translateY.value = withSpring(0);
}
},
});
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [{ translateY: translateY.value }],
};
});
const refreshIndicatorStyle = useAnimatedStyle(() => {
const opacity = Math.min(translateY.value / refreshThreshold, 1);
const rotation = `${(translateY.value / refreshThreshold) * 360}deg`;
return {
opacity,
transform: [{ rotate: rotation }],
};
});
return (
<View style={styles.container}>
<Text style={styles.title}>Pull to Refresh</Text>
<View style={styles.refreshContainer}>
<Animated.View style={[styles.refreshIndicator, refreshIndicatorStyle]}>
<Text style={styles.refreshText}>
{isRefreshing ? '🔄 Refreshing...' : '⬇️ Pull to refresh'}
</Text>
</Animated.View>
</View>
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.ScrollView
style={[styles.scrollView, animatedStyle]}
scrollEnabled={translateY.value === 0}
>
{data.map((item, index) => (
<View key={index} style={styles.item}>
<Text style={styles.itemText}>{item}</Text>
</View>
))}
</Animated.ScrollView>
</PanGestureHandler>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F5F5',
},
title: {
fontSize: 20,
fontWeight: 'bold',
textAlign: 'center',
marginTop: 50,
marginBottom: 20,
},
refreshContainer: {
height: 50,
justifyContent: 'center',
alignItems: 'center',
},
refreshIndicator: {
padding: 10,
},
refreshText: {
fontSize: 16,
color: '#666',
},
scrollView: {
flex: 1,
backgroundColor: 'white',
},
item: {
padding: 20,
borderBottomWidth: 1,
borderBottomColor: '#E0E0E0',
},
itemText: {
fontSize: 16,
},
});
export default PullToRefreshExample;
Troubleshooting
Common issues and solutions when implementing gestures in React Native.
Debugging Gesture Issues
import React, { useState } from 'react';
import { View, Text, StyleSheet, Switch } from 'react-native';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
runOnJS,
} from 'react-native-reanimated';
const GestureDebuggingExample = () => {
const [debugMode, setDebugMode] = useState(false);
const [gestureData, setGestureData] = useState({});
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const updateGestureData = (data) => {
if (debugMode) {
setGestureData(data);
}
};
const gestureHandler = useAnimatedGestureHandler({
onStart: (event) => {
runOnJS(updateGestureData)({
state: 'START',
x: event.x,
y: event.y,
timestamp: Date.now(),
});
},
onActive: (event) => {
translateX.value = event.translationX;
translateY.value = event.translationY;
runOnJS(updateGestureData)({
state: 'ACTIVE',
translationX: event.translationX,
translationY: event.translationY,
velocityX: event.velocityX,
velocityY: event.velocityY,
});
},
onEnd: (event) => {
runOnJS(updateGestureData)({
state: 'END',
finalX: event.translationX,
finalY: event.translationY,
duration: Date.now() - (gestureData.timestamp || 0),
});
},
onFail: () => {
runOnJS(updateGestureData)({
state: 'FAILED',
reason: 'Gesture recognition failed',
});
},
});
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
],
};
});
return (
<View style={styles.container}>
<Text style={styles.title}>Gesture Debugging</Text>
<View style={styles.debugToggle}>
<Text style={styles.debugLabel}>Debug Mode:</Text>
<Switch
value={debugMode}
onValueChange={setDebugMode}
/>
</View>
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View style={[styles.debugBox, animatedStyle]}>
<Text style={styles.boxText}>Debug Gesture</Text>
</Animated.View>
</PanGestureHandler>
{debugMode && (
<View style={styles.debugInfo}>
<Text style={styles.debugTitle}>Gesture Data:</Text>
<Text style={styles.debugText}>
{JSON.stringify(gestureData, null, 2)}
</Text>
</View>
)}
<View style={styles.troubleshootingTips}>
<Text style={styles.tipsTitle}>Common Issues & Solutions:</Text>
<Text style={styles.tip}>
• Gesture not working: Check gesture handler setup
</Text>
<Text style={styles.tip}>
• Poor performance: Use native driver
</Text>
<Text style={styles.tip}>
• Conflicts: Manage gesture priority
</Text>
<Text style={styles.tip}>
• Unexpected behavior: Enable debug mode
</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: '#F5F5F5',
},
title: {
fontSize: 20,
fontWeight: 'bold',
textAlign: 'center',
marginTop: 50,
marginBottom: 20,
},
debugToggle: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 30,
},
debugLabel: {
fontSize: 16,
marginRight: 10,
},
debugBox: {
width: 120,
height: 120,
backgroundColor: '#9C27B0',
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',
alignSelf: 'center',
marginBottom: 20,
},
boxText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
debugInfo: {
backgroundColor: 'white',
padding: 15,
borderRadius: 8,
marginBottom: 20,
},
debugTitle: {
fontSize: 14,
fontWeight: 'bold',
marginBottom: 10,
},
debugText: {
fontSize: 12,
fontFamily: 'monospace',
color: '#666',
},
troubleshootingTips: {
backgroundColor: 'white',
padding: 15,
borderRadius: 8,
},
tipsTitle: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 10,
},
tip: {
fontSize: 14,
marginBottom: 5,
color: '#666',
},
});
export default GestureDebuggingExample;
Summary
In this chapter, we covered comprehensive gesture handling in React Native:
- Basic Touch Events: Understanding onPress, onPressIn, onPressOut, and different touchable components
- Gesture Responder System: Using PanResponder for basic gesture recognition
- React Native Gesture Handler: Leveraging the more powerful gesture handling library
- Common Gestures: Implementing pan, pinch, swipe, and long press gestures
- Advanced Patterns: Creating custom gestures like drawer navigation and pull-to-refresh
- Performance Optimization: Best practices for smooth gesture interactions
- Conflict Resolution: Managing multiple gesture handlers
- Debugging: Tools and techniques for troubleshooting gesture issues
Key Takeaways
- Use React Native Gesture Handler for complex gestures and better performance
- Always provide visual feedback for user interactions
- Consider gesture conflicts when implementing multiple handlers
- Optimize performance by using the native driver and minimizing JS thread usage
- Test gestures thoroughly on different devices and screen sizes
- Implement proper accessibility support for gesture-based interactions
Next Steps
In the next chapter, we’ll explore React Native Hooks, which provide a powerful way to manage state and side effects in functional components, including gesture-related state management.
Related Articles:
- React Native vs Flutter in 2024: A Detailed Comparison
- Top 10 React Native Libraries You Must Know in 2024
- React vs React Native: A Deep Dive into Key Differences
- How to Set Up Navigation in React Native : Step-by-Step Guide
- What is Axios? Fetch vs Axios: What’s the Difference?
- React Native Environment Setup: A Beginner’s Step-by-Step Guide
- React Native Web: A Step-by-Step Guide to Cross-Platform Development