Implementing Gestures and Touch Handling in React Native
Touch interactions and gestures are fundamental to mobile app user experiences. React Native provides several ways to handle touches and implement complex gestures.
Before we dive into the implementation details, let's understand what gestures and touch events are.
Gestures refer to the actions performed by a user on a touchscreen device, such as tapping, swiping, pinching, or rotating. These gestures are used to interact with the app, navigate through screens, and perform various actions.
Touch events, on the other hand, are the raw input data generated by the device's touchscreen. These events include touch starts, moves, and ends, which are used to detect gestures.
Basic Touch Handling
TouchableOpacity
TouchableOpacity is a wrapper that allows views to respond properly to touches. On press down, the opacity of the wrapped view is decreased, dimming it.
Let’s see how it can be used in our code:
import { Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import React from 'react'
const touchable = () => {
Alert.alert('Touchable opacity pressed')
}
const gestureandtouchhandling = () => {
return (
<View style={styles.container}>
<TouchableOpacity style={styles.button} onPress={touchable}>
<View>
<Text style={styles.buttonText}>Touchable opacity</Text>
</View>
</TouchableOpacity>
</View>
)
}
In our code, we have been able to introduce a function that displays an alert when TocuhableOpacity has been pressed. You can find more properties related to TouchableOpacity here https://reactnative.dev/docs/touchableopacity
Try Kodaschool for free
Click below to sign up and get access to free web, android and iOs challenges.
TouchableHighlight
TouchableHIghlight is a wrapper that makes views respond properly to touches. On press down, the opacity of the wrapped view is decreased, which allows the underlay color to show through, darkening or tinting the view.
TouchableHilight is similar to TouchableOpacity which we previously looked at. The difference is that TouchableHighlight has a darker highlight compared to TouchableOpacity
TouchableHighlight must have one child (not zero or more than one). If you wish to have several child components, wrap them in a View.
<TouchableHighlight style={styles.button}
onPress={() => console.log("Pressed TOuchable Highlight")}
underlayColor="#fff">
<View>
<Text style={styles.buttonText}>Touchable Highlight</Text>
</View>
</TouchableHighlight>
In the code above, we added the TouchableHighlight, which displays text in the console that it has been pressed. Unlike the TouchableOpacity, it requires certain props in order to work. We have added one which is the underlayColor. The color of the underlay will show through when the touch is active.
Advanced Gesture Handling with PanResponder
PanResponder reconciles several touches into a single gesture. It makes single-touch gestures resilient to extra touches and can be used to recognize basic multi-touch gestures.
By default, PanResponder holds an InteractionManager handle to block long-running JS events from interrupting active gestures.
https://reactnative.dev/docs/panresponder
Let's look at how PanResponder can be implemented into our existing code.
const pan = useRef(new Animated.ValueXY()).current;
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event([null, { dx: pan.x, dy: pan.y }]),
onPanResponderRelease: () => {
pan.extractOffset();
},
}),
).current;
In our code, we have added PanResponder.create which accepts different propr like the onMoveShouldSetPanResponder which allows the component to move in all directions and onPanResponderMove updates the values as the user drags the box around
After adding the PanResponder, we use the Animate API to animate our box
<Animated.View
style={{
transform: [{ translateX: pan.x }, { translateY: pan.y }],
}}
{...panResponder.panHandlers}>
<View style={styles.box} />
</Animated.View>
Below is the Full code for what we have learnt so far
import { Alert, Animated, PanResponder, StyleSheet, Text, TouchableHighlight, TouchableOpacity, View } from 'react-native'
import React, { useRef } from 'react'
const touchable = () => {
Alert.alert('Touchable opacity pressed')
}
const gestureandtouchhandling = () => {
const pan = useRef(new Animated.ValueXY()).current;
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event([null, { dx: pan.x, dy: pan.y }]),
onPanResponderRelease: () => {
pan.extractOffset();
},
}),
).current;
return (
<View style={styles.container}>
<TouchableOpacity style={styles.button} onPress={touchable}>
<View>
<Text style={styles.buttonText}>Touchable opacity</Text>
</View>
</TouchableOpacity>
<TouchableHighlight style={styles.button}
onPress={() => console.log("Pressed TOuchable Highlight")}
underlayColor="#fff">
<View>
<Text style={styles.buttonText}>Touchable Highlight</Text>
</View>
</TouchableHighlight>
<Animated.View
style={{
transform: [{ translateX: pan.x }, { translateY: pan.y }],
}}
{...panResponder.panHandlers}>
<View style={styles.box} />
</Animated.View>
</View>
)
}
export default gestureandtouchhandling
const styles = StyleSheet.create({
container: {
paddingTop: 60,
alignItems: 'center',
},
button: {
marginBottom: 30,
width: 260,
alignItems: 'center',
backgroundColor: '#2196F3',
},
buttonText: {
textAlign: 'center',
padding: 20,
color: 'white',
},
box: {
height: 150,
width: 150,
backgroundColor: 'blue',
borderRadius: 5,
},
});
Implementing Pinch-to-Zoom
In our pinch-to-zoom feature, we will still use the PanResponder. We added a scale and a state to check the current zoom level and keep track of the zoom level with the state
const scale = useRef(new Animated.Value(1)).current;
const [lastScale, setLastScale] = useState(1);
We also added a function to calculate the two points when pressed
const distance = Math.sqrt(
Math.pow(touch2.pageX - touch1.pageX, 2) +
Math.pow(touch2.pageY - touch1.pageY, 2)
);
Here is the full code for the pinch to Zoom
import {
Alert,
Animated,
PanResponder,
PanResponderGestureState,
StyleSheet,
Text,
TouchableHighlight,
TouchableOpacity,
View
} from 'react-native';
import React, { useCallback, useRef, useState } from 'react';
const touchable = () => {
Alert.alert('Touchable opacity pressed');
};
const GestureAndTouchHandling: React.FC = () => {
const pan = useRef(new Animated.ValueXY()).current;
const scale = useRef(new Animated.Value(1)).current;
const [lastScale, setLastScale] = useState(1);
const handlePanResponderMove = useCallback((event: any, gestureState: PanResponderGestureState) => {
const touches = event.nativeEvent.touches;
if (touches.length === 1) {
// Handle dragging
Animated.event([null, { dx: pan.x, dy: pan.y }], { useNativeDriver: false })(event, gestureState);
} else if (touches.length === 2) {
// Handle pinch-to-zoom
const touch1 = touches[0];
const touch2 = touches[1];
const distance = Math.sqrt(
Math.pow(touch2.pageX - touch1.pageX, 2) +
Math.pow(touch2.pageY - touch1.pageY, 2)
);
const newScale = distance / 150;
scale.setValue(lastScale * newScale);
}
}, [pan, scale, lastScale]);
const handlePanResponderRelease = useCallback(() => {
pan.extractOffset();
scale.addListener((value) => {
setLastScale(value.value);
scale.removeAllListeners();
});
}, [pan, scale]);
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderMove: handlePanResponderMove,
onPanResponderRelease: handlePanResponderRelease,
})
).current;
return (
<View style={styles.container}>
<TouchableOpacity style={styles.button} onPress={touchable}>
<View>
<Text style={styles.buttonText}>Touchable opacity</Text>
</View>
</TouchableOpacity>
<TouchableHighlight
style={styles.button}
onPress={() => console.log("Pressed Touchable Highlight")}
underlayColor="#fff"
>
<View>
<Text style={styles.buttonText}>Touchable Highlight</Text>
</View>
</TouchableHighlight>
<Animated.View
style={{
transform: [
{ translateX: pan.x },
{ translateY: pan.y },
{ scale: scale }
],
}}
{...panResponder.panHandlers}
>
<View style={styles.box} />
</Animated.View>
</View>
);
};
export default GestureAndTouchHandling;
const styles = StyleSheet.create({
container: {
paddingTop: 60,
alignItems: 'center',
},
button: {
marginBottom: 30,
width: 260,
alignItems: 'center',
backgroundColor: '#2196F3',
},
buttonText: {
textAlign: 'center',
padding: 20,
color: 'white',
},
box: {
height: 150,
width: 150,
backgroundColor: 'blue',
borderRadius: 5,
},
});
There are many more gesture fomats you can use with React Native, there is also animation which is done using Reanimate.