4.2 Touch Events and Gestures
# Touching and Tapping Components
Another difference between React and React Native is how interaction with the user is handled. In the browser, developers are used to using the mouse events, like click
, mouseover
, and mouseout
.
On a mobile device the user is using their fingers for navigation and interaction. JavaScript does also have the TouchEvents
and the PointerEvents
but these will all fallback to the Mouse events
like click when running in a mobile browser.
When it comes to native mobile apps, and native components we do not have click
as an event. Instead we have the onPress
, onPressIn
, onPressOut
, and onLongPress
events.
By default, certain native components, like buttons, support listeners for those Press events. However, this is not the case for all elements. <Text>
and <View>
, your two most common components do
not support the PressEvents
.
# TouchableOpacity, TouchableWithoutFeedback, TouchableHighlight
To add support for the PressEvents, we need to wrap other components inside something that can be pressed.
Old Version
Please note that the TouchableOpacity
components are the old version of the code. You will still see them in samples and in the documentation. However, Pressable
is the new version.
Until recently, we could make things touchable or pressable by wrapping our components inside of one of three components.
All three support the PressEvents
. The difference is just in the visual feedback given to the user when the touch the element.
# Pressable
In more recent versions of React Native there is a new component called Pressable
which is aimed at replacing the other three - TouchableOpacity, TouchableHighlight, and TouchableWithoutFeedback.
The <Pressable>
component wraps your components to add the support for the PressEvent
listeners. To change the visual feedback to the user, we use the style prop inside it. We give it a function
that will pass in a Boolean that indicates whether or not the component is currently being pressed.
Here is an example from the reference:
import React, { useState } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
const App = () => {
const [timesPressed, setTimesPressed] = useState(0);
let textLog = "";
if (timesPressed > 1) {
textLog = timesPressed + "x onPress";
} else if (timesPressed > 0) {
textLog = "onPress";
}
return (
<View style={styles.container}>
<Pressable
onPress={() => {
setTimesPressed((current) => current + 1);
}}
onLongPress={() => {
//if the user presses and holds on the component
setTimesPressed((current) => current + 10);
}}
style={({ pressed }) => [
{
backgroundColor: pressed ? "rgb(210, 230, 255)" : "white",
},
styles.wrapperCustom,
]}
>
{({ pressed }) => (
<Text style={styles.text}>{pressed ? "Pressed!" : "Press Me"}</Text>
)}
</Pressable>
<View style={styles.logBox}>
<Text testID="pressable_press_console">{textLog}</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
},
text: {
fontSize: 16,
},
wrapperCustom: {
borderRadius: 8,
padding: 6,
},
logBox: {
padding: 20,
margin: 10,
borderWidth: StyleSheet.hairlineWidth,
borderColor: "#f0f0f0",
backgroundColor: "#f9f9f9",
},
});
export default App;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# GestureHandler and Reanimated
The GestureHandler API is useful when you want to do things other than just Press
events. Start with the install...
npx expo install react-native-gesture-handler
Full documentation for the Gesture Handler can be found here.
The Reanimated animation library is a module that we add to work with GestureHandler.
npx expo install react-native-reanimated
To use react-native-animated
we need to edit the /babel.config.js
file. Add plugins: ['react-native-reanimated/plugin'],
inside the returned object, after the presets.
If there were already other plugins then it should be the last one.
React Native Gesture Handler 2 is designed to work with the Reanimated API version 2+. These two libraries were built to work together to allow your React Native components to leverage touch, gesture, and animation features from the native platforms.
There have been 3 major versions of Reanimation and 2 major versions of GestureHandler. They each have different features, different imports and different methods. It can be confusing to know which approaches to use. When you look online for tutorials, you will find many for each and it can be difficult to know which version of each library is being used.
Here we will be discussing Reanimated version 3.x and GestureHandler version 2.x.
Reanimated
also provides the ability to run your animation code in Worklets. Worklets are separate threads to handle heavy processing away from the UI main thread.
See this page for more details about Worklets.
The typical way to use Reanimated
though is with hooks instead of creating your own worklets. useAnimatedStyle
, useAnimatedGestureHandler
, and useSharedValue
are a few of the hooks. These
hooks will take care of creating any needed Worklets.
# Reanimated Basics
When you want to animate parts of your React Native interface, keep in mind that your code is written in JavaScript but it needs to connect with native components and run commands on the native device. This is how Worklets can help. Their code can run in a separate thread so it will not slow down the UI thread.
The first import is the Animated
object. It is needed for any component that you want to animate. It has a few built-in sub-components - Animated.View
, Animated.FlatList
, Animated.Text
,
Animated.Image
and Animated.ScrollView
. If you need another component to be animated then you can import and use the createAnimatedComponent()
method
import Animated from "react-native-reanimated";
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
2
3
Assuming that you just want to animate a View
, we would use the Animated.View
component and give it whatever initial styles we want.
export default function App() {
return (
<Animated.View style={styles.container}>
<Text>Box</Text>
</Animated.View>
);
}
const styles = StyleSheet.create({
container: {
width: 100,
height: 100,
backgroundColor: "pink",
},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
To animated the View we need to be able to pass the values for the properties which will be animated back and forth between our JavaScript and the native code. To do that we will use a
useSharedValue()
hook. Pass it an initial value and it will return a variable that can be shared between your code and the native components.
import Animated, { useSharedValue } from "react-native-reanimated";
export default function App() {
const scaleXY = useSharedValue(1);
return (
<Animated.View
style={[styles.container, { transform: [{ scale: scaleXY }] }]}
>
<Text>Box</Text>
</Animated.View>
);
}
const styles = StyleSheet.create({
container: {
width: 100,
height: 100,
backgroundColor: "pink",
},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
The shared value variable can be used within the style
attribute for the Animated.View
or other Animated
component. It can hold a number or an object with numeric values. Just like in CSS, the
only properties that you can animate are the ones with numeric values. You can't animate between things like flex-start
and center
or block
and none
. Just between 0 and 1 or 250 and 500.
In the same way that a useRef
variable needs to use it's current
property to access the actual value, a useSharedValue
variable needs to use a value
property to access the actual value.
At this point it is just a starting value. So, it is like any other static value. You can add an onPress
handler on your page that will change the value of the property created with
useSharedValue
.
<Pressable onPress={()=>{
scaleXY.value = scaleXY.value * 1.1;
//increase the scale value by 10% with each tap
}} >
2
3
4
Instead of the onPress
function we will use the animation capabilities of the Reanimated
library. The next step is to add the useAnimatedStyle
hook plus the withTiming
function to create a
style that will change over time.
const scaleXY = useSharedValue(1);
const animatedStyles = useAnimatedStyle(() => ({
transform: [{ scale: withTiming(scaleXY.value * 1.1) }],
}));
2
3
4
5
Now we can add the animatedStyles
variable as a style in the Animated.View
just like styles.container
.
return (
<Animated.View style={[styles.container, animatedStyles]}>
<Text>Box</Text>
</Animated.View>
);
2
3
4
5
Now the Animated.View
has some static styles plus the animated ones that can be animated over time. The withTiming
function takes a value as the final value for the property, plus it has a default
duration. If you want the change the duration for the animation between the initial and final value you can add a config option as the second parameter.
const scaleXY = useSharedValue(1);
const animatedStyles = useAnimatedStyle(() => ({
transform: [{ scale: withTiming(2, { duration: 2000 }) }],
}));
//this version animates between the intial value of 1 and final value of 2 and it does
//the animation over a period of 2000 milliseconds.
2
3
4
5
6
7
The last part of this is to create a trigger that changes the value of the shared value. As long as you put the shared value variable inside the withTiming
function, then you can change the value
of that shared value variable inside an onPress
function or inside a useEffect()
function so it happens when the component renders or re-renders.
Without the withTiming
function the useAnimatedStyle
generated style will just jump directly and immediately from the starting value to the ending value.
This is where the GestureHandler comes into play. We can use the gesture to track and generate new values that can be fed directly into the shared value.
# GestureHandler Basics
The GestureHandler
library lets us track different types of gestures, like tap, pan, pinch, zoom, and rotate. From these gestures we can extract information like screen position of components.
We use the useSharedValue
hook to store these values and pass them through to the animated styles that are feed into the Animated
components.
First, in your App.js
file at the root of your app, you need to import and use GestureHandlerRootView
. This is just like the Provider for SafeArea.
//App.js
import { GestureHandlerRootView } from "react-native-gesture-handler";
function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>{/* other components here */}</SafeAreaProvider>
</GestureHandlerRootView>
);
}
2
3
4
5
6
7
8
9
10
Next, on the screen where the gesture will occur we need to import a Gesture
and a GestureHandler
.
import { Gesture, GestureHandler } from "react-native-gesture-handler";
function Screen({ route, navigation }) {
const tapGesture = Gesture.Tap()
.onStart(() => {})
.onEnd(() => {});
return (
<GestureDetector gesture={tapGesture}>
<View></View>
</GestureDetector>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
This will listen for a tap
gesture on the View. The Tap
gesture object created with Gesture.Tap()
has a series of methods that you can chain on to the end of it to set parameters and listener
for various gesture events like onStart()
, onUpdate()
and onEnd()
.
See here for all the events and
See here for the full event state list. The states are UNDETERMINED
, FAILED
, BEGAN
, ACTIVE
,
CANCELLED
AND END
.
There are two ways that you can add the gesture event listeners. With an imperative approach like this:
const tapGesture = Gesture.Tap()
.numberOfTaps(2)
.maxDuration(500)
.maxDelay(500)
.maxDistance(10)
.onStart(() => {
console.log("Tap!");
});
return (
<GestureDetector gesture={tapGesture}>
<View />
</GestureDetector>
);
2
3
4
5
6
7
8
9
10
11
12
13
14
Or with a Declarative approach like this:
return (
<TapGestureHandler
numberOfTaps={2}
maxDurationMs={500}
maxDelayMs={500}
maxDist={10}
onHandlerStateChange={({ nativeEvent }) => {
if (nativeEvent.state === State.ACTIVE) {
console.log("Tap!");
}
}}
>
<Animated.View />
</TapGestureHandler>
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
They both do the same thing.
The available gesture handlers are:
Gesture.Pan()
or<PanGestureHandler>
Gesture.Tap()
or<TapGestureHandler>
Gesture.LongPress()
or<LongPressGestureHandler>
Gesture.Rotation()
or<RotationGestureHandler>
Gesture.Fling()
or<FlingGestureHandler>
Gesture.Pinch()
or<PinchGestureHandler>
Gesture.ForceTouch()
or<ForceTouchGestureHandler>
Reference for the Gesture object's methods.
Each of the methods/components listed above have their own page in the documentation that lists all the posssible properties/attributes/methods to chain. As an example, here is the page for PanGestureHandler.
A useful method shared by these gestures is runOnJS(boolean)
. When passed true
it will run your gesture code in a Worklet on a separate JS thread. When set to false
it will run your script on
the UI thread.
# GestureHandler Components
The React Native Gesture Handler library includes a few components that can be common and useful. The Swipeable component - https://docs.swmansion.com/react-native-gesture-handler/docs/components/swipeable is a good example.
Here is a sample expo snack showing how to add a swipe and delete.
# Combining Reanimated with GestureHandler
If your gestures are going to make changes to the UI then the GestureHandlers should each directly contain a <Animated.View>
(or some other Animated.
component).
The key to combining these two libraries is the useSharedValue
hook. It creates a value that will be shared between your JS and the native code. Your GestureHandler methods like onStart()
and
onUpdate()
and onEnd()
can use the finger/stylus position values from the device and update the shared value's value
property.
The useAnimatedStyle()
hook along with the withTiming()
or withSpring()
methods will update, animate and apply the changes to the shared value
on any component that uses the animated style
inside its style
attribute.
The example below is from an App that has installed react-native-gesture-handler
and react-native-reanimated
. The change was made to the babel.config.js
file to add the plugin.
It builds a FlatList that displays a bunch of cards of data in two columns. Tapping on any card will make it's opacity and scale animate to a new random set of values. It uses the GestureTap
,
useSharedValue
and useAnimatedStyle
combined with withTiming()
.
The <GestureHandlerRootView>
is the provider at the root of App.js.
The <ListItem>
s have a <GestureDetector>
at the root and an <Animated.View>
directly inside it. It is the <Animated.View>
that accepts the animated style object returned from
useAnimatedStyle
.
//App.js
import "react-native-gesture-handler";
import {
Gesture,
GestureDetector,
GestureHandlerRootView,
} from "react-native-gesture-handler";
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
withSpring,
} from "react-native-reanimated";
import { useEffect, useState } from "react";
import { StyleSheet, Text, SafeAreaView, View, FlatList } from "react-native";
import { useWindowDimensions } from "react-native";
import DATA from "./data/beers.json";
//contents of this file generated from
// https://random-data-api.com/api/v2/beers?size=20
export default function App() {
const [beers, setBeers] = useState([]);
const { height, width } = useWindowDimensions(); //not used
useEffect(() => {
setBeers(DATA);
}, []);
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaView style={styles.container}>
<Text style={styles.title}>Multi-Column List with Gestures</Text>
<FlatList
style={styles.list}
data={beers}
renderItem={({ item }) => <ListItem beer={item} />}
numColumns={2}
keyExtractor={(item) => item.uid}
/>
</SafeAreaView>
</GestureHandlerRootView>
);
}
function ListItem({ beer }) {
const { name, style, brand, uid } = beer;
const scale = useSharedValue(1);
const opacity = useSharedValue(1);
const tapGesture = Gesture.Tap()
.numberOfTaps(1)
.maxDuration(500)
.maxDelay(500)
.maxDistance(20)
.onStart(() => {
console.log("starting tap");
})
.onEnd(() => {
scale.value = Math.random() + 0.5;
opacity.value = Math.random();
});
const reStyle = useAnimatedStyle(() => {
return {
opacity: withTiming(opacity.value, { duration: 400 }),
transform: [
{
scale: withTiming(scale.value, {
duration: 600,
}),
},
],
};
});
return (
<GestureDetector gesture={tapGesture}>
<Animated.View style={[styles.item, reStyle]}>
<View>
<Text style={styles.title}>{name}</Text>
<Text style={styles.itemtext}>{brand}</Text>
<Text style={styles.itemtext}>{style}</Text>
</View>
</Animated.View>
</GestureDetector>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "hsl(240, 50%, 50%)",
alignItems: "center",
justifyContent: "flex-start",
paddingTop: 20,
},
list: {
width: "100%",
paddingVertical: 20,
},
title: {
color: "white",
fontSize: 20,
},
itemtext: {
fontSize: 15,
color: "#eee",
},
item: {
flex: 1,
backgroundColor: "cornflowerblue",
padding: 10,
margin: 5,
},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# useWindowDimensions Hook
There will be times when you are animating that you need to calculate a distance as a percentage of the screen width or height. You can see the reference here.
import { useWindowDimensions } from "react-native";
export default function App() {
const { height, width } = useWindowDimensions();
//now you can use the width and height variables as part of your
//animation calculations.
}
2
3
4
5
6
7
# Animated
The Animated Library is a React Native built-in library for simple animations. This is a much simplified API that you can use for basic animations. It is not connected to or meant to be used with the GestureHandler library.
The basic premise is this:
- Create an animation object with the
useRef
hook.
import React, { useRef } from "react";
import { Animated } from "react-native"; //other imports skipped
const App = () => {
const myAnim = useRef(new Animated.Value(0)).current;
//initial value of the Animated object is 0.
//The current property of the hook is what is stored in myAnim
// Animated.Value() is for a single value.
// Animated.ValueXY() will manage two values.
};
2
3
4
5
6
7
8
9
10
- Use the
Animated.timing
method to create each animation between a starting and ending value.
const moveRight = () => {
Animated.timing(myAnim, {
toValue: 200,
duration: 500,
useNativeDriver: true,
}).start();
//when the moveRight function is called Animated will iterate through
//the possible values from 0 to 200 over the course of 500ms
//the start() method begins the iteration through the values.
//at the end, myAnim will have a current value of 200.
};
const moveLeft = () => {
Animated.timing(myAnim, {
toValue: 0,
delay: 500,
duration: 1000,
useNativeDriver: true,
}).start();
//reverse of the moveRight function except it takes a full second, and
//has a half second delay before starting.
//at the end myAnim will have a current value of 0
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- Inside your Component return value we need to have an
Animated.View
component with a style property. There are Animated versions ofView
,Text
,Image
,ScrollView
,FlatList
, andSectionList
. Inside the style property there needs to be some property that has its value asmyAnim
.
<Animated.View
style={{
// Bind paddingLeft to animated value
paddingLeft: myAnim,
}}
>
<Text>Watch me move</Text>
</Animated.View>
<View>
<Button title="click to move the text right" onPress={moveRight} />
<Button title="click to move the text left" onPress={moveLeft} />
</View>
2
3
4
5
6
7
8
9
10
11
12
You can animate any style property that can be set to increments between the starting and ending values. Just like CSS transitions.
There are A LOT of methods that you can use to animate components. See the Animated API reference
for the full list. It includes things like loop
to have an animation continue running, stagger
to have a sequence of animations run with delays between, parallel
to have a series of animations
running at the same time and stop at the same time, and sequence
to run an array of animations.
Animated
also supports interpolation which means you can use one range of values and convert those to output values. Eg: You are getting numbers from 0 to 1 from your script but you want that to be
used as a position value between 50 and 175.
myAnim.interpolate({
inputRange: [0, 1],
outputRange: [50, 175]
}
2
3
4
That bit of code could be used as the output for the value of a transformation.
style={{
opacity: this.state.myAnim, // Binds directly
transform: [{
translateY: myAnim.interpolate({
inputRange: [0, 1],
outputRange: [50, 175]
}),
}],
}}
2
3
4
5
6
7
8
9
To see the demo code for Animated
go to the rn-demoapps repo, navigate to the App.js file and then change the branch to animated.
# AppState API
If you want your app to be able to detect when it is in the foreground or background and notify you then you can use the AppState
API.
You can import AppState
from react-native
.
It has a single property called currentState
which will have a value of active
or background
. On iOS it can also have a temporary value of inactive
as it is switching between the other states.
As the app is first loading the value of currentState
will be null
.
The AppState
object has an addEventListener()
method which can listen for the change
event that will be triggered by every change in the currentState
value.
import {AppState, View} from 'react-native';
import {useState} from 'react';
export default function App(){
const [theAppState, setTheAppState] = useState(null);
useEffect(() => {
const subscription = AppState.addEventListener('change', (newAppState) => {
setTheAppState(newAppState);
switch(newAppState){
case null:
//app loading
break;
case 'active':
//app being used
break;
case 'background':
//app switched to background
break;
case 'inactive':
//ios only
break;
}
});
return (<View>{theAppState}</View>)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27