JWilk
JWilk

Reputation: 48

Cancel button click when finger is dragged off

Goal

Cancel custom button's onResponderRelease() when the user's finger moves off of it. Is there a standard way of doing this that I'm not finding online?

What I've tried

Using onLayout to grab the component's position and the onResponderMove's GestureResponderEvent's pageX/pageY to calculate the click area. However, onLayout only gets the position relative to the parent, so this doesn't work well with modals.

  const handleMove = (e: GestureResponderEvent) => {
    const { pageX, pageY } = e.nativeEvent
    if (pageX < minX || pageX > maxX || pageY < minY || pageY > maxY) {
      setCancelClick(true)
      setLongPressed(false)
      setPressed(false)
    }
  }

  const setComponentDimensions = ({ nativeEvent }: LayoutChangeEvent) => {
    const { x, y, width, height } = nativeEvent.layout
    setMinX(x - BUFFER)
    setMaxX(x + width + BUFFER)
    setMinY(y - BUFFER)
    setMaxY(y + height + BUFFER)
  }

  return (
    <View
      style={[style, pressed && pressedStyle]}
      onStartShouldSetResponder={() => true}
      onResponderGrant={() => setPressed(true)}
      onResponderRelease={handleRelease}
      onResponderMove={handleMove}
      onLayout={setComponentDimensions}
      onResponderTerminationRequest={() => false}
      {...ViewProps}>
      {children}
    </View>
  )

Other thoughts

I have also considered using a useRef and the ref's measure. This might work. A component from another library that I'm using doesn't seem to have have measure on its ref, so my concern is that this may not be the most flexible solution.

  const viewRef = useRef(null as View | null)

  useEffect(() => {
    if (viewRef.current) {
      // TODO: base calculations off of measure
      console.log(viewRef.current.measure)
    }
  }, [viewRef.current])

  ...

  return (
    <View
      ref={(view) => (viewRef.current = view)}
      ...
      {children}
    </View>
  )

Upvotes: 0

Views: 397

Answers (1)

JWilk
JWilk

Reputation: 48

Using measure() in the following way seems to work for me.

  const viewRef = useRef(null as View | null)
  const [dimensions, setDimensions] = useState({
    minX: 0,
    maxX: 0,
    minY: 0,
    maxY: 0,
  })

  const handleMove = (e: GestureResponderEvent) => {
    const { pageX, pageY } = e.nativeEvent
    const { minX, maxX, minY, maxY } = dimensions

    if (pageX < minX || pageX > maxX || pageY < minY || pageY > maxY) {
      setLongPressed(false)
      setPressed(false)
    }
  }

  const setComponentDimensions = ({ nativeEvent }: LayoutChangeEvent) => {
    const { width, height } = nativeEvent.layout
    if (viewRef.current) {
      viewRef.current.measure((_a, _b, _width, _height, px, py) => {
        setDimensions({
          minX: px - BUFFER,
          maxX: px + width + BUFFER,
          minY: py - BUFFER,
          maxY: py + height + BUFFER,
        })
      })
    }
  }

  return (
    <View
      ref={(view) => (viewRef.current = view)}
      onStartShouldSetResponder={() => true}
      onResponderMove={handleMove}
      onLayout={setComponentDimensions}
      onResponderTerminationRequest={() => false}
      {children}
    </View>
  )

Update

After refactoring my components a bit, and having never tried it before, I decided to publish a package. Hopefully, it helps someone else: react-native-better-buttons.

I ended up with a simpler hook that just needs to be put on the component's ref

const useMeasurement = (buffer = 0) => {
  const ref = useRef<View | null>(null)
  const [dimensions, setDimensions] = useState({
    minX: 0,
    maxX: 0,
    minY: 0,
    maxY: 0,
  })

  /* This goes on the view's ref property */
  const setRef = (view: View) => (ref.current = view)

  /* 
     Updating ref.current does not cause rerenders, which is fine for us.
     We just want to check for ref.current updates when component that uses
     us rerenders.
  */
  useEffect(() => {
    if (ref.current && ref.current.measure) {
      ref.current.measure((_a, _b, width, height, px, py) => {
        setDimensions({
          minX: px - buffer,
          maxX: px + width + buffer,
          minY: py - buffer,
          maxY: py + height + buffer,
        })
      })
    }
  }, [ref.current])

  return { dimensions, setRef }
}

Upvotes: 1

Related Questions