Search code examples
androidiosreactjsreact-nativepressable

React Native fire onLongPress type event but for dragging motion


I am building a game with React Native. In order to make a move in one of the four cardinal directions, the player must swipe anywhere on the screen. Up until now, I have achieved this with the following code, which records the XY position where the gesture begins and then does some calculations based on that when the gesture ends.

<Pressable style={styles.container}
  onPressIn={e => {
    touchPos.current.y = e.nativeEvent.pageY;
    touchPos.current.x = e.nativeEvent.pageX;
  }}
  onPressOut={e => {
    const minDist = 50;
    const vertDist = touchPos.current.y - e.nativeEvent.pageY;
    const horizDist = touchPos.current.x - e.nativeEvent.pageX;

    // Then do some math and pass the swipe information on to game logic
  }}>
    {/* Some game components go here. */}
  </Pressable>

This works fine, but it would be nice if when the user swipes in some direction but does not lift their finger, the game would repeatedly do moves in that direction (every 500ms for example) until they lift their finger. Unfortunately onLongPress will not fire if the user presses and holds in a swipe motion. It only fires if one presses and holds without moving their finger.

I have scoured the web but perhaps I am not using the correct terminology to define my question as I cannot find any mentions of similar issues. The React Native documentation on Pressable isn't much help either. There is a property pressRetentionOffset for how far outside a view a touch can still count as a press but nothing for how much a touch can move inside a view and still trigger onLongPress.

While it would be a suitable solution to figure out onLongPress, all I really need is a way of continuously checking the X and Y position of a touch on an element being touched.


Solution

  • So I figured out the React Native answer (no dependencies) does not involve Pressable at all. Instead, we need PanResponder. For those searching about this issue, the correct terminology for the motion described in the question is not a drag or swipe but rather a pan.

    As for the solution, we want something that looks like this (adapted from the usage pattern here):

    const panResponder = React.useRef(
      PanResponder.create({
        // Ask to be the responder:
        onStartShouldSetPanResponder: (evt, gestureState) => true,
        onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
        onMoveShouldSetPanResponder: (evt, gestureState) => true,
        onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
        onPanResponderTerminationRequest: (evt, gestureState) => true,
        onShouldBlockNativeResponder: (evt, gestureState) => true,
    
        onPanResponderGrant: (evt, gestureState) => {
          // The gesture has started. Since there is no built in 'onLongPress' type
          // event, we now start to keep track of time elapsed ourselves.
        },
        onPanResponderMove: (evt, gestureState) => {
          // The most recent move position is gestureState.moveX, gestureState.moveY
          // Distance moved is also helpful: gestureState.dx, gestureState.dy
          // Once sufficient time has passed, start handling the onLongPress 'drag'
        },
        onPanResponderRelease: (evt, gestureState) => {
          // The user has released all touches while this view is the
          // responder. This typically means a gesture has succeeded.
        },
        onPanResponderTerminate: (evt, gestureState) => {
          // Another component has become the responder, so this gesture
          // should be cancelled.
        },
      })
    ).current;
    

    This is actually much more flexible than having an onLongPress that works for panning motion would be, as it opens the door to other possibilities, such as previewing a move before the user lifts their finger to complete it or moves it back to cancel. Anyways hopefully this helps somebody.