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.

July 19, 20243 min read

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.

Sign Up

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.

Image
Cliff Gor

About Cliff Gor

As a Fullstack Software Engineer, I design and develop user-centric and robust systems and applications using HTML, CSS, Bootstrap, React Js, React Native, and Kotlin(Android).