React Native Gesture Handler

repository·main·Indexed 27 days ago

https://github.com/software-mansion/react-native-gesture-handler

A library providing native-driven gesture management APIs for React Native to build high-performance touch-based experiences. It moves gesture recognition and tracking from the JavaScript thread to the UI thread for smooth interactions. The library includes components like Pressable, and various button implementations (RectButton, BorderlessButton, BaseButton, and RawButton), though some button components are deprecated in favor of Touchable.

Tokens
49.5K
Snippets
102
Records
238
Agent score
92%

What's inside react-native-gesture-handler

  1. Overview of React Native Gesture Handler

    main

    React Native Gesture Handler provides a declarative API that exposes the platform's native touch and gesture system to React Native. It is designed as a high-performance replacement for React Native's built-in Gesture Responder System by handling gestures on the native thread.

    Key Benefits:

    • Native Gesture Recognition: Uses native systems for gestures like pinch, rotation, and pan.
    • Gesture Composition: Allows defining relations between gestures to prevent conflicts between gestures or native components.
    • Native Thread Performance: Provides mechanisms for components running on the native thread to follow platform default behaviors (e.g., delaying pressed states in scrollable components).
    • Reanimated Integration: Deeply integrates with react-native-reanimated to process touch events on the UI thread.
    • Input Support: Supports various input devices including touch screens, pens, and mice.
    • Native Component Compatibility: Allows any native component to be included in the Gesture Handler's touch system.
  2. Use the Touchable component

    main
    The Touchable component is a versatile component introduced in Gesture Handler 3 designed to replace both buttons (RectButton, BorderlessButton) and legacy touchable variants (TouchableOpacity, TouchableHighlight, etc.). It provides native platform animations for opacity, underlay, and scale. On Android, it supports the native ripple effect via the androidRipple prop.
  3. Android Gesture Implementation Details

    main

    On Android, gestures are implemented manually via a custom system to manage gesture interactions.

    Key mechanisms include:

    • GestureHandlerRootView: Wrapping a component in GestureHandlerRootView allows RNGH to intercept all touch events. This view contains a specific handler that decides whether to pass touch events to the underlying view or consume them.
    • Touch Interception: When a pointer touches the screen, the view tree is traversed to extract all handlers attached to the views below the finger. These handlers transition to the BEGAN state.
    • Activation Logic: Touch events are delivered to all extracted handlers until one recognizes the gesture and attempts to activate. The orchestrator then determines if the gesture must wait for other gestures to fail. If it activates, other non-simultaneous gestures are cancelled.
    • State Transitions: The GestureHandlerRootView handler transitions from UNDETERMINED (no touch in progress) to BEGAN when a touch starts. If a gesture handler activates, the root handler consumes all incoming touch events to prevent the underlying view from receiving them.
  4. iOS Gesture Implementation Details

    main
    On iOS, gestures are implemented using UIGestureRecognizers. When you assign a gesture configuration to a GestureDetector, the library creates the required recognizers and assigns them to the detector's child view. Most gesture processing is handled by UIKit, with RNGH providing modifications to allow for customization and alignment with the RNGH state flow.
  5. Use ReanimatedSwipeable for swipeable rows

    main

    ReanimatedSwipeable is a drop-in replacement for the standard Swipeable component, rewritten using Reanimated. It allows users to swipe rows horizontally to reveal action containers.

    Key features:

    • renderLeftActions: Renders a component beneath the row when swiped to the right.
    • renderRightActions: Renders a component beneath the row when swiped to the left.
    • Manual Control: Use a ref to call .openLeft(), .openRight(), .close(), or .reset().

    Note: To support RTL (right-to-left) layouts, use the flexDirection style property in your action render functions.

    import React from 'react';
    import { Text, StyleSheet } from 'react-native';
    
    import { GestureHandlerRootView } from 'react-native-gesture-handler';
    import ReanimatedSwipeable from 'react-native-gesture-handler/ReanimatedSwipeable';
    import Reanimated, {
      SharedValue,
      useAnimatedStyle,
    } from 'react-native-reanimated';
    
    function RightAction(prog: SharedValue<number>, drag: SharedValue<number>) {
      const styleAnimation = useAnimatedStyle(() => {
        console.log('showRightProgress:', prog.value);
        console.log('appliedTranslation:', drag.value);
    
        return {
          transform: [{ translateX: drag.value + 50 }],
        };
      });
    
      return (
        <Reanimated.View style={styleAnimation}>
          <Text style={styles.rightAction}>Text</Text>
        </Reanimated.View>
      );
    }
    
    export default function Example() {
      return (
        <GestureHandlerRootView>
          <ReanimatedSwipeable
            containerStyle={styles.swipeable}
            friction={2}
            enableTrackpadTwoFingerGesture
            rightThreshold={40}
            renderRightActions={RightAction}>
            <Text>Swipe me!</Text>
          </ReanimatedSwipeable>
        </GestureHandlerRootView>
      );
    }
    
    const styles = StyleSheet.create({
      rightAction: { width: 50, height: 50, backgroundColor: 'purple' },
      separator: {
        width: '100%',
        borderTopWidth: 1,
      },
      swipeable: {
        height: 50,
        backgroundColor: 'papayawhip',
        alignItems: 'center',
      },
    });
  6. Migrate gestures to the Gesture Handler 3 hook API

    main

    In Gesture Handler 3, the builder pattern (e.g., Gesture.Pan().onBegin(...)) is replaced by a hook-based API where all configuration is passed as an object to a specific hook.

    Mapping of Gesture Builders to Hooks:

    • Gesture.Pan() $\rightarrow$ usePanGesture()
    • Gesture.Tap() $\rightarrow$ useTapGesture()
    • Gesture.LongPress() $\rightarrow$ useLongPressGesture()
    • Gesture.Rotation() $\rightarrow$ useRotationGesture()
    • Gesture.Pinch() $\rightarrow$ usePinchGesture()
    • Gesture.Fling() $\rightarrow$ useFlingGesture()
    • Gesture.Hover() $\rightarrow$ useHoverGesture()
    • Gesture.Native() $\rightarrow$ useNativeGesture()
    • Gesture.Manual() $\rightarrow$ useManualGesture()

    Note: ForceTouch is not available in the hook API.

    // RNGH 3 Example
    const gesture = usePanGesture({
      onBegin: () => {
        console.log('Pan!');
      },
      minDistance: 25,
    });
  7. Understand expected behaviors and limitations

    main

    Be aware of the following behaviors which are intentional design choices rather than bugs:

    • enabled prop timing: Changing the enabled prop during an active gesture has no effect. The enabled prop is only evaluated when a gesture starts (when a finger touches the screen).
    • Native gesture state flow: Native gestures may not follow the standard state flow due to platform-specific workarounds required to integrate native views into RNGH.
    • Legacy Touchables styling: Legacy Touchables render two additional views. You may need to style both the style and containerStyle props to achieve your desired visual effect.
    • Gesture Composition requirement: For gesture composition to function correctly, all composed gestures must be attached to the same GestureHandlerRootView.
  8. Manually activate existing continuous gestures

    main
    If you want to use existing gestures (like Pinch or Rotation) but need custom activation logic, you do not need to use useManualGesture. Instead, you can use the manualActivation modifier on continuous gestures. This prevents the gesture from activating automatically, giving you full control over when it starts via the state manager.
  9. Transform a view with multiple simultaneous gestures

    main

    To implement complex interactions like a photo viewer (simultaneous pan, pinch, and rotation), do not use separate transform properties in a React transform array. Instead, use an affine matrix to accumulate transformations.

    Key Strategies:

    1. Use an Affine Matrix: Store the accumulated transformation as a single matrix. This allows each new gesture to build upon the previous state by multiplying the current matrix with a new transformation matrix. The in-progress gesture values (scale, rotation, translation) are temporary, and when the gesture ends, they are 'folded' into the main matrix.

    2. Keep the Origin Stable: Scaling and rotation pivot around an origin. To ensure the view pivots around the user's fingers, wrap the transformation between two translations: shift the pivot point to the origin, apply the scale/rotation, and then shift it back.

      matrix = multiply(matrix, translate(origin.x, origin.y));
      matrix = multiply(matrix, scale(scaleValue, scaleValue));
      matrix = multiply(matrix, translate(-origin.x, -origin.y));

      Capture the pivot point once during onActivate and store it in a shared value. Do not recompute it every frame to avoid view jumping.

    3. Composition: Use useSimultaneousGestures to allow multiple gestures (e.g., Pan, Pinch, Rotation) to run at the same time.

    import React, { useState } from 'react';
    import { StyleSheet, View } from 'react-native';
    import {
      GestureDetector,
      usePanGesture,
      usePinchGesture,
      useRotationGesture,
      useSimultaneousGestures,
      useTapGesture,
    } from 'react-native-gesture-handler';
    import Animated, {
      useAnimatedStyle,
      useSharedValue,
    } from 'react-native-reanimated';
    
    // ... (helper functions like identity3, multiply3, scale3, etc.)
    
    function Photo() {
      const [size, setSize] = useState({ width: 0, height: 0 });
      const translation = useSharedValue({ x: 0, y: 0 });
      const origin = useSharedValue({ x: 0, y: 0 });
      const scale = useSharedValue(1);
      const rotation = useSharedValue(0);
      const isRotating = useSharedValue(false);
      const isScaling = useSharedValue(false);
    
      const transform = useSharedValue(identity3());
    
      const style = useAnimatedStyle(() => {
        const matrix = applyTransformations(
          translation.value,
          scale.value,
          rotation.value,
          origin.value,
          transform.value
        );
    
        return {
          transform: [
            { translateX: matrix[6] },
            { translateY: matrix[7] },
            { scale: Math.hypot(matrix[0], matrix[1]) },
            { rotateZ: `${Math.atan2(matrix[1], matrix[0])}rad` },
          ],
        };
      });
    
      const rotationGesture = useRotationGesture({
        onActivate: (e) => {
          if (!isRotating.value && !isScaling.value) {
            origin.value = {
              x: -(e.anchorX - size.width / 2),
              y: -(e.anchorY - size.height / 2),
            };
          }
          isRotating.value = true;
        },
        onUpdate: (e) => {
          rotation.value += e.rotationChange;
        },
        onDeactivate: () => {
          transform.value = applyTransformations(
            translation.value,
            scale.value,
            rotation.value,
            origin.value,
            transform.value
          );
    
          rotation.value = 0;
          translation.value = { x: 0, y: 0 };
          scale.value = 1;
          isRotating.value = false;
        },
      });
    
      const scaleGesture = usePinchGesture({
        onActivate: (e) => {
          if (!isRotating.value && !isScaling.value) {
            origin.value = {
              x: -(e.focalX - size.width / 2),
              y: -(e.focalY - size.height / 2),
            };
          }
          isScaling.value = true;
        },
        onUpdate: (e) => {
          scale.value *= e.scaleChange;
        },
        onDeactivate: () => {
          transform.value = applyTransformations(
            translation.value,
            scale.value,
            rotation.value,
            origin.value,
            transform.value
          );
          rotation.value = 0;
          translation.value = { x: 0, y: 0 };
          scale.value = 1;
          isScaling.value = false;
        },
      });
    
      const panGesture = usePanGesture({
        averageTouches: true,
        onUpdate: (e) => {
          translation.value = {
            x: translation.value.x + e.changeX,
            y: translation.value.y + e.changeY,
          };
        },
        onDeactivate: () => {
          transform.value = applyTransformations(
            translation.value,
            scale.value,
            rotation.value,
            origin.value,
            transform.value
          );
    
          rotation.value = 0;
          translation.value = { x: 0, y: 0 };
          scale.value = 1;
        },
      });
    
      const doubleTapGesture = useTapGesture({
        numberOfTaps: 2,
        onDeactivate: () => {
          scale.value *= 1.25;
        },
      });
    
      const gesture = useSimultaneousGestures(
        rotationGesture,
        scaleGesture,
        panGesture,
        doubleTapGesture
      );
    
      return (
        <GestureDetector gesture={gesture}>
          <Animated.View
            onLayout={({ nativeEvent }) => {
              setSize({
                width: nativeEvent.layout.width,
                height: nativeEvent.layout.height,
              });
            }}
            style={[styles.container, style]}
          />
        </GestureDetector>
      );
    }
    
    export default function Example() {
      return (
        <View style={styles.home}>
          <Photo />
        </View>
      );
    }
    
    const styles = StyleSheet.create({
      home: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
      },
      container: {
        width: 240,
        height: 240,
        backgroundColor: '#5b6ef5',
        elevation: 8,
        borderRadius: 48,
        shadowColor: '#000',
        shadowOffset: { width: 0, height: 2 },
        shadowOpacity: 0.3,
        shadowRadius: 4,
      },
    });
  10. Use Virtual Detectors for non-host components

    main

    In RNGH3, GestureDetector is a host component which can disrupt view hierarchies. To attach gestures to elements that are not standard host components (like SVG elements or specific parts of a Text component) without interfering with the hierarchy, use the following pattern:

    1. Wrap the parent area with InterceptingGestureDetector.
    2. Wrap the specific target element with VirtualGestureDetector.

    InterceptingGestureDetector acts as a proxy for VirtualGestureDetector within its subtree. Its gesture prop is optional if it is only being used to establish context for virtual detectors.

    import React from 'react';
    import { StyleSheet } from 'react-native';
    import {
      GestureHandlerRootView,
      InterceptingGestureDetector,
      useTapGesture,
      VirtualGestureDetector,
    } from 'react-native-gesture-handler';
    import Svg, { Circle } from 'react-native-svg';
    
    export default function App() {
      const outerTap = useTapGesture({
        onDeactivate: () => {
          console.log('Box tapped!');
        },
      });
      const innerTap = useTapGesture({
        onDeactivate: () => {
          console.log('Circle tapped!');
        },
      });
    
      return (
        <GestureHandlerRootView style={styles.container}>
          <InterceptingGestureDetector gesture={outerTap}>
            <Svg height="250" width="250" style={{ backgroundColor: '#b58df1' }}>
              <VirtualGestureDetector gesture={innerTap}>
                <Circle
                  cx="125"
                  cy="125"
                  r="125"
                  fill="#001A72"
                  onPress={() => {}}
                />
              </VirtualGestureDetector>
            </Svg>
          </InterceptingGestureDetector>
        </GestureHandlerRootView>
      );
    }
    
    const styles = StyleSheet.create({
      container: {
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
      },
    });