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.