Skip to content

Commit 2d739ec

Browse files
authored
One detector to rule them all (#3732)
## Description To simplify migration the `GestureDetector` will check the type of the gesture it receives and render the detector accordingly it: * Checks if the gesture it received is V2 or V2: * If it's V2, it renders the old `GestureDetector` component * If it's V3, it checks for the `InterceptingGestureDetector` context: * If context is there, it's inside boundary and renders `LogicDetector` * If the context is not there, it renders `NativeDetector` ### Why separate `NativeDetector` and `InterceptingGestureDetector`? We did some [performance tests](#3689 (comment)) on the `NativeDetector` after adding changes that handle attaching `LogicDetector`, which revealed that the new logic adds quite a bit of overhead, but only on the JS side. New logic on the native side does not seem to have a significant effect. We concluded that the best solution is to create a separate component that will have all functionalities of `NativeDetector` and also allow `LogicDetector` attachment, while NativeDetector will be reverted to how it had been before implementing `LogicDetector`. ## Test plan Rendering `LogicDetector`: <details> ```tsx import React from 'react'; import { Text, View, StyleSheet } from 'react-native'; import { GestureDetectorBoundary, GestureDetector, useTap, GestureHandlerRootView } from 'react-native-gesture-handler'; import Svg, { Circle, Rect } from 'react-native-svg'; export default function SvgExample() { const circleElementTap = useTap({ onStart: () => { 'worklet'; console.log('RNGH: clicked circle') }, }); const rectElementTap = useTap({ onStart: () => { 'worklet'; console.log('RNGH: clicked parallelogram') }, }); return ( <GestureHandlerRootView> <View style={styles.container}> <Text style={styles.header}> Overlapping SVGs with gesture detectors </Text> <View style={{ backgroundColor: 'tomato' }}> <GestureDetectorBoundary> <Svg height="250" width="250" onPress={() => console.log('SVG: clicked container')}> <GestureDetector gesture={circleElementTap}> <Circle cx="125" cy="125" r="125" fill="green" onPress={() => console.log('SVG: clicked circle')} /> </GestureDetector> <GestureDetector gesture={rectElementTap}> <Rect skewX="45" width="125" height="250" fill="yellow" onPress={() => console.log('SVG: clicked parallelogram')} /> </GestureDetector> </Svg> </GestureDetectorBoundary> </View> <Text> Tapping each color should read to a different console.log output </Text> </View> </GestureHandlerRootView> ); } const styles = StyleSheet.create({ container: { alignItems: 'center', justifyContent: 'center', marginBottom: 48, }, header: { fontSize: 18, fontWeight: 'bold', margin: 10, }, }); ``` </details Rendering `NativeDetector`: <details> ```tsx import React from 'react'; import { StyleSheet, View } from 'react-native'; import { GestureDetector, useTap, } from 'react-native-gesture-handler'; import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing, interpolateColor, } from 'react-native-reanimated'; export default function SimpleTap() { const colorValue = useSharedValue(0); const tapGesture = useTap({ onStart: () => { 'worklet'; colorValue.value = withTiming(colorValue.value === 0 ? 1 : 0, { duration: 400, easing: Easing.inOut(Easing.ease), }); }, }); const animatedStyle = useAnimatedStyle(() => { const backgroundColor = interpolateColor( colorValue.value, [0, 1], ['#b58df1', '#ff7f50'] // purple → coral ); return { backgroundColor, }; }); return ( <View style={styles.centerView}> <GestureDetector gesture={tapGesture}> <Animated.View style={[styles.box, animatedStyle]} /> </GestureDetector> </View> ); } const styles = StyleSheet.create({ centerView: { flex: 1, justifyContent: 'center', alignItems: 'center', }, box: { height: 120, width: 120, backgroundColor: '#b58df1', marginBottom: 30, borderRadius: 12, }, }); ``` </details Rendering old v2 `GestureDetector`: <details> ```tsx import React from 'react'; import { StyleSheet, View } from 'react-native'; import { Gesture, GestureDetector, } from 'react-native-gesture-handler'; import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing, interpolateColor, } from 'react-native-reanimated'; export default function SimpleTap() { const colorValue = useSharedValue(0); const tapGesture = Gesture.Tap() .onStart(() => { 'worklet'; colorValue.value = withTiming(colorValue.value === 0 ? 1 : 0, { duration: 400, easing: Easing.inOut(Easing.ease), }); }); const animatedStyle = useAnimatedStyle(() => { const backgroundColor = interpolateColor( colorValue.value, [0, 1], ['#b58df1', '#ff7f50'] // purple → coral ); return { backgroundColor, }; }); return ( <View style={styles.centerView}> <GestureDetector gesture={tapGesture}> <Animated.View style={[styles.box, animatedStyle]} /> </GestureDetector> </View> ); } const styles = StyleSheet.create({ centerView: { flex: 1, justifyContent: 'center', alignItems: 'center', }, box: { height: 120, width: 120, backgroundColor: '#b58df1', marginBottom: 30, borderRadius: 12, }, }); ``` </details
1 parent 50d6d7b commit 2d739ec

File tree

15 files changed

+260
-76
lines changed

15 files changed

+260
-76
lines changed

apps/basic-example/src/NativeDetector.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react';
22
import { Animated, Button, useAnimatedValue } from 'react-native';
33
import {
44
GestureHandlerRootView,
5-
NativeDetector,
5+
GestureDetector,
66
usePan,
77
} from 'react-native-gesture-handler';
88

@@ -32,7 +32,7 @@ export default function App() {
3232
/>
3333

3434
{visible && (
35-
<NativeDetector gesture={gesture}>
35+
<GestureDetector gesture={gesture}>
3636
<Animated.View
3737
style={[
3838
{
@@ -48,7 +48,7 @@ export default function App() {
4848
{ transform: [{ translateX: value }] },
4949
]}
5050
/>
51-
</NativeDetector>
51+
</GestureDetector>
5252
)}
5353
</GestureHandlerRootView>
5454
);

apps/basic-example/src/Text.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import { StyleSheet, Text, View } from 'react-native';
33
import {
44
Gesture,
55
GestureDetector,
6-
LogicDetector,
7-
NativeDetector,
6+
InterceptingGestureDetector,
87
useTap,
98
} from 'react-native-gesture-handler';
109

@@ -37,24 +36,24 @@ function NativeDetectorExample() {
3736
<Text style={styles.header}>
3837
Native Detector example - this one should work
3938
</Text>
40-
<NativeDetector gesture={tapAll}>
39+
<InterceptingGestureDetector gesture={tapAll}>
4140
<Text style={{ fontSize: 18, textAlign: 'center' }}>
4241
Some text example running with RNGH
43-
<LogicDetector gesture={tapFirstPart}>
42+
<GestureDetector gesture={tapFirstPart}>
4443
<Text style={{ fontSize: 24, color: COLORS.NAVY }}>
4544
{' '}
4645
try tapping on this part
4746
</Text>
48-
</LogicDetector>
49-
<LogicDetector gesture={tapSecondPart}>
47+
</GestureDetector>
48+
<GestureDetector gesture={tapSecondPart}>
5049
<Text style={{ fontSize: 28, color: COLORS.KINDA_BLUE }}>
5150
{' '}
5251
or on this part
5352
</Text>
54-
</LogicDetector>
53+
</GestureDetector>
5554
this part is not special :(
5655
</Text>
57-
</NativeDetector>
56+
</InterceptingGestureDetector>
5857
</View>
5958
);
6059
}

packages/react-native-gesture-handler/src/__tests__/RelationsTraversal.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { tagMessage } from '../utils';
22
import { useExclusive, useRace, useSimultaneous } from '../v3/hooks/relations';
33
import { useGesture } from '../v3/hooks/useGesture';
4-
import { configureRelations } from '../v3/NativeDetector/utils';
4+
import { configureRelations } from '../v3/detectors/utils';
55
import { SingleGesture, SingleGestureName } from '../v3/types';
66
import { renderHook } from '@testing-library/react-native';
77

packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ function propagateDetectorConfig(
4242
}
4343
}
4444

45-
interface GestureDetectorProps {
45+
export interface GestureDetectorProps {
4646
children?: React.ReactNode;
4747
/**
4848
* A gesture object containing the configuration and callbacks.

packages/react-native-gesture-handler/src/index.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export { RotationGestureHandler } from './handlers/RotationGestureHandler';
5151
export { FlingGestureHandler } from './handlers/FlingGestureHandler';
5252
export { default as createNativeWrapper } from './handlers/createNativeWrapper';
5353
export type { NativeViewGestureHandlerProps } from './handlers/NativeViewGestureHandler';
54-
export { GestureDetector } from './handlers/gestures/GestureDetector';
54+
export { GestureDetector as LegacyGestureDetector } from './handlers/gestures/GestureDetector';
5555
export { GestureObjects as Gesture } from './handlers/gestures/gestureObjects';
5656
export type { TapGestureType as LegacyTapGesture } from './handlers/gestures/tapGesture';
5757
export type { PanGestureType as LegacyPanGesture } from './handlers/gestures/panGesture';
@@ -149,10 +149,12 @@ export type {
149149
} from './components/Pressable';
150150
export { default as Pressable } from './components/Pressable';
151151

152-
export type { NativeDetectorProps } from './v3/NativeDetector/NativeDetector';
153-
export { NativeDetector } from './v3/NativeDetector/NativeDetector';
152+
export {
153+
GestureDetector,
154+
InterceptingGestureDetector,
155+
GestureDetectorProps,
156+
} from './v3/detectors';
154157

155-
export { LogicDetector } from './v3/LogicDetector';
156158
export * from './v3/hooks/useGesture';
157159
export * from './v3/hooks/relations';
158160

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { NativeDetectorProps } from './common';
2+
import { BaseGesture } from '../../handlers/gestures/gesture';
3+
import { NativeDetector } from './NativeDetector';
4+
import { ComposedGesture } from '../../handlers/gestures/gestureComposition';
5+
import {
6+
GestureDetectorProps as LegacyGestureDetectorProps,
7+
GestureDetector as LegacyGestureDetector,
8+
} from '../../handlers/gestures/GestureDetector';
9+
import { DetectorContext } from './LogicDetector/useDetectorContext';
10+
import { LogicDetector } from './LogicDetector/LogicDetector';
11+
import { use } from 'react';
12+
13+
export function GestureDetector<THandlerData, TConfig>(
14+
props: NativeDetectorProps<THandlerData, TConfig> | LegacyGestureDetectorProps
15+
) {
16+
if (
17+
props.gesture instanceof ComposedGesture ||
18+
props.gesture instanceof BaseGesture
19+
) {
20+
return <LegacyGestureDetector {...(props as LegacyGestureDetectorProps)} />;
21+
}
22+
23+
const context = use(DetectorContext);
24+
return context ? (
25+
<LogicDetector {...(props as NativeDetectorProps<THandlerData, TConfig>)} />
26+
) : (
27+
<NativeDetector
28+
{...(props as NativeDetectorProps<THandlerData, TConfig>)}
29+
/>
30+
);
31+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ const HostGestureDetector = (props: GestureHandlerDetectorProps) => {
110110
);
111111

112112
props.logicChildren?.forEach((child) => {
113+
if (child.viewRef.current == null) {
114+
// We must check whether viewRef is not null as otherwise we get an error when intercepting gesture detector
115+
// switches its component based on whether animated/reanimated events should run.
116+
return;
117+
}
113118
if (!attachedLogicHandlers.current.has(child.viewTag)) {
114119
attachedLogicHandlers.current.set(child.viewTag, new Set());
115120
}

packages/react-native-gesture-handler/src/v3/NativeDetector/NativeDetector.tsx renamed to packages/react-native-gesture-handler/src/v3/detectors/LogicDetector/InterceptingGestureDetector.tsx

Lines changed: 65 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,55 @@
11
import React, { RefObject, useCallback, useRef, useState } from 'react';
2-
import { Animated, StyleSheet } from 'react-native';
3-
import HostGestureDetector from './HostGestureDetector';
4-
import { tagMessage } from '../../utils';
2+
import HostGestureDetector from '../HostGestureDetector';
53
import {
64
LogicChildren,
7-
Gesture,
8-
DetectorCallbacks,
95
GestureHandlerEvent,
10-
} from '../types';
6+
DetectorCallbacks,
7+
} from '../../types';
118
import { DetectorContext } from './useDetectorContext';
12-
import { Reanimated } from '../../handlers/gestures/reanimatedWrapper';
13-
import { configureRelations } from './utils';
14-
import { isComposedGesture } from '../hooks/utils/relationUtils';
15-
16-
export interface NativeDetectorProps<THandlerData, TConfig> {
17-
children?: React.ReactNode;
18-
gesture: Gesture<THandlerData, TConfig>;
19-
}
20-
21-
const AnimatedNativeDetector =
22-
Animated.createAnimatedComponent(HostGestureDetector);
23-
24-
const ReanimatedNativeDetector =
25-
Reanimated?.default.createAnimatedComponent(HostGestureDetector);
26-
27-
export function NativeDetector<THandlerData, TConfig>({
9+
import { Reanimated } from '../../../handlers/gestures/reanimatedWrapper';
10+
import { configureRelations, ensureNativeDetectorComponent } from '../utils';
11+
import { isComposedGesture } from '../../hooks/utils/relationUtils';
12+
import {
13+
AnimatedNativeDetector,
14+
InterceptingGestureDetectorProps,
15+
nativeDetectorStyles,
16+
ReanimatedNativeDetector,
17+
} from '../common';
18+
import { tagMessage } from '../../../utils';
19+
20+
export function InterceptingGestureDetector<THandlerData, TConfig>({
2821
gesture,
2922
children,
30-
}: NativeDetectorProps<THandlerData, TConfig>) {
23+
}: InterceptingGestureDetectorProps<THandlerData, TConfig>) {
3124
const [logicChildren, setLogicChildren] = useState<LogicChildren[]>([]);
25+
3226
const logicMethods = useRef<
3327
Map<number, RefObject<DetectorCallbacks<unknown>>>
3428
>(new Map());
3529

36-
const NativeDetectorComponent = gesture.config.dispatchesAnimatedEvents
30+
const [shouldUseReanimated, setShouldUseReanimated] = useState(
31+
gesture ? gesture.config.shouldUseReanimatedDetector : false
32+
);
33+
const [dispatchesAnimatedEvents, setDispatchesAnimatedEvents] = useState(
34+
gesture ? gesture.config.dispatchesAnimatedEvents : false
35+
);
36+
37+
const NativeDetectorComponent = dispatchesAnimatedEvents
3738
? AnimatedNativeDetector
38-
: gesture.config.shouldUseReanimatedDetector
39+
: shouldUseReanimated
3940
? ReanimatedNativeDetector
4041
: HostGestureDetector;
4142

4243
const register = useCallback(
43-
(child: LogicChildren, methods: RefObject<DetectorCallbacks<unknown>>) => {
44+
(
45+
child: LogicChildren,
46+
methods: RefObject<DetectorCallbacks<unknown>>,
47+
forReanimated: boolean | undefined,
48+
forAnimated: boolean | undefined
49+
) => {
50+
setShouldUseReanimated(!!forReanimated);
51+
setDispatchesAnimatedEvents(!!forAnimated);
52+
4453
setLogicChildren((prev) => {
4554
const index = prev.findIndex((c) => c.viewTag === child.viewTag);
4655
if (index !== -1) {
@@ -76,11 +85,9 @@ export function NativeDetector<THandlerData, TConfig>({
7685
);
7786
}
7887

79-
configureRelations(gesture);
80-
8188
const handleGestureEvent = (key: keyof DetectorCallbacks<THandlerData>) => {
8289
return (e: GestureHandlerEvent<THandlerData>) => {
83-
if (gesture.detectorCallbacks[key]) {
90+
if (gesture?.detectorCallbacks[key]) {
8491
gesture.detectorCallbacks[key](e);
8592
}
8693

@@ -97,7 +104,7 @@ export function NativeDetector<THandlerData, TConfig>({
97104
(key: keyof DetectorCallbacks<unknown>) => {
98105
const handlers: ((e: GestureHandlerEvent<THandlerData>) => void)[] = [];
99106

100-
if (gesture.detectorCallbacks[key]) {
107+
if (gesture?.detectorCallbacks[key]) {
101108
handlers.push(
102109
gesture.detectorCallbacks[key] as (
103110
e: GestureHandlerEvent<unknown>
@@ -116,21 +123,27 @@ export function NativeDetector<THandlerData, TConfig>({
116123

117124
return handlers;
118125
},
119-
[logicChildren, gesture.detectorCallbacks]
126+
[logicChildren, gesture?.detectorCallbacks]
120127
);
121128

122129
const reanimatedEventHandler = Reanimated?.useComposedEventHandler(
123130
getHandlers('onReanimatedUpdateEvent')
124131
);
125-
const reanimedStateChangeHandler = Reanimated?.useComposedEventHandler(
132+
const reanimatedStateChangeHandler = Reanimated?.useComposedEventHandler(
126133
getHandlers('onReanimatedStateChange')
127134
);
128135
const reanimatedTouchEventHandler = Reanimated?.useComposedEventHandler(
129136
getHandlers('onReanimatedTouchEvent')
130137
);
131138

139+
ensureNativeDetectorComponent(NativeDetectorComponent);
140+
141+
if (gesture) {
142+
configureRelations(gesture);
143+
}
144+
132145
return (
133-
<DetectorContext.Provider value={{ register, unregister }}>
146+
<DetectorContext value={{ register, unregister }}>
134147
<NativeDetectorComponent
135148
// @ts-ignore This is a type mismatch between RNGH types and RN Codegen types
136149
onGestureHandlerStateChange={handleGestureEvent(
@@ -140,32 +153,36 @@ export function NativeDetector<THandlerData, TConfig>({
140153
onGestureHandlerEvent={handleGestureEvent('onGestureHandlerEvent')}
141154
// @ts-ignore This is a type mismatch between RNGH types and RN Codegen types
142155
onGestureHandlerAnimatedEvent={
143-
gesture.detectorCallbacks.onGestureHandlerAnimatedEvent
156+
gesture?.detectorCallbacks.onGestureHandlerAnimatedEvent
144157
}
145158
// @ts-ignore This is a type mismatch between RNGH types and RN Codegen types
146159
onGestureHandlerTouchEvent={handleGestureEvent(
147160
'onGestureHandlerTouchEvent'
148161
)}
149162
// @ts-ignore This is a type mismatch between RNGH types and RN Codegen types
150-
onGestureHandlerReanimatedStateChange={reanimatedEventHandler}
163+
onGestureHandlerReanimatedStateChange={
164+
shouldUseReanimated ? reanimatedStateChangeHandler : undefined
165+
}
151166
// @ts-ignore This is a type mismatch between RNGH types and RN Codegen types
152-
onGestureHandlerReanimatedEvent={reanimedStateChangeHandler}
167+
onGestureHandlerReanimatedEvent={
168+
shouldUseReanimated ? reanimatedEventHandler : undefined
169+
}
153170
// @ts-ignore This is a type mismatch between RNGH types and RN Codegen types
154-
onGestureHandlerReanimatedTouchEvent={reanimatedTouchEventHandler}
171+
onGestureHandlerReanimatedTouchEvent={
172+
shouldUseReanimated ? reanimatedTouchEventHandler : undefined
173+
}
155174
moduleId={globalThis._RNGH_MODULE_ID}
156-
handlerTags={isComposedGesture(gesture) ? gesture.tags : [gesture.tag]}
157-
style={styles.detector}
175+
handlerTags={
176+
gesture
177+
? isComposedGesture(gesture)
178+
? gesture.tags
179+
: [gesture.tag]
180+
: []
181+
}
182+
style={nativeDetectorStyles.detector}
158183
logicChildren={logicChildren}>
159184
{children}
160185
</NativeDetectorComponent>
161-
</DetectorContext.Provider>
186+
</DetectorContext>
162187
);
163188
}
164-
165-
const styles = StyleSheet.create({
166-
detector: {
167-
display: 'contents',
168-
// TODO: remove, debug info only
169-
backgroundColor: 'red',
170-
},
171-
});

packages/react-native-gesture-handler/src/v3/LogicDetector.tsx renamed to packages/react-native-gesture-handler/src/v3/detectors/LogicDetector/LogicDetector.tsx

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
11
import { RefObject, useCallback, useEffect, useRef, useState } from 'react';
2-
import { Wrap } from '../handlers/gestures/GestureDetector/Wrap';
2+
import { Wrap } from '../../../handlers/gestures/GestureDetector/Wrap';
33
import { findNodeHandle, Platform } from 'react-native';
4-
import { useDetectorContext } from './NativeDetector/useDetectorContext';
5-
import { NativeDetectorProps } from './NativeDetector/NativeDetector';
6-
import { isComposedGesture } from './hooks/utils/relationUtils';
7-
import { DetectorCallbacks } from './types';
4+
import { useDetectorContext } from './useDetectorContext';
5+
import { isComposedGesture } from '../../hooks/utils/relationUtils';
6+
import { NativeDetectorProps } from '../common';
7+
import { configureRelations } from '../utils';
8+
import { tagMessage } from '../../../utils';
9+
import { DetectorCallbacks } from '../../types';
810

911
export function LogicDetector<THandlerData, TConfig>(
1012
props: NativeDetectorProps<THandlerData, TConfig>
1113
) {
12-
const { register, unregister } = useDetectorContext();
14+
const context = useDetectorContext();
15+
if (!context) {
16+
throw new Error(
17+
tagMessage(
18+
'Logic detector must be a descendant of an InterceptingGestureDecector'
19+
)
20+
);
21+
}
22+
const { register, unregister } = context;
23+
1324
const viewRef = useRef(null);
1425
const [viewTag, setViewTag] = useState<number>(-1);
1526
const logicMethods = useRef(props.gesture.detectorCallbacks);
16-
1727
const handleRef = useCallback((node: any) => {
1828
viewRef.current = node;
1929
if (!node) {
@@ -52,12 +62,19 @@ export function LogicDetector<THandlerData, TConfig>(
5262
Object.assign(logicProps, { viewRef });
5363
}
5464

55-
register(logicProps, logicMethods as RefObject<DetectorCallbacks<unknown>>);
65+
register(
66+
logicProps,
67+
logicMethods as RefObject<DetectorCallbacks<unknown>>,
68+
props.gesture.config.shouldUseReanimatedDetector,
69+
props.gesture.config.dispatchesAnimatedEvents
70+
);
5671

5772
return () => {
5873
unregister(viewTag, handlerTags);
5974
};
6075
}, [viewTag, props.gesture, register, unregister]);
6176

77+
configureRelations(props.gesture);
78+
6279
return <Wrap ref={handleRef}>{props.children}</Wrap>;
6380
}

0 commit comments

Comments
 (0)