Skip to content

Commit 2c24bd1

Browse files
authored
Handle Animated.Event in onUpdate callback (#3630)
## Description This PR automatically assigns `onUpdate` callback to `onGestureHandlerAnimatedEvent` when `Animated.Event` is passed. It also checks whether `change*` fields were used in `Animated.Event` - if so, we throw an error. ## Test plan Since basic example is already "under construction", I'll copy whole code. Forgive me for I have sinned 🙏 Basically it covers the case where we switch between `Animated.Event` and standard callback, along with using `change*` in `Animated.Event`. <details> <summary> Test code </summary> ```tsx import * as React from 'react'; import { Animated, Button, useAnimatedValue } from 'react-native'; import { GestureHandlerRootView, NativeDetector, useGesture, } from 'react-native-gesture-handler'; export default function App() { const [visible, setVisible] = React.useState(true); const [shouldUseAnimated, setShouldUseAnimated] = React.useState(true); const value = useAnimatedValue(0); const animatedEvent = Animated.event( [ { nativeEvent: { handlerData: { translationX: value } } }, // { nativeEvent: { handlerData: { changeX: 10 } } }, ], { useNativeDriver: true, } ); const jsCallback = (e: any) => console.log(e.nativeEvent); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const gesture = useGesture('PanGestureHandler', { // onGestureHandlerAnimatedEvent: event, // onGestureHandlerEvent: (e: any) => // console.log('onGestureHandlerEvent', e.nativeEvent), onUpdate: shouldUseAnimated ? animatedEvent : jsCallback, onEnd: (_e: any) => { setShouldUseAnimated((prev) => !prev); }, changeEventCalculator: (event: any, _lastUpdateEvent: any) => { return { ...event.nativeEvent, changeX: 10 }; }, }); return ( <GestureHandlerRootView style={{ flex: 1, backgroundColor: 'white', paddingTop: 8 }}> <Button title="Toggle visibility" onPress={() => { setVisible(!visible); }} /> {visible && ( <NativeDetector gesture={gesture}> <Animated.View style={[ { width: 150, height: 150, backgroundColor: 'blue', opacity: 0.5, borderWidth: 10, borderColor: 'green', marginTop: 20, marginLeft: 40, }, { transform: [{ translateX: value }] }, ]} /> </NativeDetector> )} </GestureHandlerRootView> ); } ``` </details>
1 parent 6c99f4b commit 2c24bd1

File tree

5 files changed

+68
-19
lines changed

5 files changed

+68
-19
lines changed

packages/react-native-gesture-handler/src/v3/hooks/events/useGestureHandlerEvent.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { CALLBACK_TYPE } from '../../../handlers/gestures/gesture';
2-
import { isEventForHandlerWithTag, runWorkletCallback } from '../utils';
2+
import {
3+
isAnimatedEvent,
4+
isEventForHandlerWithTag,
5+
runWorkletCallback,
6+
} from '../utils';
37
import {
48
Reanimated,
59
ReanimatedContext,
@@ -49,8 +53,10 @@ export function useGestureHandlerEvent(
4953
};
5054

5155
if (config.disableReanimated) {
52-
return (event: UpdateEvent<Record<string, unknown>>) =>
53-
onGestureHandlerEvent(event, jsContext);
56+
return isAnimatedEvent(config.onUpdate)
57+
? undefined
58+
: (event: UpdateEvent<Record<string, unknown>>) =>
59+
onGestureHandlerEvent(event, jsContext);
5460
}
5561

5662
// eslint-disable-next-line react-hooks/rules-of-hooks
@@ -65,8 +71,10 @@ export function useGestureHandlerEvent(
6571
!!reanimatedHandler?.doDependenciesDiffer
6672
);
6773

68-
return shouldUseReanimated
69-
? reanimatedEvent
70-
: (event: UpdateEvent<Record<string, unknown>>) =>
71-
onGestureHandlerEvent(event, jsContext);
74+
return isAnimatedEvent(config.onUpdate)
75+
? undefined
76+
: shouldUseReanimated
77+
? reanimatedEvent
78+
: (event: UpdateEvent<Record<string, unknown>>) =>
79+
onGestureHandlerEvent(event, jsContext);
7280
}

packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import RNGestureHandlerModule from '../../RNGestureHandlerModule';
44
import { useGestureEvent } from './useGestureEvent';
55
import { Reanimated } from '../../handlers/gestures/reanimatedWrapper';
66
import { tagMessage } from '../../utils';
7+
import { AnimatedEvent } from '../types';
78

89
type GestureType =
910
| 'TapGestureHandler'
@@ -18,9 +19,9 @@ type GestureType =
1819

1920
type GestureEvents = {
2021
onGestureHandlerStateChange: (event: any) => void;
21-
onGestureHandlerEvent: (event: any) => void;
22+
onGestureHandlerEvent: undefined | ((event: any) => void);
2223
onGestureHandlerTouchEvent: (event: any) => void;
23-
onGestureHandlerAnimatedEvent: (event: any) => void;
24+
onGestureHandlerAnimatedEvent: undefined | AnimatedEvent;
2425
};
2526

2627
export interface NativeGesture {
@@ -77,7 +78,8 @@ export function useGesture(
7778
// we have to mark these as possibly undefined to make TypeScript happy.
7879
if (
7980
!onGestureHandlerStateChange ||
80-
!onGestureHandlerEvent ||
81+
// If onUpdate is an AnimatedEvent, `onGestureHandlerEvent` will be undefined and vice versa.
82+
(!onGestureHandlerEvent && !onGestureHandlerAnimatedEvent) ||
8183
!onGestureHandlerTouchEvent
8284
) {
8385
throw new Error(tagMessage('Failed to create event handlers.'));
@@ -98,10 +100,10 @@ export function useGesture(
98100
useEffect(() => {
99101
// TODO: filter changes - passing functions (and possibly other types)
100102
// causes a native crash
101-
const animatedEvent = config.onGestureHandlerAnimatedEvent;
102-
config.onGestureHandlerAnimatedEvent = null;
103+
const animatedEvent = config.onUpdate;
104+
config.onUpdate = null;
103105
RNGestureHandlerModule.updateGestureHandler(tag, config);
104-
config.onGestureHandlerAnimatedEvent = animatedEvent;
106+
config.onUpdate = animatedEvent;
105107

106108
RNGestureHandlerModule.flushOperations();
107109
}, [config, tag]);
@@ -118,7 +120,7 @@ export function useGesture(
118120
},
119121
shouldUseReanimated,
120122
dispatchesAnimatedEvents:
121-
onGestureHandlerAnimatedEvent &&
123+
!!onGestureHandlerAnimatedEvent &&
122124
'__isNative' in onGestureHandlerAnimatedEvent,
123125
};
124126
}

packages/react-native-gesture-handler/src/v3/hooks/useGestureEvent.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { useGestureStateChangeEvent } from './events/useGestureStateChangeEvent';
22
import { useGestureHandlerEvent } from './events/useGestureHandlerEvent';
33
import { useTouchEvent } from './events/useTouchEvent';
4+
import { AnimatedEvent } from '../types';
5+
import { checkMappingForChangeProperties, isAnimatedEvent } from './utils';
46

57
export function useGestureEvent(
68
handlerTag: number,
@@ -24,10 +26,16 @@ export function useGestureEvent(
2426
shouldUseReanimated
2527
);
2628

27-
// TODO: Assign `onGestureHandlerAnimatedEvent` automatically when user passes `onUpdate` callback as Animated.Event
28-
// Also throw error when someone uses `change*` properties with Animated Event in `onUpdate`
29-
const onGestureHandlerAnimatedEvent =
30-
config.onGestureHandlerAnimatedEvent as (...args: any[]) => void;
29+
let onGestureHandlerAnimatedEvent: AnimatedEvent | undefined;
30+
31+
if (isAnimatedEvent(config.onUpdate)) {
32+
for (const mapping of config.onUpdate._argMapping) {
33+
checkMappingForChangeProperties(mapping);
34+
}
35+
36+
// TODO: Remove cast when config is properly typed.
37+
onGestureHandlerAnimatedEvent = config.onUpdate as AnimatedEvent;
38+
}
3139

3240
return {
3341
onGestureHandlerStateChange,

packages/react-native-gesture-handler/src/v3/hooks/utils.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import { NativeSyntheticEvent } from 'react-native';
1+
import { Animated, NativeSyntheticEvent } from 'react-native';
22
import { CALLBACK_TYPE } from '../../handlers/gestures/gesture';
33
import { TouchEventType } from '../../TouchEventType';
44
import {
5+
AnimatedEvent,
56
CallbackHandlers,
67
GestureHandlerEvent,
78
GestureStateChangeEventWithData,
89
GestureUpdateEventWithData,
910
} from '../types';
1011
import { GestureTouchEvent } from '../../handlers/gestureHandlerCommon';
12+
import { tagMessage } from '../../utils';
1113

1214
export function getHandler(type: CALLBACK_TYPE, config: CallbackHandlers) {
1315
'worklet';
@@ -85,3 +87,25 @@ export function isEventForHandlerWithTag(
8587
? event.nativeEvent.handlerTag === handlerTag
8688
: event.handlerTag === handlerTag;
8789
}
90+
91+
export function isAnimatedEvent(
92+
callback: ((event: any) => void) | AnimatedEvent
93+
): callback is AnimatedEvent {
94+
'worklet';
95+
96+
return '_argMapping' in callback;
97+
}
98+
99+
export function checkMappingForChangeProperties(obj: Animated.Mapping) {
100+
if (!('nativeEvent' in obj) || !('handlerData' in obj.nativeEvent)) {
101+
return;
102+
}
103+
104+
for (const key in obj.nativeEvent.handlerData) {
105+
if (key.startsWith('change')) {
106+
throw new Error(
107+
tagMessage(`${key} is not available when using Animated.Event.`)
108+
);
109+
}
110+
}
111+
}

packages/react-native-gesture-handler/src/v3/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,10 @@ export type CallbackHandlers = Omit<
4141
| 'changeEventCalculator'
4242
| 'onChange'
4343
>;
44+
45+
// This is almost how Animated.event is typed in React Native. We add _argMapping in order to:
46+
// 1. Distinguish it from a regular function,
47+
// 2. Have access to the _argMapping property to check for usage of `change*` callbacks.
48+
export type AnimatedEvent = ((...args: any[]) => void) & {
49+
_argMapping?: unknown;
50+
};

0 commit comments

Comments
 (0)