Skip to content

Commit 76caf44

Browse files
authored
Virtual detector native gestures (#3765)
## Description This PR makes `VirtualDetector` compatible with native gestures. Back in #3689 I decided to handle `VirtualDetector` compatibility with native gestures for later, as it is a extremely niche feature and it seemed to require a lot of work. Turns out it doesn't, beacause we use wrap in VirtualDetector we can handle them as normal gestures. ## Test plan Tested on: <details> ```tsx import * as React from 'react'; import { StyleSheet, Text, View, ScrollView, Button } from 'react-native'; import { GestureHandlerRootView, GestureDetector, useNative, useTap, InterceptingGestureDetector } from 'react-native-gesture-handler'; export default function App() { const items = Array.from({ length: 100 }, (_, index) => `Item ${index + 1}`); const [enabled, setEnabled] = React.useState(true); const gesture = useNative({ onStart: (e) => { 'worklet'; console.log('onStart'); } }); const outerGesture = useTap({ onStart: (e) => { 'worklet' console.log('onOuterStart'); } }); const SV1 = () => ( <ScrollView style={styles.scrollView1}> {items.map((item, index) => ( <View key={index} style={styles.item}> <Text style={styles.text}>{item}</Text> </View> ))} </ScrollView> ); const SV2 = () => ( <ScrollView style={styles.scrollView2}> {items.map((item, index) => ( <View key={index} style={styles.item}> <Text style={styles.text}>{item}</Text> </View> ))} </ScrollView> ); return ( <GestureHandlerRootView style={styles.root}> <View style={styles.buttonContainer}> <Button title="Swap the child" onPress={() => setEnabled(!enabled)} color="#4a90e2" /> </View> <InterceptingGestureDetector gesture={outerGesture}> <View style={styles.outerContainer}> <View style={styles.frame}> <GestureDetector gesture={gesture}> {enabled ? <SV1 /> : <SV2 />} </GestureDetector> </View> </View> </InterceptingGestureDetector> </GestureHandlerRootView> ); } const styles = StyleSheet.create({ scrollView1: { backgroundColor: 'pink', marginHorizontal: 20, }, scrollView2: { backgroundColor: 'lightblue', marginHorizontal: 20, }, item: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', padding: 20, margin: 2, backgroundColor: 'white', borderRadius: 10, }, text: { fontSize: 20, color: 'black', }, root: { flex: 1, backgroundColor: '#fafafa', paddingTop: 60, alignItems: 'center', }, buttonContainer: { marginBottom: 20, width: '80%', }, outerContainer: { padding: 14, backgroundColor: '#fff', borderRadius: 18, borderWidth: 1, borderColor: '#e0e0e0', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.12, shadowRadius: 8, elevation: 5, }, frame: { borderRadius: 14, borderWidth: 1, borderColor: '#d6d6d6', backgroundColor: '#fdfdfd', overflow: 'hidden', }, innerContainer: { paddingVertical: 24, paddingHorizontal: 20, borderRadius: 12, alignItems: 'center', justifyContent: 'center', }, active: { backgroundColor: '#e9f7ef', borderColor: '#4caf50', borderWidth: 1.5, }, inactive: { backgroundColor: '#fff8e1', borderColor: '#ffb300', borderWidth: 1, }, }); ``` </details>
1 parent 8dd560f commit 76caf44

File tree

4 files changed

+60
-37
lines changed

4 files changed

+60
-37
lines changed

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorView.kt

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ class RNGestureHandlerDetectorView(context: Context) : ReactViewGroup(context) {
9090
for (tag in newHandlers) {
9191
handlersToDetach.remove(tag)
9292
if (!attachedHandlers.contains(tag)) {
93-
if (shouldAttachGestureToChildView(tag)) {
93+
if (shouldAttachGestureToChildView(tag) && actionType == GestureHandler.ACTION_TYPE_NATIVE_DETECTOR) {
9494
// It might happen that `attachHandlers` will be called before children are added into view hierarchy. In that case we cannot
9595
// attach `NativeViewGestureHandlers` here and we have to do it in `addView` method.
9696
nativeHandlers.add(tag)
@@ -121,27 +121,32 @@ class RNGestureHandlerDetectorView(context: Context) : ReactViewGroup(context) {
121121
private fun attachVirtualChildren(virtualChildrenToAttach: List<VirtualChildren>) {
122122
val virtualChildrenToDetach = attachedVirtualHandlers.keys.toMutableSet()
123123

124+
for (child in virtualChildrenToAttach) {
125+
virtualChildrenToDetach.remove(child.viewTag)
126+
}
127+
128+
val registry = RNGestureHandlerModule.registries[moduleId]
129+
?: throw Exception("Tried to access a non-existent registry")
130+
131+
for (child in virtualChildrenToDetach) {
132+
for (tag in attachedVirtualHandlers[child]!!) {
133+
registry.detachHandler(tag)
134+
}
135+
attachedVirtualHandlers.remove(tag)
136+
}
137+
124138
for (child in virtualChildrenToAttach) {
125139
if (!attachedVirtualHandlers.containsKey(child.viewTag)) {
126140
attachedVirtualHandlers[child.viewTag] = mutableSetOf()
127141
}
128142

129-
virtualChildrenToDetach.remove(child.viewTag)
130143
attachHandlers(
131144
child.handlerTags,
132145
child.viewTag,
133146
GestureHandler.ACTION_TYPE_VIRTUAL_DETECTOR,
134147
attachedVirtualHandlers[child.viewTag]!!,
135148
)
136149
}
137-
138-
val registry = RNGestureHandlerModule.registries[moduleId]
139-
?: throw Exception("Tried to access a non-existent registry")
140-
141-
for (tag in virtualChildrenToDetach) {
142-
registry.detachHandler(tag)
143-
attachedVirtualHandlers.remove(tag)
144-
}
145150
}
146151

147152
private fun tryAttachNativeHandlersToChildView(childId: Int) {

packages/react-native-gesture-handler/apple/RNGestureHandlerDetector.mm

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ - (void)attachHandlers:(const std::vector<int> &)handlerTags
171171
for (const int tag : handlerTags) {
172172
[handlersToDetach removeObject:@(tag)];
173173
if (![attachedHandlers containsObject:@(tag)]) {
174-
if ([self shouldAttachGestureToSubview:@(tag)]) {
174+
if ([self shouldAttachGestureToSubview:@(tag)] && actionType == RNGestureHandlerActionTypeNativeDetector) {
175175
// It might happen that `attachHandlers` will be called before children are added into view hierarchy. In that
176176
// case we cannot attach `NativeViewGestureHandlers` here and we have to do it in `didAddSubview` method.
177177
[_nativeHandlers addObject:@(tag)];
@@ -236,22 +236,24 @@ - (void)updateVirtualChildren:(const std::vector<RNGestureHandlerDetectorVirtual
236236
}
237237

238238
for (const auto &child : virtualChildren) {
239-
if (_attachedVirtualHandlers.find(child.viewTag) == _attachedVirtualHandlers.end()) {
240-
_attachedVirtualHandlers[child.viewTag] = [NSMutableSet set];
241-
}
242-
243239
[virtualChildrenToDetach removeObject:@(child.viewTag)];
244-
245-
[self attachHandlers:child.handlerTags
246-
actionType:RNGestureHandlerActionTypeVirtualDetector
247-
viewTag:child.viewTag
248-
attachedHandlers:_attachedVirtualHandlers[child.viewTag]];
249240
}
250241

251242
for (const NSNumber *tag : virtualChildrenToDetach) {
252243
for (id handlerTag : _attachedVirtualHandlers[tag.intValue]) {
253244
[handlerManager.registry detachHandlerWithTag:handlerTag];
254245
}
246+
_attachedVirtualHandlers.erase(tag.intValue);
247+
}
248+
249+
for (const auto &child : virtualChildren) {
250+
if (_attachedVirtualHandlers.find(child.viewTag) == _attachedVirtualHandlers.end()) {
251+
_attachedVirtualHandlers[child.viewTag] = [NSMutableSet set];
252+
}
253+
[self attachHandlers:child.handlerTags
254+
actionType:RNGestureHandlerActionTypeVirtualDetector
255+
viewTag:child.viewTag
256+
attachedHandlers:_attachedVirtualHandlers[child.viewTag]];
255257
}
256258
}
257259

packages/react-native-gesture-handler/src/v3/detectors/HostGestureDetector.web.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ const HostGestureDetector = (props: GestureHandlerDetectorProps) => {
5353
if (
5454
RNGestureHandlerModule.getGestureHandlerNode(
5555
tag
56-
).shouldAttachGestureToChildView()
56+
).shouldAttachGestureToChildView() &&
57+
actionType === ActionType.NATIVE_DETECTOR
5758
) {
5859
RNGestureHandlerModule.attachGestureHandler(
5960
tag,
@@ -109,6 +110,14 @@ const HostGestureDetector = (props: GestureHandlerDetectorProps) => {
109110
attachedVirtualHandlers.current.keys()
110111
);
111112

113+
props.virtualChildren?.forEach((child) => {
114+
virtualChildrenToDetach.delete(child.viewTag);
115+
});
116+
117+
virtualChildrenToDetach.forEach((tag) => {
118+
detachHandlers(EMPTY_HANDLERS, attachedVirtualHandlers.current.get(tag)!);
119+
});
120+
112121
props.virtualChildren?.forEach((child) => {
113122
if (child.viewRef.current == null) {
114123
// We must check whether viewRef is not null as otherwise we get an error when intercepting gesture detector
@@ -118,7 +127,6 @@ const HostGestureDetector = (props: GestureHandlerDetectorProps) => {
118127
if (!attachedVirtualHandlers.current.has(child.viewTag)) {
119128
attachedVirtualHandlers.current.set(child.viewTag, new Set());
120129
}
121-
virtualChildrenToDetach.delete(child.viewTag);
122130

123131
const currentHandlerTags = new Set(child.handlerTags);
124132
detachHandlers(
@@ -134,10 +142,6 @@ const HostGestureDetector = (props: GestureHandlerDetectorProps) => {
134142
ActionType.VIRTUAL_DETECTOR
135143
);
136144
});
137-
138-
virtualChildrenToDetach.forEach((tag) => {
139-
detachHandlers(EMPTY_HANDLERS, attachedVirtualHandlers.current.get(tag)!);
140-
});
141145
}, [props.virtualChildren]);
142146

143147
return (

packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,34 @@ export function VirtualDetector<THandlerData, TConfig>(
2323

2424
const viewRef = useRef(null);
2525
const [viewTag, setViewTag] = useState<number>(-1);
26+
2627
const virtualMethods = useRef(props.gesture.detectorCallbacks);
27-
const handleRef = useCallback((node: any) => {
28-
viewRef.current = node;
29-
if (!node) {
30-
return;
31-
}
3228

33-
if (Platform.OS === 'web') {
34-
setViewTag(node);
35-
} else {
36-
const tag = findNodeHandle(node);
29+
const handleRef = useCallback(
30+
(node: any) => {
31+
viewRef.current = node;
32+
if (!node) {
33+
return;
34+
}
35+
36+
const tag = Platform.OS === 'web' ? node : findNodeHandle(node);
37+
3738
if (tag != null) {
3839
setViewTag(tag);
3940
}
40-
}
41-
}, []);
41+
42+
return () => {
43+
if (tag != null) {
44+
const handlerTags = isComposedGesture(props.gesture)
45+
? props.gesture.tags
46+
: [props.gesture.tag];
47+
48+
unregister(tag, handlerTags);
49+
}
50+
};
51+
},
52+
[props.children]
53+
);
4254

4355
useEffect(() => {
4456
virtualMethods.current = props.gesture.detectorCallbacks;

0 commit comments

Comments
 (0)