Skip to content

Commit ae41fec

Browse files
j-piaseckim-bert
andauthored
[General] Listen for Reanimated events directly, without createAnimatedComponent when possible (#3837)
## Description Shouldn't need software-mansion/react-native-reanimated#8655 to work properly Instead of using `createAnimatedComponent`, which does a lot more stuff other than setting up events, we can set up events ourselves using Reanimated's `NativeEventsManager` directly. The implementation in the PR should be independent of the Reanimated version being used, as long as it's a relatively modern one. ## Test plan <details> <summary>Tested on this stress-test</summary> ```jsx import React, { Profiler, useEffect, useRef } from 'react'; import { ScrollView, StyleSheet, View } from 'react-native'; import { GestureDetector, usePanGesture } from 'react-native-gesture-handler'; // import { PerfMonitor } from 'react-native-gesture-handler/src/v3/PerfMonitor'; import Animated, { useAnimatedStyle, useSharedValue, } from 'react-native-reanimated'; const DATA = new Array(500).fill(null).map((_, i) => `Item ${i + 1}`); function Item() { const translateX = useSharedValue(0); const style = useAnimatedStyle(() => { return { transform: [{ translateX: translateX.value }], }; }); const pan = usePanGesture({ // disableReanimated: true, onUpdate: (event) => { 'worklet'; console.log('pan onUpdate', event.handlerData.changeX); }, onStart: () => { 'worklet'; console.log('pan onStart', _WORKLET); }, onEnd: () => { 'worklet'; console.log('pan onEnd'); }, onBegin: () => { 'worklet'; console.log('pan onBegin'); }, onFinalize: () => { 'worklet'; console.log('pan onFinalize'); }, onTouchesDown: () => { 'worklet'; console.log('pan onTouchesDown'); }, }); return ( <View style={{ height: 80, padding: 16, backgroundColor: 'gray' }}> <GestureDetector gesture={pan}> {/* <View collapsable={false} style={{opacity: 0.5}}> */} <Animated.View style={[ { backgroundColor: 'red', height: '100%', aspectRatio: 1 }, style, ]} /> {/* </View> */} </GestureDetector> </View> ); } function Benchmark() { return ( <ScrollView style={{ flex: 1 }} contentContainerStyle={{ flexGrow: 1, gap: 8 }}> {DATA.map((_, index) => ( <Item key={index} /> ))} </ScrollView> ); } const TIMES = 35; export default function EmptyExample() { const times = useRef<number[]>([]).current; const [visible, setVisible] = React.useState(false); useEffect(() => { if (!visible && times.length < TIMES) { setTimeout(() => { setVisible(true); }, 24); } if (times.length === TIMES) { // calculate average, but remove highest and lowest const sortedTimes = [...times].sort((a, b) => a - b); sortedTimes.shift(); sortedTimes.shift(); sortedTimes.pop(); sortedTimes.pop(); const avgTime = sortedTimes.reduce((sum, time) => sum + time, 0) / sortedTimes.length; console.log(`Average render time: ${avgTime} ms`); // console.log(JSON.stringify(PerfMonitor.getMeasures(), null, 2)); // PerfMonitor.clear(); } }, [visible]); return ( <View style={styles.container}> {visible && ( <Profiler id="v3" onRender={(_id, _phase, actualDuration) => { times.push(actualDuration); setTimeout(() => { setVisible(false); }, 24); }}> <Benchmark /> </Profiler> )} </View> ); } const styles = StyleSheet.create({ container: { flex: 1, }, }); ``` </details> Before: 614ms, after: 576ms --------- Co-authored-by: Michał Bert <63123542+m-bert@users.noreply.github.com>
1 parent 6a6dc33 commit ae41fec

File tree

7 files changed

+117
-6
lines changed

7 files changed

+117
-6
lines changed

packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@ export type ReanimatedHandler<THandlerData> = {
2929
context: ReanimatedContext<THandlerData>;
3030
};
3131

32+
export type NativeEventsManager = new (component: {
33+
props: Record<string, unknown>;
34+
_componentRef: React.Ref<unknown>;
35+
// Removed in https://github.com/software-mansion/react-native-reanimated/pull/6736
36+
// but we likely want to keep it for compatibility with older Reanimated versions
37+
_componentViewTag: number;
38+
getComponentViewTag: () => number;
39+
}) => {
40+
attachEvents: () => void;
41+
detachEvents: () => void;
42+
updateEvents: (prevProps: Record<string, unknown>) => void;
43+
};
44+
3245
let Reanimated:
3346
| {
3447
default: {
@@ -38,6 +51,7 @@ let Reanimated:
3851
options?: unknown
3952
): ComponentClass<P>;
4053
};
54+
NativeEventsManager: NativeEventsManager;
4155
useHandler: <THandlerData>(
4256
handlers: GestureCallbacks<THandlerData>
4357
) => ReanimatedHandler<THandlerData>;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
import RNGestureHandlerDetectorNativeComponent from '../../specs/RNGestureHandlerDetectorNativeComponent';
2+
export type { NativeProps as RNGestureHandlerDetectorNativeComponentProps } from '../../specs/RNGestureHandlerDetectorNativeComponent';
23
const HostGestureDetector = RNGestureHandlerDetectorNativeComponent;
34
export default HostGestureDetector;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import {
66
AnimatedNativeDetector,
77
NativeDetectorProps,
88
nativeDetectorStyles,
9-
ReanimatedNativeDetector,
109
} from './common';
10+
import { ReanimatedNativeDetector } from './ReanimatedNativeDetector';
1111

1212
export function NativeDetector<THandlerData, TConfig>({
1313
gesture,
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { useEffect, useMemo, useRef } from 'react';
2+
import {
3+
NativeEventsManager,
4+
Reanimated,
5+
} from '../../handlers/gestures/reanimatedWrapper';
6+
import HostGestureDetector, {
7+
type RNGestureHandlerDetectorNativeComponentProps,
8+
} from './HostGestureDetector';
9+
import { findNodeHandle } from 'react-native';
10+
11+
let NativeEventsManagerImpl = Reanimated?.NativeEventsManager;
12+
13+
if (!NativeEventsManagerImpl) {
14+
// When Reanimated.NativeEventsManager is undefined, it may be that an older
15+
// Reanimated version is used which doesn't export NativeEventsManager, or
16+
// Reanimated is not installed at all. For the older versions, try to import
17+
// NativeEventsManager by a subpath. Otherwise, it will stay undefined.
18+
try {
19+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
20+
NativeEventsManagerImpl =
21+
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-member-access
22+
require('react-native-reanimated/src/createAnimatedComponent/NativeEventsManager').NativeEventsManager;
23+
} catch {
24+
// fail silently
25+
}
26+
}
27+
28+
type WorkletProps = Pick<
29+
RNGestureHandlerDetectorNativeComponentProps,
30+
| 'onGestureHandlerReanimatedStateChange'
31+
| 'onGestureHandlerReanimatedEvent'
32+
| 'onGestureHandlerReanimatedTouchEvent'
33+
>;
34+
35+
function LeanReanimatedNativeDetector(
36+
props: RNGestureHandlerDetectorNativeComponentProps
37+
) {
38+
const prevProps = useRef<WorkletProps>(null);
39+
const eventManager = useRef<InstanceType<NativeEventsManager> | null>(null);
40+
const viewRef = useRef<any>(null);
41+
42+
const {
43+
onGestureHandlerReanimatedStateChange,
44+
onGestureHandlerReanimatedEvent,
45+
onGestureHandlerReanimatedTouchEvent,
46+
...restProps
47+
} = props;
48+
49+
const reaProps: WorkletProps = useMemo(
50+
() => ({
51+
onGestureHandlerReanimatedStateChange,
52+
// @ts-ignore This is a type mismatch between RNGH types and RN Codegen types
53+
onGestureHandlerReanimatedEvent,
54+
// @ts-ignore This is a type mismatch between RNGH types and RN Codegen types
55+
onGestureHandlerReanimatedTouchEvent,
56+
}),
57+
[
58+
onGestureHandlerReanimatedEvent,
59+
onGestureHandlerReanimatedStateChange,
60+
onGestureHandlerReanimatedTouchEvent,
61+
]
62+
);
63+
64+
useEffect(() => {
65+
const nativeTag = findNodeHandle(viewRef.current) ?? -1;
66+
// @ts-expect-error Reanimated expects __nativeTag to be present on the ref
67+
viewRef.__nativeTag = nativeTag;
68+
// @ts-expect-error NativeEventsManager should be defined here, if it isn't, we should
69+
// go the fallback way and use Reanimated's createAnimatedComponent
70+
eventManager.current = new NativeEventsManagerImpl({
71+
props: reaProps,
72+
_componentRef: viewRef,
73+
_componentViewTag: nativeTag,
74+
getComponentViewTag: () => nativeTag,
75+
});
76+
eventManager.current.attachEvents();
77+
78+
return () => {
79+
eventManager.current?.detachEvents();
80+
};
81+
}, []);
82+
83+
useEffect(() => {
84+
if (prevProps.current) {
85+
eventManager.current?.updateEvents(prevProps.current);
86+
}
87+
prevProps.current = reaProps;
88+
}, [reaProps]);
89+
90+
return <HostGestureDetector ref={viewRef} {...restProps} />;
91+
}
92+
93+
export const ReanimatedNativeDetector = NativeEventsManagerImpl
94+
? LeanReanimatedNativeDetector
95+
: Reanimated?.default.createAnimatedComponent(HostGestureDetector);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Reanimated } from '../../handlers/gestures/reanimatedWrapper';
2+
import HostGestureDetector from './HostGestureDetector';
3+
4+
export const ReanimatedNativeDetector =
5+
Reanimated?.default.createAnimatedComponent(HostGestureDetector);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ import {
1717
AnimatedNativeDetector,
1818
InterceptingGestureDetectorProps,
1919
nativeDetectorStyles,
20-
ReanimatedNativeDetector,
2120
} from '../common';
2221
import { tagMessage } from '../../../utils';
2322
import { useEnsureGestureHandlerRootView } from '../useEnsureGestureHandlerRootView';
23+
import { ReanimatedNativeDetector } from '../ReanimatedNativeDetector';
2424

2525
interface VirtualChildrenForNative {
2626
viewTag: number;

packages/react-native-gesture-handler/src/v3/detectors/common.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import React from 'react';
22
import { Gesture } from '../types';
3-
import { Reanimated } from '../../handlers/gestures/reanimatedWrapper';
43
import { Animated, StyleSheet } from 'react-native';
54
import HostGestureDetector from './HostGestureDetector';
65
import { GestureDetectorProps as LegacyDetectorProps } from '../../handlers/gestures/GestureDetector';
@@ -23,9 +22,6 @@ export type GestureDetectorProps<THandlerData, TConfig> =
2322
export const AnimatedNativeDetector =
2423
Animated.createAnimatedComponent(HostGestureDetector);
2524

26-
export const ReanimatedNativeDetector =
27-
Reanimated?.default.createAnimatedComponent(HostGestureDetector);
28-
2925
export const nativeDetectorStyles = StyleSheet.create({
3026
detector: {
3127
display: 'contents',

0 commit comments

Comments
 (0)