React Native

4.2 Touch Events and Gestures

Module Still Under Development

# 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;
1
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
1

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
1

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);
1
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",
  },
});
1
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",
  },
});
1
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
}} >
1
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) }],
}));
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>
);
1
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.
1
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>
  );
}
1
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>
  );
}
1
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>
);
1
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>
);
1
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,
  },
});
1
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.
}
1
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:

  1. 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.
};
1
2
3
4
5
6
7
8
9
10
  1. 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
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  1. Inside your Component return value we need to have an Animated.View component with a style property. There are Animated versions of View, Text, Image, ScrollView, FlatList, and SectionList. Inside the style property there needs to be some property that has its value as myAnim.
<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>
1
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.

React Native Style Cheatsheet

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]
      }
1
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]
      }),
    }],
  }}
1
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>)
}
1
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
Last Updated: 9/30/2024, 10:07:21 AM