4

The docs say:

Simply set manualActivation to true on a PanGesture and use StateManager to fail the gesture if the user attempts to drag the component sooner than the duration of the long press.

However, I can't figure out how to measure timing in the callbacks of the PanGesture, because the app crashes if I try to use setTimeout, and even if I were able to use setTimeout, I can't get a reference to the GestureStateManager except in the touch callbacks, so I'm not sure how to move the gesture into the START state.

Is there a tool besides setTimeout that I can use to implement a timer in what seems to be an RN Reanimated worklet? For example, can I use performance.now()?

Here's what I have so far:


  const isPressed = useSharedValue(false);
  const isDragging = useSharedValue(false);

  const start = useSharedValue({
    x: 0,
    y: 0,
  });

  const offset = useSharedValue({
    x: 0,
    y: 0,
  });

const gesture =
  Gesture.Pan()
    .manualActivation(true)
    .onBegin((evt) => {
      console.log('pan begin');
    })
    .onStart(() => {
      console.log('pan start');
      isPressed.value = true;
      offset.value = {
        x: 0,
        y: 0,
      };
    })
    .onTouchesDown((evt, state) => {
      if (evt.numberOfTouches !== 1) {
        state.fail();
      }

      isPressed.value = true;
      start.value = {
        x: evt.allTouches[0].x,
        y: evt.allTouches[0].y,
      };

      // using setTimeout here causes a crash, and using runOnJS doesn't fix it 

      // runOnJS(setTimeout)(() => {
      //   isDragging.value = true;
      //   state.activate();
      // }, 500);
    })
    .onTouchesMove((evt, state) => {
      isPressed.value = true;

      const offsetX = start.value.x - evt.allTouches[0].x;
      const offsetY = start.value.y - evt.allTouches[0].y;

      const dist = Math.sqrt(offsetX * offsetX + offsetY * offsetY);

      if (dist > 10) {
        state.fail();
      }
    })
    .onUpdate((evt) => {
      offset.value = {
        x: evt.translationX,
        y: evt.translationY,
      };
    })
    .onFinalize(() => {
      offset.value = {
        x: 0,
        y: 0,
      };
      isPressed.value = false;
      isDragging.value = false;

      console.log('pan finalize');
    });
laptou
  • 6,389
  • 2
  • 28
  • 59

2 Answers2

2

I could achieve the result by using the Composed Gestures concept and by using a boolean SharedValue. I composed the LongPress gesture and PanGesture so I could detect when the LongPress begins and set the SharedValue boolean to true. So basically it would become true after a duration has passed after pressing the button. Then I use the SharedValue in the onTouchesMove function to enable the Pan Gesture

const MyComponent = () => {

  const isLongPressed = useSharedValue(false);

  const longPress = Gesture.LongPress()
    .minDuration(1000)
    .onStart(event => {
      isLongPressed.value = true;
    });

  const panGesture = Gesture.Pan()
    .manualActivation(true)
    .onTouchesMove((event, stateManager) => {
      if (isLongPressed.value) {
        stateManager.activate();
      } else {
        stateManager.fail();
      }
    })
    .onUpdate(event => {
      console.log(event.x);
    })
    .onTouchesUp(() => {
      isLongPressed.value = false;
    });

  const composed = Gesture.Simultaneous(longPress, panGesture);

  return (
    <GestureDetector gesture={composed}>
      <Animated.View style={styles.container}>
       .........
      </Animated.View>
    </GestureDetector>
  );
};
2

UPDATED answer:

react-native-gesture-handler v2.6.0 provided the activateAfterLongPress option as a straight-forward solution for this, e.g. Gesture.Pan().activateAfterLongPress(milliseconds)


(Old answer below: hacky solution, not recommended)

I managed a solution using Date.now() and only the Pan() gesture. But with my solution there is no way to give visual feedback to the user when the long-press delay passes, e.g. device vibration, which could be a dealbreaker.

const dragGesture = Gesture.Pan()
  .manualActivation(true)
  .onTouchesDown((evt, stateManager) => {
    // Save a variable in the stateManager (though I'm not 100 sure
    // if adding custom variables to the state is allowed)
    stateManager.startedAt = Date.now();

    // Activate the gesture right away,
    // so other components cannot steal the gesture
    stateManager.activate();
  })
  .onTouchesMove((evt, stateManager) => {
    if (evt.state !== State.ACTIVE || stateManager.startedAt == null) {
      return;
    }
    
    // Compute enough movement using your offset sharedValue
    const movedEnough = ...;
    if (!movedEnough) {
      return;
    }

    // Check if the long-press delay has passed
    if (Date.now() - stateManager.startedAt < 500) {
      stateManager.fail();
      // The user will have to lift the finger and start over
    }
    
    // Delete the custom variable --> this delay-check is run only once
    stateManager.startedAt = null;
  })
  .onStart(...) // when the user first presses
  .onUpdate(...) // when the user moves the finger
  .onEnd(...); // when finished
  // There is no event fired exactly when the delay ends
pdpino
  • 444
  • 4
  • 13
  • I just started using react-native-gesture-handler recently, I'm also looking forward for better solutions or suggestions – pdpino Mar 16 '22 at 15:24