Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

react-native - Detect a finger move into a view

I am researching about how to detect a finger move into a view in React Native. For example, I have a single view in screen. My finger press outside the view and slowly move to the view. The view can recognize my finger when the finger cross the border.

I researched about Gesture Responder and PanResponder. But all the examples I found are touching on the view and moving it.

If you have a way or an example code to do what I said above, please let me know.

Do i need to do some mathematical calculations to reach the destination.?

Thank you in advance!

like image 379
Luong Truong Avatar asked May 04 '26 11:05

Luong Truong


1 Answers

The previous comments by @LuongTrong touched on this, but it's not as hard as it sounds, once you get a handle on what the PanResponder does and how it works. The way you need to detect when the user drags their finger over a view is using a PanResponder, view.onLayout and view References. When the view you want to track touches into and out of loads, capture the 4 corners of the view (onLayout gives you x,y and height, width so some addition is required). Using the onPanResponderMove method, check whether or not the touchpoint is in the bounds of the view, then update the ref's state accordingly.

It sounds more complicated than it is, here's an App.js (100% demo code) that demonstrates how this works. I've broken the "button" components out to a separate component to make it more reusable.

import React, {useEffect, useState} from 'react'
import {setStatusBarBackgroundColor, StatusBar} from 'expo-status-bar'
import {PanResponder, Pressable, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
import {GestureHandlerRootView, PanGestureHandler} from 'react-native-gesture-handler'
import {useEvent} from 'react-native-reanimated'

const CustButton = React.forwardRef((props, ref) => {
  const [viewLayout, setViewLayout] = useState({})
  const selColor = 'red'
  const unselectedColor = 'blue'
  const [selectedColor, setSelectedColor] = useState(unselectedColor)

  function getViewLatout() {
    return viewLayout
  }

  const updateSelectionStatus = (isActive) => {
    setSelectedColor(isActive ? selColor : unselectedColor)
  }

  const checkPoint = (x,y) => {
    const {top, left, right, bottom} = viewLayout
    return (x > left && x < right && y > top && y < bottom)
  }

  React.useImperativeHandle(ref, () => ({
    // each key is connected to `ref` as a method name
    // they can execute code directly, or call a local method
    setActive: (isActive) => { updateSelectionStatus(isActive) },
    isPointInView: (x,y) => { return checkPoint(x,y)}
  }))

  return (<View
    ref={ref}
    style={[styles.button, {backgroundColor: selectedColor}]}
    onLayout={(evt) => {

      const {x, y, height, width} = evt.nativeEvent.layout
      const viewLayoutProp = {
        top: y,
        bottom: y + height,
        right: x + width,
        left: x
      }
      setViewLayout(viewLayoutProp)
  }}>
    <Text style={{color: 'white', fontSize: 24, fontWeight: 'bold'}}>move over me</Text>
  </View>)
})

export default function App() {

  const viewRef = React.useRef()
  const viewRef2 = React.useRef()
  const viewRef3 = React.useRef()
  const checkIfAViewIsSelected = (evt) => {
    const {pageX:x, pageY:y} = evt.nativeEvent
    viewRef.current.setActive(viewRef.current.isPointInView(x,y))
    viewRef2.current.setActive(viewRef2.current.isPointInView(x,y))
    viewRef3.current.setActive(viewRef3.current.isPointInView(x,y))
  }

  const panResponder = React.useMemo(() =>
      PanResponder.create({
          onStartShouldSetPanResponder: (evt, gestureState) => true,
          onMoveShouldSetPanResponder: (evt, gestureState) => true,
          onPanResponderTerminationRequest: (evt, gestureState) => true,
          onPanResponderRelease: (evt, gestureState) => {
          },
          onPanResponderStart:(evt) => {
            checkIfAViewIsSelected(evt)
          },
          onPanResponderMove: (evt, gestureState) => {
              checkIfAViewIsSelected(evt)
          }
      }), [viewRef, viewRef2, viewRef3])

  useEffect(() => {
    if (viewRef.current == null) {
      return;
    }
  }, [viewRef.current])

  return (
    <View style={styles.container}>

          <View style={[styles.container, {backgroundColor: '#fff'}]}
                {...panResponder.panHandlers}
          >
            <CustButton ref={viewRef}  />
            <CustButton ref={viewRef2} />
            <CustButton ref={viewRef3} />

            <StatusBar style="auto"/>
          </View>
    </View>
  )
}

const styles = StyleSheet.create(
  {
   container: {
     flex: 0,
     backgroundColor: '#555',
     alignItems: 'center',
     justifyContent: 'center',
     gap: 20,
     height: '100%',
       width:'100%'
   },
   button: {
     flex: 0,
     height: 100,
     width: 250,
     backgroundColor: 'red',
     alignItems: 'center',
     justifyContent: 'center'
   }
 })

The real gotcha's in here are:

  • Getting back to the child view and calling methods on the child. You need a reference to the child, which is accomplished with forwardRef,
  • Exposing methods in the child must be done using useImperativeHandle, otherwise the ref cannot see the methods and will crash if use them.
  • Make sure your parent View includes {...panResponders.panHandlers},
  • I had to make my panHandler using useMemo instead of useRef as the examples on the react native website indicate. When using useRef, I wasn't getting any updated view references.

Hope this helps you, or someone else who happens to stumble into this post.

like image 162
James Avatar answered May 06 '26 02:05

James



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!