Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
68e48c6
Handle shared values on JS
m-bert Aug 5, 2025
bc7c28b
Update config on android
m-bert Aug 5, 2025
e3f51d2
Update config on iOS
m-bert Aug 5, 2025
2df4900
Rename
m-bert Aug 5, 2025
3929618
Change method in ForceTouch
m-bert Aug 6, 2025
ea1f170
Update mocks
m-bert Aug 6, 2025
7cf3962
Rename ID variable
m-bert Aug 6, 2025
dedb725
Remove unnecessary worklet directive
m-bert Aug 6, 2025
03fb873
Merge branch 'next' into @mbert/shared-values
m-bert Aug 7, 2025
f337486
Disable coalescing on Android
m-bert Aug 7, 2025
43c84e5
Basic simultaneous
m-bert Aug 7, 2025
c0433bf
Add Exclusvie composition
m-bert Aug 8, 2025
4a81c1c
useRace
m-bert Aug 8, 2025
18de5ca
Add gesture names
m-bert Aug 8, 2025
26f38b3
Merge branch 'next' into @mbert/shared-values
m-bert Aug 8, 2025
d906df9
Merge branch '@mbert/shared-values' into @mbert/gesture-relations
m-bert Aug 8, 2025
50ffbcd
Inline updateGestureHandler
m-bert Aug 11, 2025
8d1ad9b
Move extracting SharedValues into separate effect
m-bert Aug 11, 2025
9a98e19
Store shared values in array
m-bert Aug 11, 2025
fd6eb79
Rename functions
m-bert Aug 11, 2025
2af12c1
Do not store shared values
m-bert Aug 11, 2025
d8a2824
Merge branch 'next' into @mbert/shared-values
m-bert Aug 11, 2025
4c29715
Merge branch 'next' into @mbert/shared-values
m-bert Aug 11, 2025
ee6c581
Merge branch '@mbert/shared-values' into @mbert/gesture-relations
m-bert Aug 11, 2025
b973da8
Implement DFS for gesture relations
m-bert Aug 13, 2025
39d976d
Implement module method for updating relations
m-bert Aug 14, 2025
f622d69
Add blocksHandlers array
m-bert Aug 14, 2025
b83c17f
Handle Simultaneous as root
m-bert Aug 14, 2025
18b1269
Change name to enum
m-bert Aug 14, 2025
b7ca9bb
Move DFS to other file
m-bert Aug 14, 2025
fe7386a
Do not traverse single gestures
m-bert Aug 18, 2025
988fba3
Handle external relations
m-bert Aug 18, 2025
390faa1
Reanimated composedHandler
m-bert Aug 18, 2025
fd20c1b
Merge branch 'next' into @mbert/shared-values
m-bert Aug 19, 2025
e4f6aa4
Merge branch '@mbert/shared-values' into @mbert/gesture-relations
m-bert Aug 19, 2025
43c2d86
Merge branch 'next' into @mbert/shared-values
m-bert Aug 19, 2025
1e9270c
Merge branch '@mbert/shared-values' into @mbert/gesture-relations
m-bert Aug 19, 2025
1829491
Do not freeze config
m-bert Aug 19, 2025
73bd21b
Use operation block
m-bert Aug 20, 2025
ee63683
Copy config
m-bert Aug 20, 2025
ed5e9d8
Move flushOperations into JS
m-bert Aug 20, 2025
9c89722
Merge branch 'next' into @mbert/shared-values
m-bert Aug 20, 2025
de5b0f1
Merge branch '@mbert/shared-values' into @mbert/gesture-relations
m-bert Aug 20, 2025
36d0c25
Add exports for type
m-bert Aug 20, 2025
5e31c72
Merge branch 'next' into @mbert/gesture-relations
m-bert Aug 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 114 additions & 3 deletions packages/react-native-gesture-handler/src/v3/NativeDetector.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import React from 'react';
import { NativeGesture } from './types';
import { NativeGesture, ComposedGesture } from './types';
import { Reanimated } from '../handlers/gestures/reanimatedWrapper';

import { Animated, StyleSheet } from 'react-native';
import HostGestureDetector from './HostGestureDetector';
import { tagMessage } from '../utils';
import { isComposedGesture } from './hooks/utils';

export interface NativeDetectorProps {
children?: React.ReactNode;
gesture: NativeGesture;
gesture: NativeGesture | ComposedGesture;
}

const AnimatedNativeDetector =
Expand All @@ -34,20 +35,130 @@ export function NativeDetector({ gesture, children }: NativeDetectorProps) {
);
}

// This piece of magic traverses the gesture tree and populates `waitFor` and `simultaneousHandlers`
// arrays for each gesture. It traverses the tree recursively using DFS.
// `waitFor` and `simultaneousHandlers` are global data structures that will be populated into each gesture.
// For `waitFor` we need array as order of the gestures matters.
// For `simultaneousHandlers` we use Set as the order doesn't matter.
// The tree consists of ComposedGestures and NativeGestures. NativeGestures are always leaf nodes.
const dfs = (
node: NativeGesture | ComposedGesture,
waitFor: number[] = [],
simultaneousHandlers: Set<number> = new Set()
) => {
// If we are in the leaf node, we want to fill gesture relations arrays with current
// waitFor and simultaneousHandlers.
// TODO: handle `simultaneousWithExternalGesture`, `requreExternalGestureToFail`, `blocksExternalGesture`
if (!isComposedGesture(node)) {
node.simultaneousHandlers.push(...simultaneousHandlers);
node.waitFor.push(...waitFor);

return;
}

// If we are in the composed gesture, we want to traverse its children.
node.gestures.forEach((child) => {
// If child is composed gesture, we have to correctly fill `waitFor` and `simultaneousHandlers`.
if (isComposedGesture(child)) {
// We have to update `simultaneousHandlers` before traversing the child.

// If we go from a non-simultaneous gesture to a simultaneous gesture,
// we add the tags of the simultaneous gesture to the `simultaneousHandlers`.
// This way when we traverse the child, we already have the tags of the simultaneous gestures
if (
node.name !== 'SimultaneousGesture' &&
child.name === 'SimultaneousGesture'
) {
child.tags.forEach((tag) => simultaneousHandlers.add(tag));
}

// If we go from a simultaneous gesture to a non-simultaneous gesture,
// we remove the tags of the child gestures from the `simultaneousHandlers`,
// as those are not simultaneous with each other.
if (
node.name === 'SimultaneousGesture' &&
child.name !== 'SimultaneousGesture'
) {
child.tags.forEach((tag) => simultaneousHandlers.delete(tag));
}

// We will keep the current length of `waitFor` to reset it to previous state
// after traversing the child.
const length = waitFor.length;

// We traverse the child, passing the current `waitFor` and `simultaneousHandlers`.
dfs(child, waitFor, simultaneousHandlers);

// After traversing the child, we need to update `waitFor` and `simultaneousHandlers`

// If we go back from a simultaneous gesture to a non-simultaneous gesture,
// we want to delete the tags of the simultaneous gesture from the `simultaneousHandlers` -
// those gestures are not simultaneous with each other anymore.
if (
child.name === 'SimultaneousGesture' &&
node.name !== 'SimultaneousGesture'
) {
node.tags.forEach((tag) => simultaneousHandlers.delete(tag));
}

// If we go back from a non-simultaneous gesture to a simultaneous gesture,
// we want to add the tags of the simultaneous gesture to the `simultaneousHandlers`,
// as those gestures are simultaneous with other children of the current node.
if (
child.name !== 'SimultaneousGesture' &&
node.name === 'SimultaneousGesture'
) {
node.tags.forEach((tag) => simultaneousHandlers.add(tag));
}

// If we go back to an exclusive gesture, we want to add the tags of the child gesture to the `waitFor` array.
// This will allow us to pass exclusive gesture tags to the right subtree of the current node.
if (node.name === 'ExclusiveGesture') {
child.tags.forEach((tag) => waitFor.push(tag));
}

// If we go back from an exclusive gesture to a non-exclusive gesture, we want to reset the `waitFor` array
// to the previous state, siblings of the exclusive gesture are not exclusive with it. Since we use `push` method to
// add tags to the `waitFor` array, we can override `length` property to reset it to the previous state.
if (
child.name === 'ExclusiveGesture' &&
node.name !== 'ExclusiveGesture'
) {
waitFor.length = length;
}
}
// This means that child is a leaf node.
else {
// In the leaf node, we only care about filling `waitFor` array. First we traverse the child...
dfs(child, waitFor, simultaneousHandlers);

// ..and when we go back we add the tag of the child to the `waitFor` array.
if (node.name === 'ExclusiveGesture') {
waitFor.push(child.tag);
}
}
});
};

dfs(gesture);

return (
<NativeDetectorComponent
// @ts-ignore TODO: Fix types
onGestureHandlerStateChange={
gesture.gestureEvents.onGestureHandlerStateChange
}
// @ts-ignore TODO: Fix types
onGestureHandlerEvent={gesture.gestureEvents.onGestureHandlerEvent}
onGestureHandlerAnimatedEvent={
gesture.gestureEvents.onGestureHandlerAnimatedEvent
}
// @ts-ignore TODO: Fix types
onGestureHandlerTouchEvent={
gesture.gestureEvents.onGestureHandlerTouchEvent
}
moduleId={globalThis._RNGH_MODULE_ID}
handlerTags={gesture.tag}
handlerTags={isComposedGesture(gesture) ? gesture.tags : [gesture.tag]}
style={styles.detector}>
{children}
</NativeDetectorComponent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@ import {
StateChangeEvent,
UpdateEvent,
TouchEvent,
ComposedGesture,
} from '../../types';
import { isComposedGesture } from '../utils';
import { tagMessage } from '../../../utils';

export function useComposedGesture(...gestures: NativeGesture[]) {
const tags = gestures.flatMap((gesture) => gesture.tag);
// TODO: Simplify repeated relations (Simultaneous with Simultaneous, Exclusive with Exclusive, etc.)
// eslint-disable-next-line @eslint-react/hooks-extra/ensure-custom-hooks-using-other-hooks, @eslint-react/hooks-extra/no-unnecessary-use-prefix
export function useComposedGesture(
...gestures: (NativeGesture | ComposedGesture)[]
): ComposedGesture {
const tags = gestures.flatMap((gesture) =>
isComposedGesture(gesture) ? gesture.tags : gesture.tag
);

const config = {
shouldUseReanimated: gestures.some(
Expand All @@ -17,6 +26,14 @@ export function useComposedGesture(...gestures: NativeGesture[]) {
),
};

if (config.shouldUseReanimated && config.dispatchesAnimatedEvents) {
throw new Error(
tagMessage(
'Composed gestures cannot use both Reanimated and Animated events at the same time.'
)
);
}

const onGestureHandlerStateChange = (
event: StateChangeEvent<Record<string, unknown>>
) => {
Expand Down Expand Up @@ -57,7 +74,7 @@ export function useComposedGesture(...gestures: NativeGesture[]) {
}

return {
tag: tags,
tags,
name: 'ComposedGesture',
config,
gestureEvents: {
Expand All @@ -66,5 +83,6 @@ export function useComposedGesture(...gestures: NativeGesture[]) {
onGestureHandlerAnimatedEvent,
onGestureHandlerTouchEvent,
},
gestures,
};
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { NativeGesture } from '../../types';
import { NativeGesture, ComposedGesture } from '../../types';
import { useComposedGesture } from './useComposedGesture';

export function useExclusive(...gestures: NativeGesture[]) {
export function useExclusive(...gestures: (NativeGesture | ComposedGesture)[]) {
const composedGesture = useComposedGesture(...gestures);

const tags = gestures.flatMap((gesture) => gesture.tag);

for (let i = 0; i < gestures.length; i++) {
gestures[i].config.waitFor = tags.slice(0, i);
}

composedGesture.name = 'ExclusiveGesture';

return composedGesture;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NativeGesture } from '../../types';
import { ComposedGesture, NativeGesture } from '../../types';
import { useComposedGesture } from './useComposedGesture';

export function useRace(...gestures: NativeGesture[]) {
export function useRace(...gestures: (NativeGesture | ComposedGesture)[]) {
const composedGesture = useComposedGesture(...gestures);

composedGesture.name = 'RaceGesture';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import { NativeGesture } from '../../types';
import { ComposedGesture, NativeGesture } from '../../types';
import { useComposedGesture } from './useComposedGesture';

export function useSimultaneous(...gestures: NativeGesture[]) {
export function useSimultaneous(
...gestures: (NativeGesture | ComposedGesture)[]
) {
const composedGesture = useComposedGesture(...gestures);

const tags = gestures.flatMap((gesture) => gesture.tag);

for (const gesture of gestures) {
const simultaneousHandlersTags = [
...tags.filter((tag) => !gesture.tag.includes(tag)),
];

gesture.config.simultaneousHandlers = simultaneousHandlersTags;
}

composedGesture.name = 'SimultaneousGesture';

return composedGesture;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export function useGesture(
}, [config, tag]);

return {
tag: [tag],
tag,
name: type,
config,
gestureEvents: {
Expand All @@ -155,5 +155,7 @@ export function useGesture(
onGestureHandlerTouchEvent,
onGestureHandlerAnimatedEvent,
},
simultaneousHandlers: [],
waitFor: [],
};
}
8 changes: 8 additions & 0 deletions packages/react-native-gesture-handler/src/v3/hooks/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
GestureHandlerEvent,
GestureStateChangeEventWithData,
GestureUpdateEventWithData,
NativeGesture,
ComposedGesture,
} from '../types';
import { GestureTouchEvent } from '../../handlers/gestureHandlerCommon';
import { tagMessage } from '../../utils';
Expand Down Expand Up @@ -109,3 +111,9 @@ export function checkMappingForChangeProperties(obj: Animated.Mapping) {
}
}
}

export function isComposedGesture(
gesture: NativeGesture | ComposedGesture
): gesture is ComposedGesture {
return 'tags' in gesture;
}
40 changes: 34 additions & 6 deletions packages/react-native-gesture-handler/src/v3/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,44 @@ export type GestureType =
| 'ManualGestureHandler'
| 'NativeViewGestureHandler';

export type ComposedGestureType =
| 'SimultaneousGesture'
| 'ExclusiveGesture'
| 'RaceGesture'
| 'ComposedGesture';

export type GestureEvents = {
onGestureHandlerStateChange: (event: any) => void;
onGestureHandlerEvent: undefined | ((event: any) => void);
onGestureHandlerTouchEvent: (event: any) => void;
onGestureHandlerStateChange: (
event: StateChangeEvent<Record<string, unknown>>
) => void;
onGestureHandlerEvent:
| undefined
| ((event: UpdateEvent<Record<string, unknown>>) => void);
onGestureHandlerTouchEvent: (event: TouchEvent) => void;
onGestureHandlerAnimatedEvent: undefined | AnimatedEvent;
};

export interface NativeGesture {
tag: number[];
export type GestureRelations = {
simultaneousGestures: number[];
exclusiveGestures: number[];
};

export type NativeGesture = {
tag: number;
name: GestureType;
config: Record<string, unknown>;
gestureEvents: GestureEvents;
}
simultaneousHandlers: number[];
waitFor: number[];
};

export type ComposedGesture = {
tags: number[];
name: ComposedGestureType;
config: {
shouldUseReanimated: boolean;
dispatchesAnimatedEvents: boolean;
};
gestureEvents: GestureEvents;
gestures: (NativeGesture | ComposedGesture)[];
};
Loading