reactjsreact-nativedrawingreact-native-svgpanresponder

react-native [Expo]: can I increase PanResponder's input sampling rate?


I am currently implementing a react-native-svg based drawing input for my Expo application (it is a slight modification of this answer: link). I use a PanResponder to register move events and create different paths that I then display as multiple Polyline elements while the user draws on the view like so:

const GesturePath: React.FC<GesturePathProps> = ({
  paths,
  color,
  width,
  height,
  strokeWidth,
}) => {
  return (
    <Svg height='100%' width='100%' viewBox={`0 0 ${width} ${height}`}>
      {paths.map((path) => (
        <Polyline
          key={path.id}
          points={path.points.map((p) => `${p.x},${p.y}`).join(" ")}
          fill='none'
          stroke={color}
          strokeWidth={strokeWidth}
        />
      ))}
    </Svg>
  );
};

Unfortunately, the lines I produce are extremely rough and jagged and I believe the PanResponder.onPanResponderMove handler is fired too infrequently for my needs (on average it is called every 50ms on the Android Pixel 4 emulator and I'm not sure if I can expect more from a real device).

Maybe there is better candidate than PanResponder for handling gestures in my use case?

I have implemented a smoothing function (based on this answer link) that behaves correctly but since the points are so distant from each other the user's input gets noticeably distorted.

Here's an example without smoothing:

SVG drawing example

Below my GestureHandler implementation:

const GestureRecorder: React.FC<GestureRecorderProps> = ({ addPath }) => {
  const buffRef = useRef<Position[]>([]);
  const pathRef = useRef<Position[]>([]);
  const timeRef = useRef<number>(Date.now());
  const pathIdRef = useRef<number>(0);

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

      onPanResponderMove: (event) => {
        // workaround for release event
        // not working consistently on android
        if (Date.now() - timeRef.current > RELEASE_TIME) {
          pathIdRef.current += 1;
          pathRef.current = [];
          buffRef.current = [];
        }
        timeRef.current = Date.now();

        pathRef.current.push({
          x: event.nativeEvent.locationX,
          y: event.nativeEvent.locationY,
        });

        addPath({
          points: calculateSmoothedPath(event),
          id: pathIdRef.current,
        });
      },

      // not working on Android
      // release event is not consistent
      // onPanResponderRelease: () => {
      // pathIdRef.current += 1;
      // pathRef.current = [];
      // buffRef.current = [];
      // },
    })
  ).current;

  const calculateSmoothedPath = (event: GestureResponderEvent) => { 
    // implementation
    // ...
    // see: https://stackoverflow.com/questions/40324313/svg-smooth-freehand-drawing
  }

  return (
    <View
      style={StyleSheet.absoluteFill}
      collapsable={false}
      {...panResponder.panHandlers}
    />
  );
};

Side Note

I have not found any documentation on PanResponder suggesting that a sample rate configuration option exists so I am totally open to alternatives (even dropping the PanResponder + native SVG method entirely) as long as I don't have to eject the expo project and I can have control over the layout (I don't want to use external components that come with specific UIs).

I have tried using the expo-pixi library (specifically the Sketch component), but the repository seems not to be mantained anymore and the expo client crashes consistently when using it.


Solution

  • I have been trying different solutions after I posted the question and I have ended up changing my GestureHandler implementation slightly. I am now using PanGestureHandler from react-native-gesture-handler instead of PanResponder. This component lets the user specify a minDist prop for tweaking activation, here is the description from the docs:

    Minimum distance the finger (or multiple finger) need to travel before the handler activates. Expressed in points.

    Below the new implementation:

    <PanGestureHandler
      minDist={1}
      onGestureEvent={(e) => {
        // same code from the previous onPanResponderMove
      }
    >
      <View style={StyleSheet.absoluteFill} collapsable={false} />
    </PanGestureHandler>
    

    I had also overestimated my GPU capabilities while running the emulator, after building an APK and testing on a real device I realized the drawing input behaves much more fluidly.