DailyDevDiet

logo - dailydevdiet

Learn. Build. Innovate. Elevate your coding skills with dailydevdiet!

Chapter 10: Handling User Input and Gestures

Handling User Input

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:

  1. Basic Touch Events: Understanding onPress, onPressIn, onPressOut, and different touchable components
  2. Gesture Responder System: Using PanResponder for basic gesture recognition
  3. React Native Gesture Handler: Leveraging the more powerful gesture handling library
  4. Common Gestures: Implementing pan, pinch, swipe, and long press gestures
  5. Advanced Patterns: Creating custom gestures like drawer navigation and pull-to-refresh
  6. Performance Optimization: Best practices for smooth gesture interactions
  7. Conflict Resolution: Managing multiple gesture handlers
  8. 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:

Scroll to Top