-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Separate Reanimated from JS
#3682
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
m-bert
commented
Aug 22, 2025
packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureStateChangeEvent.ts
Outdated
Show resolved
Hide resolved
j-piasecki
reviewed
Aug 25, 2025
...sture-handler/android/src/main/java/com/swmansion/gesturehandler/react/events/EventTarget.kt
Outdated
Show resolved
Hide resolved
.../src/main/java/com/swmansion/gesturehandler/react/events/RNGestureHandlerStateChangeEvent.kt
Outdated
Show resolved
Hide resolved
...ndroid/src/main/java/com/swmansion/gesturehandler/react/events/RNGestureHandlerTouchEvent.kt
Outdated
Show resolved
Hide resolved
packages/react-native-gesture-handler/apple/RNGestureHandlerEvents.mm
Outdated
Show resolved
Hide resolved
packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureStateChangeEvent.ts
Outdated
Show resolved
Hide resolved
packages/react-native-gesture-handler/src/v3/hooks/callbacks/onGestureHandlerEvent.ts
Outdated
Show resolved
Hide resolved
packages/react-native-gesture-handler/src/v3/hooks/useGestureCallbacks.ts
Outdated
Show resolved
Hide resolved
j-piasecki
reviewed
Aug 25, 2025
...ve-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt
Show resolved
Hide resolved
j-piasecki
reviewed
Sep 3, 2025
packages/react-native-gesture-handler/apple/RNGestureHandler.mm
Outdated
Show resolved
Hide resolved
...ges/react-native-gesture-handler/src/v3/hooks/callbacks/GestureHandlerEventWorkletHandler.ts
Outdated
Show resolved
Hide resolved
...act-native-gesture-handler/src/v3/hooks/callbacks/GestureHandlerStateChangeWorkletHandler.ts
Outdated
Show resolved
Hide resolved
...eact-native-gesture-handler/src/v3/hooks/callbacks/GestureHandlerTouchEventWorkletHandler.ts
Outdated
Show resolved
Hide resolved
packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts
Outdated
Show resolved
Hide resolved
packages/react-native-gesture-handler/src/v3/hooks/useGestureCallbacks.ts
Outdated
Show resolved
Hide resolved
j-piasecki
approved these changes
Sep 5, 2025
packages/react-native-gesture-handler/apple/RNGestureHandler.mm
Outdated
Show resolved
Hide resolved
Merged
m-bert
added a commit
that referenced
this pull request
Sep 15, 2025
> [!IMPORTANT] > Supersede #3664. > > I've decided to create a separate PR since it was easier to start working on it directly than waiting for #3682 to be merged. ## Description This PR introduces hooks to set relations between handlers. ## API New API replaces the old one as follows: - `Gesture.Race(g1, g2)` $\rightarrow$ `useRace(g1, g2)` - `Gesture.Exclusive(g1, g2)` $\rightarrow$ `useExclusive(g1, g2)` - `Gesture.Simultaneous(g1, g2)` $\rightarrow$ `useSimultaneous(g1, g2)` ## Algorithm for populating relations ### Handling external relations In order to properly handle gesture relations, we need to pass 3 arrays to the native side: - `waitFor` - responsible for handling `Exclusive` and `requireExternalGestureToFail` relations - `simultaneousHandlers` - responsible for `Simultaneous` and `simultaneousWithExternalGesture` relations - `blocksHandlers` - responsible for `blocksExternalGesture` relation At first, these arrays are filled with external relations in `useGesture` hook. Then we use `DFS` algorithm to add remaining relations, added with relation hooks. Since `Race` doesn't really change anything when it comes to gesture interactions, we can ignore it in our algorithm. ### DFS overview We use `DFS` because gesture relations form tree structure. <details> <summary>The algorithm works as follows:</summary> - Initialize two arrays: `waitFor` and `simultaneousHandlers`. If root node is `SimultaneousGesture`, we also add its handler tags into `simultaneousHandlers` array. This ensures that the algorithm works even if we have only `Simultaneous` as the root node (e.g. `useSimultaneous(g1, g1)`) - Traverse the gesture tree: - If we are not in the `ComposedGesture`, it means that we reached leaf node. In that case we populate `waitFor` and `simultanoursHandlers` arrays into `node` arrays and then update relations on the native side - If we are in the `ComposedGesture`, then for each child: - If the child is not `ComposedGesture`: - We call `traverseGestureRelations` to reach stop condition and configure relations on the native side - If current node is `Exclusive`, then we add child `tag` to `waitFor` array - If the child is `ComposedGesture`: - On the way down: - Going from `non-simultaneous` gesture to `simultaneous` gesture we add all **child** tags into global `simulatneousHandlers` array - If we go from `simultaneous` to `non-simultaneous` gesture, we remove **child** tags instead of adding. - We store length of `waitFor` to reset it later. - We call `traverseGestureRelations` - On the way back: - if we go from `simultaneous` (child) to `non-simultaneous` (node) gesture, we remove **node** tags from `simultaneousHandlers` - Going from `non-simultaneous` (child) to `simultaneous` (node) gesture we add **node** tags instead of removing - Returning to `Exclusive` gesture means that we want to add all children tags into `waitFor` - If we return from `Exclusive` child to `non-exclusive` node, we want to reset `waitFor` to previous state, using `length` variable. </details> ### Example Below you can see example of the algorithm. <details> <summary>We use the following notation:</summary> - Handlers and composition: - `E` - `Exclusive` - `S` - `Simultaneous` - `P` - `Pan` - `T` - `Tap` - Relation arrays: - `SH` - `simultaneousHandlers` - `WF` - `waitFor` - Operators: - `+=` - adding tags - `-=` - removing tags </details> _**Note:**_ vertex label in relation arrays expands to all tags in the composed gesture. ```mermaid graph TB E1["E₁"] --> |SH += S₁| S1 E1["E₁"] --> |SH += S₂| S2 S1["S₁"] --> |SH -= S₁ <br/> WF += S₁| E1 S1 --> |SH -= E₂| E2 S1 --> P3 E2["E₂"] --> |SH += E₂ <br/> WF -= E₂| S1 P3["P₃ <br/> SH: {T₁, T₂, P₃}<br/>WF: #91;#93;"] --> S1 E2 --> T1 E2 --> T2 T1["T₁ <br/> SH: {P₃}<br/>WF: #91;#93;"] --> |WF += T₁| E2 T2["T₂ <br/> SH: {P₃}<br/>WF: #91;T₁#93;"] --> |WF += T₂| E2 S2["S₂"] -->|SH -= S₂| E1 S2 --> P4 S2 --> P5 P4["P₄ <br/> SH: {P₄, P₅}<br/>WF: #91;T₁, T₂, P₃#93;"] --> S2 P5["P₅ <br/> SH: {P₄, P₅}<br/>WF: #91;T₁, T₂, P₃#93;"] --> S2 style DFS fill-opacity:0,stroke-opacity:0,stroke-width:0px ``` ## Limitations Currently the following setup doesn't work on `android`: ```js const composedGesture = useExclusive(tap1, useRace(pan1, pan2)); ``` I've managed to find out what is the difference between this and using only `useRace`. >[!WARNING] > This problem seems to be present also on `main`, so I think it will be better to solve it in the follow-up PR. For now, external relation props do not support composed gestures. Let me know if this should be done in this PR, or in a follow-up. ## Test plan ### Same detector interactions Verified that the following relations work: <details> <summary>Android</summary> - [x] `Simultaneous` - [x] Only JS - [x] Only Reanimated - [x] JS + Reanimated - [x] JS + Animated - [x] `Exclusive` - [x] Only JS - [x] Only Reanimated - [x] JS + Reanimated - [x] JS + Animated - [x] `Race` - [x] Only JS - [x] Only Reanimated - [x] JS + Reanimated - [x] JS + Animated - [x] Simple composition - [x] `Exclusive` + `Simultaneous` </details> <details> <summary>iOS</summary> - [x] `Simultaneous` - [x] Only JS - [x] Only Reanimated - [x] JS + Reanimated - [x] JS + Animated - [x] `Exclusive` - [x] Only JS - [x] Only Reanimated - [x] JS + Reanimated - [x] JS + Animated - [x] `Race` - [x] Only JS - [x] Only Reanimated - [x] JS + Reanimated - [x] JS + Animated - [x] Simple composition - [x] `Exclusive` + `Simultaneous` </details> <details> <summary>Base code used for testing:</summary> ```tsx import * as React from 'react'; import { Animated, Button, useAnimatedValue } from 'react-native'; import { GestureHandlerRootView, NativeDetector, useSimultaneous, useGesture, useExclusive, useRace, } from 'react-native-gesture-handler'; export default function App() { const [visible, setVisible] = React.useState(true); const value = useAnimatedValue(0); const event = Animated.event( [{ nativeEvent: { handlerData: { translationX: value } } }], { useNativeDriver: true, } ); const tap1 = useGesture('TapGestureHandler', { onEnd: () => { // 'worklet'; console.log('Tap 1'); }, numberOfTaps: 1, disableReanimated: true, }); const tap2 = useGesture('TapGestureHandler', { onEnd: () => { // 'worklet'; console.log('Tap 2'); }, numberOfTaps: 2, disableReanimated: true, }); const pan1 = useGesture('PanGestureHandler', { // onUpdate: event, onUpdate: (e) => { // 'worklet'; console.log('Pan 1'); }, disableReanimated: true, }); const pan2 = useGesture('PanGestureHandler', { onUpdate: (e) => { // 'worklet'; console.log('Pan 2'); }, disableReanimated: true, }); const composedGesture = useSimultaneous(pan1, pan2); // const composedGesture = useExclusive(tap2, tap1); // const composedGesture = useExclusive(pan2, pan1); // For Animtaed.Event // const composedGesture = useExclusive(pan1, pan2); // For Animtaed.Event // const composedGesture = useRace(pan1, pan2); // const composedGesture = useRace(pan2, pan1); // const composedGesture = useExclusive(tap1, useSimultaneous(pan1, pan2)); return ( <GestureHandlerRootView style={{ flex: 1, backgroundColor: 'white', paddingTop: 8 }}> <Button title="Toggle visibility" onPress={() => { setVisible(!visible); }} /> {visible && ( <NativeDetector gesture={composedGesture}> <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> ### Cross detector interactions Verified that the following relations work: <details> <summary>Android</summary> - [x] `simultaneousWithExternalGesture` - [x] Only JS - [x] Only Reanimated - [x] JS + Reanimated - [x] JS + Animated - [x] `requireExternalGestureToFail` - [x] Only JS - [x] Only Reanimated - [x] JS + Reanimated - [x] JS + Animated - [x] `blocksExternalGesture` - [x] Only JS - [x] Only Reanimated - [x] JS + Reanimated - [x] JS + Animated </details> <details> <summary>iOS</summary> - [x] `simultaneousWithExternalGesture` - [x] Only JS - [x] Only Reanimated - [x] JS + Reanimated - [x] JS + Animated - [x] `requireExternalGestureToFail` - [x] Only JS - [x] Only Reanimated - [x] JS + Reanimated - [x] JS + Animated - [x] `blocksExternalGesture` - [x] Only JS - [x] Only Reanimated - [x] JS + Reanimated - [x] JS + Animated </details> <details> <summary>Base code used for testing:</summary> ```tsx import * as React from 'react'; import { Animated, Button, useAnimatedValue } from 'react-native'; import { GestureHandlerRootView, NativeDetector, useSimultaneous, useGesture, useExclusive, useRace, } from 'react-native-gesture-handler'; export default function App() { const [visible, setVisible] = React.useState(true); const value = useAnimatedValue(0); const event = Animated.event( [{ nativeEvent: { handlerData: { translationX: value } } }], { useNativeDriver: true, } ); const tap1 = useGesture('TapGestureHandler', { onEnd: () => { // 'worklet'; console.log('Tap 1'); }, numberOfTaps: 1, disableReanimated: true, }); const tap2 = useGesture('TapGestureHandler', { onEnd: () => { // 'worklet'; console.log('Tap 2'); }, numberOfTaps: 2, disableReanimated: true, blocksExternalGesture: tap1, }); // const tap1 = useGesture('TapGestureHandler', { // onEnd: () => { // 'worklet'; // console.log('Tap 1'); // }, // numberOfTaps: 1, // // disableReanimated: true, // requireExternalGestureToFail: tap2, // }); const pan1 = useGesture('PanGestureHandler', { // onUpdate: event, onUpdate: (e) => { 'worklet'; console.log('Pan 1'); }, // disableReanimated: true, }); const pan2 = useGesture('PanGestureHandler', { onUpdate: (e) => { 'worklet'; console.log('Pan 2'); }, simultaneousWithExternalGesture: pan1, // requireExternalGestureToFail: pan1, // blocksExternalGesture: pan1, // disableReanimated: true, }); // const composedGesture = useSimultaneous(pan1, pan2); // const composedGesture = useExclusive(tap2, tap1); // const composedGesture = useExclusive(pan2, pan1); // For Animated.Event // const composedGesture = useExclusive(pan1, pan2); // For Animated.Event // const composedGesture = useRace(pan1, pan2); // const composedGesture = useRace(pan2, pan1); // const composedGesture = useExclusive(tap1, useSimultaneous(pan1, pan2)); return ( <GestureHandlerRootView style={{ flex: 1, backgroundColor: 'white', paddingTop: 8 }}> <Button title="Toggle visibility" onPress={() => { setVisible(!visible); }} /> {visible && ( <NativeDetector gesture={pan1}> <Animated.View style={[ { width: 150, height: 150, backgroundColor: 'blue', opacity: 0.5, borderWidth: 10, borderColor: 'green', marginTop: 20, marginLeft: 40, display: 'flex', alignItems: 'center', justifyContent: 'space-around', }, { transform: [{ translateX: value }] }, ]}> <NativeDetector gesture={pan2}> <Animated.View style={{ width: 100, height: 100, backgroundColor: 'green' }} /> </NativeDetector> </Animated.View> </NativeDetector> )} </GestureHandlerRootView> ); } ``` </details>
akwasniewski
added a commit
that referenced
this pull request
Sep 18, 2025
## Description Web portion of functionality from PR #3682 > Currently we have 2 ways of handling events - Animated and JS/Reanimated. Handling both JS and Reanimated in the same places results in more complex codebase. It also introduces problems with composing gestures. We decided to spit those implementations. ## Test plan ```tsx import * as React from 'react'; import { Animated, Button } from 'react-native'; import { GestureHandlerRootView, NativeDetector, useGesture, } from 'react-native-gesture-handler'; export default function App() { const [visible, setVisible] = React.useState(true); const av = React.useRef(new Animated.Value(0)).current const event = Animated.event( [{ handlerData: { translationX: av } }], { useNativeDriver: false, } ); const gesture = useGesture('PanGestureHandler', { onBegin: (e: unknown) => { 'worklet'; console.log('onBegin', e); }, onStart: (e: unknown) => { 'worklet'; console.log('onStart', e); }, // onUpdate: event, onUpdate: (e: unknown) => { 'worklet'; console.log('onUpdate', e); }, onEnd: (e: unknown) => { 'worklet'; console.log('onEnd', e); }, onFinalize: (e: unknown) => { 'worklet'; console.log('onFinalize', e); }, onTouchesDown: (e: unknown) => { 'worklet'; console.log('onTouchesDown', e); }, onTouchesMove: (e: unknown) => { 'worklet'; console.log('onTouchesMoved', e); }, onTouchesUp: (e: unknown) => { 'worklet'; console.log('onTouchesUp', e); }, onTouchesCancelled: (e: unknown) => { 'worklet'; console.log('onTouchesCancelled', e); }, }); 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: av }] }, ]} /> </NativeDetector> )} </GestureHandlerRootView> ); } ```
Merged
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Description
Currently we have 2 ways of handling events -
AnimatedandJS/Reanimated. Handling bothJSandReanimatedin the same places results in more complex codebase. It also introduces problems with composing gestures. We decided to split those implementations.Test plan
Tested on the following code: