-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Gesture relations #3693
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
Gesture relations #3693
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
akwasniewski
reviewed
Sep 9, 2025
packages/react-native-gesture-handler/src/v3/hooks/relations/useComposedGesture.ts
Outdated
Show resolved
Hide resolved
packages/react-native-gesture-handler/src/v3/hooks/utils/relationUtils.ts
Outdated
Show resolved
Hide resolved
j-piasecki
reviewed
Sep 12, 2025
packages/react-native-gesture-handler/src/v3/hooks/utils/relationUtils.ts
Show resolved
Hide resolved
j-piasecki
approved these changes
Sep 15, 2025
This was referenced Sep 18, 2025
Merged
akwasniewski
added a commit
that referenced
this pull request
Sep 19, 2025
## Description After #3693 old api gesture relations did not work as they were never configured. This PR fixes this issue ## Test plan Check if relations examples from [v2 docs](https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition) work
akwasniewski
added a commit
that referenced
this pull request
Sep 19, 2025
## Description This PR adds a function in web module to configure gesture relations via hooks from #3693. ## Test plan ```ts import * as React from 'react'; import { Animated, Button, useAnimatedValue } from 'react-native'; import { GestureHandlerRootView, NativeDetector, useSimultaneous, useGesture, useExclusive, useRace, SingleGestureName, } 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( [{ nativeEvent: { handlerData: { translationX: av } } }], { useNativeDriver: true, } ); const tap1 = useGesture(SingleGestureName.Tap, { onEnd: () => { // 'worklet'; console.log('Tap 1'); }, numberOfTaps: 1, disableReanimated: true, }); const tap2 = useGesture(SingleGestureName.Tap, { 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(SingleGestureName.Pan, { // onUpdate: event, onUpdate: (e) => { 'worklet'; console.log('Pan 1'); }, // disableReanimated: true, }); const pan2 = useGesture(SingleGestureName.Pan, { 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: av }] }, ]}> <NativeDetector gesture={pan2}> <Animated.View style={{ width: 100, height: 100, backgroundColor: 'green' }} /> </NativeDetector> </Animated.View> </NativeDetector> )} </GestureHandlerRootView> ); } ```
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.
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)useRace(g1, g2)Gesture.Exclusive(g1, g2)useExclusive(g1, g2)Gesture.Simultaneous(g1, g2)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 handlingExclusiveandrequireExternalGestureToFailrelationssimultaneousHandlers- responsible forSimultaneousandsimultaneousWithExternalGesturerelationsblocksHandlers- responsible forblocksExternalGesturerelationAt first, these arrays are filled with external relations in
useGesturehook. Then we useDFSalgorithm to add remaining relations, added with relation hooks. SinceRacedoesn't really change anything when it comes to gesture interactions, we can ignore it in our algorithm.DFS overview
We use
DFSbecause gesture relations form tree structure.The algorithm works as follows:
waitForandsimultaneousHandlers. If root node isSimultaneousGesture, we also add its handler tags intosimultaneousHandlersarray. This ensures that the algorithm works even if we have onlySimultaneousas the root node (e.g.useSimultaneous(g1, g1))ComposedGesture, it means that we reached leaf node. In that case we populatewaitForandsimultanoursHandlersarrays intonodearrays and then update relations on the native sideComposedGesture, then for each child:ComposedGesture:traverseGestureRelationsto reach stop condition and configure relations on the native sideExclusive, then we add childtagtowaitForarrayComposedGesture:non-simultaneousgesture tosimultaneousgesture we add all child tags into globalsimulatneousHandlersarraysimultaneoustonon-simultaneousgesture, we remove child tags instead of adding.waitForto reset it later.traverseGestureRelationssimultaneous(child) tonon-simultaneous(node) gesture, we remove node tags fromsimultaneousHandlersnon-simultaneous(child) tosimultaneous(node) gesture we add node tags instead of removingExclusivegesture means that we want to add all children tags intowaitForExclusivechild tonon-exclusivenode, we want to resetwaitForto previous state, usinglengthvariable.Example
Below you can see example of the algorithm.
We use the following notation:
E-ExclusiveS-SimultaneousP-PanT-TapSH-simultaneousHandlersWF-waitFor+=- adding tags-=- removing tagsNote: vertex label in relation arrays expands to all tags in the composed gesture.
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:0pxLimitations
Currently the following setup doesn't work on
android: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:
Android
SimultaneousExclusiveRaceExclusive+SimultaneousiOS
SimultaneousExclusiveRaceExclusive+SimultaneousBase code used for testing:
Cross detector interactions
Verified that the following relations work:
Android
simultaneousWithExternalGesturerequireExternalGestureToFailblocksExternalGestureiOS
simultaneousWithExternalGesturerequireExternalGestureToFailblocksExternalGestureBase code used for testing: