Lesson 08-React Native Gesture Recognition

Gesture System and Event Interaction

Gesture Recognition Basics

Tap Gesture Implementation

import React from 'react';
import { View, Text, TouchableOpacity, TouchableWithoutFeedback, StyleSheet } from 'react-native';

const TapGestureExample = () => {
  const handleTap = () => {
    console.log('Tap gesture detected!');
  };

  return (
    <View style={styles.container}>
      {/* Method 1: Using TouchableOpacity for simple tap */}
      <TouchableOpacity onPress={handleTap} style={styles.button}>
        <Text>TouchableOpacity Tap</Text>
      </TouchableOpacity>

      {/* Method 2: Using TouchableWithoutFeedback for no-feedback tap */}
      <TouchableWithoutFeedback onPress={handleTap}>
        <View style={styles.box}>
          <Text>TouchableWithoutFeedback Tap</Text>
        </View>
      </TouchableWithoutFeedback>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  button: {
    backgroundColor: '#2196F3',
    padding: 15,
    borderRadius: 5,
    marginBottom: 20,
  },
  box: {
    width: 200,
    height: 100,
    backgroundColor: '#4CAF50',
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default TapGestureExample;

Pan Gesture Implementation

import React, { useRef, useState } from 'react';
import { View, Text, PanResponder, StyleSheet } from 'react-native';

const PanGestureExample = () => {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const panResponder = useRef(
    PanResponder.create({
      // Request to become responder
      onStartShouldSetPanResponder: () => true,
      onMoveShouldSetPanResponder: () => true,

      // Start dragging
      onPanResponderGrant: () => {
        console.log('Drag started');
      },

      // During drag
      onPanResponderMove: (evt, gestureState) => {
        setPosition({
          x: gestureState.dx,
          y: gestureState.dy,
        });
      },

      // End drag
      onPanResponderRelease: () => {
        console.log('Drag ended');
      },
    })
  ).current;

  return (
    <View style={styles.container}>
      <View
        {...panResponder.panHandlers}
        style={[
          styles.box,
          {
            transform: [
              { translateX: position.x },
              { translateY: position.y },
            ],
          },
        ]}
      >
        <Text>Drag me!</Text>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  box: {
    width: 100,
    height: 100,
    backgroundColor: '#FF5722',
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default PanGestureExample;

Gesture Combinations and Conflict Resolution

Pinch Gesture Implementation

import React, { useRef, useState } from 'react';
import { View, Text, PanResponder, StyleSheet } from 'react-native';

const PinchGestureExample = () => {
  const [scale, setScale] = useState(1);
  const [lastDistance, setLastDistance] = useState(0);

  const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: () => true,
      onMoveShouldSetPanResponder: () => true,

      onPanResponderGrant: (evt) => {
        if (evt.nativeEvent.touches.length === 2) {
          const dx = evt.nativeEvent.touches[0].pageX - evt.nativeEvent.touches[1].pageX;
          const dy = evt.nativeEvent.touches[0].pageY - evt.nativeEvent.touches[1].pageY;
          const distance = Math.sqrt(dx * dx + dy * dy);
          setLastDistance(distance);
        }
      },

      onPanResponderMove: (evt) => {
        if (evt.nativeEvent.touches.length === 2) {
          const dx = evt.nativeEvent.touches[0].pageX - evt.nativeEvent.touches[1].pageX;
          const dy = evt.nativeEvent.touches[0].pageY - evt.nativeEvent.touches[1].pageY;
          const distance = Math.sqrt(dx * dx + dy * dy);
          
          if (lastDistance > 0) {
            const newScale = scale * (distance / lastDistance);
            setScale(Math.min(Math.max(newScale, 0.5), 3)); // Limit scale range
          }
          
          setLastDistance(distance);
        }
      },

      onPanResponderRelease: () => {
        setLastDistance(0);
      },
    })
  ).current;

  return (
    <View style={styles.container}>
      <View
        {...panResponder.panHandlers}
        style={[
          styles.box,
          {
            transform: [{ scale }],
          },
        ]}
      >
        <Text>Pinch to zoom</Text>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  box: {
    width: 200,
    height: 200,
    backgroundColor: '#9C27B0',
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default PinchGestureExample;

Gesture Conflict Resolution

import React, { useRef, useState } from 'react';
import { View, Text, PanResponder, StyleSheet, TouchableOpacity } from 'react-native';
import { TapGestureHandler, PanGestureHandler, State } from 'react-native-gesture-handler';

const GestureConflictExample = () => {
  const [tapCount, setTapCount] = useState(0);
  const [panCount, setPanCount] = useState(0);

  const tapResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: () => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => {
        // Do not respond to tap if movement exceeds 5 pixels
        return Math.abs(gestureState.dx) < 5 && Math.abs(gestureState.dy) < 5;
      },
      onPanResponderGrant: () => {
        // Use a short delay to confirm tap
        setTimeout(() => {
          if (Math.abs(gestureState.dx) < 5 && Math.abs(gestureState.dy) < 5) {
            setTapCount(tapCount + 1);
          }
        }, 100);
      },
      onPanResponderMove: () => {},
      onPanResponderRelease: () => {},
    })
  ).current;

  const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: (evt, gestureState) => {
        // Respond to drag if movement exceeds 5 pixels
        return Math.abs(gestureState.dx) >= 5 || Math.abs(gestureState.dy) >= 5;
      },
      onPanResponderMove: (evt, gestureState) => {
        setPanCount(panCount + 1);
      },
      onPanResponderRelease: () => {},
    })
  ).current;

  const onTapEvent = (event) => {
    if (event.nativeEvent.state === State.ACTIVE) {
      setTapCount(tapCount + 1);
    }
  };

  const onPanEvent = (event) => {
    if (event.nativeEvent.state === State.ACTIVE) {
      setPanCount(panCount + 1);
    }
  };

  return (
    <View style={styles.container}>
      <View
        style={styles.box}
        {...tapResponder.panHandlers}
      >
        <Text>Tap me (count: {tapCount})</Text>
      </View>
      
      <View
        style={[styles.box, { marginTop: 20 }]}
        {...panResponder.panHandlers}
      >
        <Text>Drag me (count: {panCount})</Text>
      </View>
      
      {/* Method 2: Combining TouchableOpacity and Gesture Handler */}
      <TapGestureHandler onHandlerStateChange={onTapEvent}>
        <View style={[styles.box, { marginTop: 20 }]}>
          <Text>Tap me (count: {tapCount})</Text>
        </View>
      </TapGestureHandler>
      
      <PanGestureHandler onHandlerStateChange={onPanEvent}>
        <View style={[styles.box, { marginTop: 20 }]}>
          <Text>Drag me (count: {panCount})</Text>
        </View>
      </PanGestureHandler>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  box: {
    width: 200,
    height: 100,
    backgroundColor: '#4CAF50',
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default GestureConflictExample;

Advanced Gesture Interaction

ScrollView and Gesture Interaction

import React, { useRef, useState } from 'react';
import { View, Text, ScrollView, PanResponder, StyleSheet } from 'react-native';

const ScrollViewGestureExample = () => {
  const scrollViewRef = useRef(null);
  const [scrollEnabled, setScrollEnabled] = useState(true);

  const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: () => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => {
        // Disable ScrollView scrolling when vertical movement is greater
        if (Math.abs(gestureState.dy) > Math.abs(gestureState.dx)) {
          setScrollEnabled(false);
          return true;
        }
        return false;
      },
      onPanResponderGrant: () => {
        setScrollEnabled(false);
      },
      onPanResponderRelease: () => {
        setScrollEnabled(true);
      },
      onPanResponderTerminate: () => {
        setScrollEnabled(true);
      },
    })
  ).current;

  return (
    <View style={styles.container}>
      <ScrollView
        ref={scrollViewRef}
        scrollEnabled={scrollEnabled}
        style={styles.scrollView}
      >
        {/* Long content */}
        {Array.from({ length: 50 }).map((_, i) => (
          <Text key={i} style={styles.text}>Item {i}</Text>
        ))}
      </ScrollView>
      
      <View
        {...panResponder.panHandlers}
        style={styles.overlay}
      >
        <Text>Drag to disable ScrollView scrolling</Text>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  scrollView: {
    flex: 1,
  },
  text: {
    padding: 20,
    fontSize: 18,
    borderBottomWidth: 1,
    borderBottomColor: '#ccc',
  },
  overlay: {
    position: 'absolute',
    top: 100,
    left: 50,
    right: 50,
    height: 100,
    backgroundColor: 'rgba(255,0,0,0.3)',
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default ScrollViewGestureExample;

Gesture Sequence Recognition

import React, { useRef, useState } from 'react';
import { View, Text, PanResponder, StyleSheet } from 'react-native';

const GestureSequenceExample = () => {
  const [sequence, setSequence] = useState([]);
  const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: () => true,
      onMoveShouldSetPanResponder: () => true,

      onPanResponderGrant: (evt, gestureState) => {
        setSequence(['Grant']);
      },

      onPanResponderMove: (evt, gestureState) => {
        if (Math.abs(gestureState.dx) > Math.abs(gestureState.dy)) {
          setSequence(prev => [...prev, 'Horizontal Move']);
        } else {
          setSequence(prev => [...prev, 'Vertical Move']);
        }
      },

      onPanResponderRelease: (evt, gestureState) => {
        setSequence(prev => [...prev, 'Release']);
        
        // Recognize specific gesture sequence
        if (sequence.join(',').includes('Grant,Horizontal Move,Release')) {
          console.log('Horizontal swipe detected');
        } else if (sequence.join(',').includes('Grant,Vertical Move,Release')) {
          console.log('Vertical swipe detected');
        }
        
        // Reset sequence
        setTimeout(() => setSequence([]), 1000);
      },
    })
  ).current;

  return (
    <View style={styles.container}>
      <View
        {...panResponder.panHandlers}
        style={styles.box}
      >
        <Text>Perform gesture sequence</Text>
      </View>
      
      <Text>Current gesture sequence: {sequence.join(', ')}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  box: {
    width: 200,
    height: 200,
    backgroundColor: '#FF9800',
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default GestureSequenceExample;

Performance Optimization and Best Practices

Gesture Performance Optimization Techniques

import React, { useRef, memo } from 'react';
import { View, Text, PanResponder, StyleSheet } from 'react-native';

// Use memo to avoid unnecessary re-renders
const OptimizedGestureComponent = memo(() => {
  const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: () => true,
      onMoveShouldSetPanResponder: () => true,
      // Use throttle to reduce event handling frequency
      onPanResponderMove: throttle((evt, gestureState) => {
        console.log('Throttled move event', gestureState.dx, gestureState.dy);
      }, 16), // Approx. 60fps
    })
  ).current;

  return (
    <View
      {...panResponder.panHandlers}
      style={styles.box}
    >
      <Text>Optimized Gesture</Text>
    </View>
  );
});

// Simple throttle function implementation
function throttle(func, limit) {
  let lastFunc;
  let lastRan;
  return function(...args) {
    if (!lastRan) {
      func.apply(this, args);
      lastRan = Date.now();
    } else {
      clearTimeout(lastFunc);
      lastFunc = setTimeout(function() {
        if ((Date.now() - lastRan) >= limit) {
          func.apply(this, args);
          lastRan = Date.now();
        }
      }, limit - (Date.now() - lastRan));
    }
  }
}

const styles = StyleSheet.create({
  box: {
    width: 200,
    height: 200,
    backgroundColor: '#607D8B',
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default OptimizedGestureComponent;

Gesture Recognition Best Practices

import React, { useRef, useState } from 'react';
import { View, Text, PanResponder, StyleSheet, Animated } from 'react-native';

const BestPracticeGestureExample = () => {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const animatedValue = useRef(new Animated.ValueXY()).current;
  const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: () => true,
      onMoveShouldSetPanResponder: () => true,
      
      // Use animation for smooth movement
      onPanResponderMove: (evt, gestureState) => {
        animatedValue.x.setValue(gestureState.dx);
        animatedValue.y.setValue(gestureState.dy);
      },
      
      onPanResponderRelease: () => {
        // Add spring animation effect
        Animated.spring(animatedValue, {
          toValue: { x: 0, y: 0 },
          friction: 4,
          tension: 30,
          useNativeDriver: true, // Use native driver for better performance
        }).start();
      },
    })
  ).current;

  // Bind animation value to position state
  React.useEffect(() => {
    animatedValue.addListener(({ x, y }) => {
      setPosition({ x, y });
    });
    
    return () => {
      animatedValue.removeAllListeners();
    };
  }, [animatedValue]);

  return (
    <View style={styles.container}>
      <Animated.View
        {...panResponder.panHandlers}
        style=[
          styles.box,
          {
            transform: [
              { translateX: animatedValue.x },
              { translateY: animatedValue.y },
            ],
          },
        ]
      >
        <Text>Drag me with animation</Text>
      </Animated.View>
      
      <Text>Current position: x: {position.x.toFixed(2)}, y: {position.y.toFixed(2)}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  box: {
    width: 150,
    height: 150,
    backgroundColor: '#E91E63',
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default BestPracticeGestureExample;

Summary of Gesture System and Event Interaction

React Native provides various gesture recognition and handling methods, from simple taps and swipes to complex pinch and rotation gestures. Developers can choose the appropriate approach based on their needs:

  1. Simple Gestures: Use built-in components like TouchableOpacity and TouchableWithoutFeedback for basic tap interactions.
  2. Complex Gestures: Use the PanResponder API for custom gesture recognition, such as dragging and zooming.
  3. Advanced Gestures: Use the react-native-gesture-handler library for complex gesture combinations and conflict resolution.
  4. Performance Optimization:
    • Use throttling or debouncing to reduce event handling frequency.
    • Use the Animated API for smooth animation effects.
    • Use useNativeDriver to improve animation performance.
    • Avoid complex computations in gesture handler functions.
  5. Best Practices:
    • Handle gesture conflicts appropriately to ensure a smooth user experience.
    • Provide visual feedback for gesture operations.
    • Consider compatibility with different devices and screen sizes.
    • Use native drivers when appropriate to enhance performance.

By combining these techniques effectively, developers can build responsive, user-friendly interactive applications.

Share your love