diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerEvent.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerEvent.kt index 153105fbff..d656a31f01 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerEvent.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerEvent.kt @@ -61,7 +61,8 @@ class RNGestureHandlerEvent private constructor() : Event EVENT_NAME } - override fun canCoalesce() = true + // Unfortunately getCoalescingKey is not considered when sending event to C++, therefore we have to disable coalescing in v3 + override fun canCoalesce() = actionType != GestureHandler.ACTION_TYPE_NATIVE_DETECTOR override fun getCoalescingKey() = coalescingKey diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerModule.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerModule.kt index 3e47d7f5ce..4e3711722b 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerModule.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerModule.kt @@ -96,8 +96,6 @@ class RNGestureHandlerModule(reactContext: ReactApplicationContext?) : val handler = registry.getHandler(handlerTag) ?: return val factory = RNGestureHandlerFactoryUtil.findFactoryForHandler(handler) ?: return - interactionManager.dropRelationsForHandlerWithTag(handlerTag) - interactionManager.configureInteractions(handler, config) factory.setConfig(handler, config) } @@ -110,6 +108,15 @@ class RNGestureHandlerModule(reactContext: ReactApplicationContext?) : factory.updateConfig(handler, config) } + @ReactMethod + override fun configureRelations(handlerTagDouble: Double, relations: ReadableMap) { + val handlerTag = handlerTagDouble.toInt() + val handler = registry.getHandler(handlerTag) ?: return + + interactionManager.dropRelationsForHandlerWithTag(handlerTag) + interactionManager.configureInteractions(handler, relations) + } + @ReactMethod override fun dropGestureHandler(handlerTagDouble: Double) { val handlerTag = handlerTagDouble.toInt() diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandler.h b/packages/react-native-gesture-handler/apple/RNGestureHandler.h index 4de4ca6a51..7ae62fb687 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandler.h +++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.h @@ -86,6 +86,7 @@ - (void)resetConfig NS_REQUIRES_SUPER; - (void)setConfig:(nullable NSDictionary *)config NS_REQUIRES_SUPER; - (void)updateConfig:(nullable NSDictionary *)config NS_REQUIRES_SUPER; +- (void)updateRelations:(nonnull NSDictionary *)relations; - (void)handleGesture:(nonnull id)recognizer; - (void)handleGesture:(nonnull id)recognizer inState:(RNGestureHandlerState)state; - (BOOL)containsPointInView; diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm index c1c94bb7da..f7d7dbf9d2 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm @@ -121,10 +121,6 @@ - (void)setConfig:(NSDictionary *)config - (void)updateConfig:(NSDictionary *)config { - _handlersToWaitFor = [RCTConvert NSNumberArray:config[@"waitFor"]]; - _simultaneousHandlers = [RCTConvert NSNumberArray:config[@"simultaneousHandlers"]]; - _handlersThatShouldWait = [RCTConvert NSNumberArray:config[@"blocksHandlers"]]; - id prop = config[@"enabled"]; if (prop != nil) { self.enabled = [RCTConvert BOOL:prop]; @@ -184,6 +180,13 @@ - (void)updateConfig:(NSDictionary *)config } } +- (void)updateRelations:(NSDictionary *)relations +{ + _handlersToWaitFor = [RCTConvert NSNumberArray:relations[@"waitFor"]]; + _simultaneousHandlers = [RCTConvert NSNumberArray:relations[@"simultaneousHandlers"]]; + _handlersThatShouldWait = [RCTConvert NSNumberArray:relations[@"blocksHandlers"]]; +} + - (void)setEnabled:(BOOL)enabled { _enabled = enabled; diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerManager.h b/packages/react-native-gesture-handler/apple/RNGestureHandlerManager.h index fe4a950865..123d85cd54 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerManager.h +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerManager.h @@ -28,6 +28,8 @@ - (void)updateGestureHandlerConfig:(nonnull NSNumber *)handlerTag config:(nonnull NSDictionary *)config; +- (void)updateGestureHandlerRelations:(nonnull NSNumber *)handlerTag relations:(nonnull NSDictionary *)relations; + - (void)dropGestureHandler:(nonnull NSNumber *)handlerTag; - (void)dropAllGestureHandlers; diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerManager.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerManager.mm index a0f79967ee..bd0e2e0cae 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerManager.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerManager.mm @@ -190,6 +190,12 @@ - (void)updateGestureHandlerConfig:(NSNumber *)handlerTag config:(NSDictionary * [handler updateConfig:config]; } +- (void)updateGestureHandlerRelations:(NSNumber *)handlerTag relations:(NSDictionary *)relations +{ + RNGestureHandler *handler = [_registry handlerWithTag:handlerTag]; + [handler updateRelations:relations]; +} + - (void)dropGestureHandler:(NSNumber *)handlerTag { [_registry dropHandlerWithTag:handlerTag]; diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm index fab69544b7..c8052bc822 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm @@ -159,6 +159,12 @@ - (void)updateGestureHandlerConfig:(double)handlerTag newConfig:(NSDictionary *) }]; } +- (void)configureRelations:(double)handlerTag relations:(NSDictionary *)relations +{ + RNGestureHandlerManager *manager = [RNGestureHandlerModule handlerManagerForModuleId:_moduleId]; + [manager updateGestureHandlerRelations:[NSNumber numberWithDouble:handlerTag] relations:relations]; +} + - (void)dropGestureHandler:(double)handlerTag { [self addOperationBlock:^(RNGestureHandlerManager *manager) { diff --git a/packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper.ts b/packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper.ts index dfce442813..54a70573f5 100644 --- a/packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper.ts +++ b/packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper.ts @@ -36,6 +36,9 @@ let Reanimated: events: string[], rebuild: boolean ) => (event: unknown) => void; + useComposedEventHandler( + handlers: (((event: T) => void) | null)[] + ): (event: T) => void; useSharedValue: (value: T) => SharedValue; setGestureState: (handlerTag: number, newState: number) => void; isSharedValue: (value: unknown) => value is SharedValue; diff --git a/packages/react-native-gesture-handler/src/index.ts b/packages/react-native-gesture-handler/src/index.ts index 1f6ad3b622..3b54e09b2c 100644 --- a/packages/react-native-gesture-handler/src/index.ts +++ b/packages/react-native-gesture-handler/src/index.ts @@ -162,9 +162,14 @@ export type { } from './components/DrawerLayout'; export { default as DrawerLayout } from './components/DrawerLayout'; -export type { NativeDetectorProps } from './v3/NativeDetector'; -export { NativeDetector } from './v3/NativeDetector'; +export type { NativeDetectorProps } from './v3/NativeDetector/NativeDetector'; +export { NativeDetector } from './v3/NativeDetector/NativeDetector'; export * from './v3/hooks/useGesture'; +export * from './v3/hooks/relations/useSimultaneous'; +export * from './v3/hooks/relations/useExclusive'; +export * from './v3/hooks/relations/useRace'; + +export { HandlerType } from './v3/types'; initialize(); diff --git a/packages/react-native-gesture-handler/src/specs/NativeRNGestureHandlerModule.ts b/packages/react-native-gesture-handler/src/specs/NativeRNGestureHandlerModule.ts index f3d3ae181a..25cf9733dd 100644 --- a/packages/react-native-gesture-handler/src/specs/NativeRNGestureHandlerModule.ts +++ b/packages/react-native-gesture-handler/src/specs/NativeRNGestureHandlerModule.ts @@ -22,6 +22,8 @@ export interface Spec extends TurboModule { setGestureHandlerConfig: (handlerTag: Double, newConfig: Object) => void; // eslint-disable-next-line @typescript-eslint/ban-types updateGestureHandlerConfig: (handlerTag: Double, newConfig: Object) => void; + // eslint-disable-next-line @typescript-eslint/ban-types + configureRelations: (handlerTag: Double, relations: Object) => void; dropGestureHandler: (handlerTag: Double) => void; flushOperations: () => void; } diff --git a/packages/react-native-gesture-handler/src/v3/HostGestureDetector.tsx b/packages/react-native-gesture-handler/src/v3/HostGestureDetector.tsx deleted file mode 100644 index 1bc9860d9a..0000000000 --- a/packages/react-native-gesture-handler/src/v3/HostGestureDetector.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import RNGestureHandlerDetectorNativeComponent from '../specs/RNGestureHandlerDetectorNativeComponent'; -const HostGestureDetector = RNGestureHandlerDetectorNativeComponent; -export default HostGestureDetector; diff --git a/packages/react-native-gesture-handler/src/v3/NativeDetector/HostGestureDetector.tsx b/packages/react-native-gesture-handler/src/v3/NativeDetector/HostGestureDetector.tsx new file mode 100644 index 0000000000..5b91d456e2 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/NativeDetector/HostGestureDetector.tsx @@ -0,0 +1,3 @@ +import RNGestureHandlerDetectorNativeComponent from '../../specs/RNGestureHandlerDetectorNativeComponent'; +const HostGestureDetector = RNGestureHandlerDetectorNativeComponent; +export default HostGestureDetector; diff --git a/packages/react-native-gesture-handler/src/v3/HostGestureDetector.web.tsx b/packages/react-native-gesture-handler/src/v3/NativeDetector/HostGestureDetector.web.tsx similarity index 92% rename from packages/react-native-gesture-handler/src/v3/HostGestureDetector.web.tsx rename to packages/react-native-gesture-handler/src/v3/NativeDetector/HostGestureDetector.web.tsx index cbe2e77441..46f0064574 100644 --- a/packages/react-native-gesture-handler/src/v3/HostGestureDetector.web.tsx +++ b/packages/react-native-gesture-handler/src/v3/NativeDetector/HostGestureDetector.web.tsx @@ -1,9 +1,9 @@ import React, { Ref, useEffect, useRef } from 'react'; -import RNGestureHandlerModule from '../RNGestureHandlerModule.web'; -import { ActionType } from '../ActionType'; -import { PropsRef } from '../web/interfaces'; +import RNGestureHandlerModule from '../../RNGestureHandlerModule.web'; +import { ActionType } from '../../ActionType'; +import { PropsRef } from '../../web/interfaces'; import { View } from 'react-native'; -import { tagMessage } from '../utils'; +import { tagMessage } from '../../utils'; export interface GestureHandlerDetectorProps extends PropsRef { handlerTags: number[]; diff --git a/packages/react-native-gesture-handler/src/v3/NativeDetector.tsx b/packages/react-native-gesture-handler/src/v3/NativeDetector/NativeDetector.tsx similarity index 62% rename from packages/react-native-gesture-handler/src/v3/NativeDetector.tsx rename to packages/react-native-gesture-handler/src/v3/NativeDetector/NativeDetector.tsx index 22d347d1f5..e76f7a488f 100644 --- a/packages/react-native-gesture-handler/src/v3/NativeDetector.tsx +++ b/packages/react-native-gesture-handler/src/v3/NativeDetector/NativeDetector.tsx @@ -1,14 +1,17 @@ import React from 'react'; -import { NativeGesture } from './hooks/useGesture'; -import { Reanimated } from '../handlers/gestures/reanimatedWrapper'; +import { NativeGesture, ComposedGesture, ComposedGestureType } from '../types'; +import { Reanimated } from '../../handlers/gestures/reanimatedWrapper'; import { Animated, StyleSheet } from 'react-native'; import HostGestureDetector from './HostGestureDetector'; -import { tagMessage } from '../utils'; +import { tagMessage } from '../../utils'; +import { isComposedGesture } from '../hooks/utils'; +import { dfs } from './utils'; +import RNGestureHandlerModule from '../../RNGestureHandlerModule'; export interface NativeDetectorProps { children?: React.ReactNode; - gesture: NativeGesture; + gesture: NativeGesture | ComposedGesture; } const AnimatedNativeDetector = @@ -34,20 +37,37 @@ export function NativeDetector({ gesture, children }: NativeDetectorProps) { ); } + if (isComposedGesture(gesture)) { + if (gesture.type === ComposedGestureType.Simultaneous) { + dfs(gesture, new Set(gesture.tags)); + } else { + dfs(gesture); + } + } else { + RNGestureHandlerModule.configureRelations(gesture.tag, { + waitFor: gesture.gestureRelations.waitFor, + simultaneousHandlers: gesture.gestureRelations.simultaneousHandlers, + blocksHandlers: gesture.gestureRelations.blocksHandlers, + }); + } + return ( {children} diff --git a/packages/react-native-gesture-handler/src/v3/NativeDetector/utils.ts b/packages/react-native-gesture-handler/src/v3/NativeDetector/utils.ts new file mode 100644 index 0000000000..251f24b9e3 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/NativeDetector/utils.ts @@ -0,0 +1,115 @@ +// 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. + +import RNGestureHandlerModule from '../../RNGestureHandlerModule'; +import { isComposedGesture } from '../hooks/utils'; +import { ComposedGesture, ComposedGestureType, NativeGesture } from '../types'; + +// The tree consists of ComposedGestures and NativeGestures. NativeGestures are always leaf nodes. +export const dfs = ( + node: NativeGesture | ComposedGesture, + simultaneousHandlers: Set = new Set(), + waitFor: number[] = [] +) => { + // If we are in the leaf node, we want to fill gesture relations arrays with current + // waitFor and simultaneousHandlers. We also want to configure relations on the native side. + // TODO: handle `simultaneousWithExternalGesture`, `requreExternalGestureToFail`, `blocksExternalGesture` + if (!isComposedGesture(node)) { + node.gestureRelations.simultaneousHandlers.push(...simultaneousHandlers); + node.gestureRelations.waitFor.push(...waitFor); + + RNGestureHandlerModule.configureRelations(node.tag, { + waitFor: node.gestureRelations.waitFor, + simultaneousHandlers: node.gestureRelations.simultaneousHandlers, + blocksHandlers: node.gestureRelations.blocksHandlers, + }); + + 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.type !== ComposedGestureType.Simultaneous && + child.type === ComposedGestureType.Simultaneous + ) { + 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.type === ComposedGestureType.Simultaneous && + child.type !== ComposedGestureType.Simultaneous + ) { + 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, simultaneousHandlers, waitFor); + + // 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.type === ComposedGestureType.Simultaneous && + node.type !== ComposedGestureType.Simultaneous + ) { + 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.type !== ComposedGestureType.Simultaneous && + node.type === ComposedGestureType.Simultaneous + ) { + 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.type === ComposedGestureType.Exclusive) { + 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.type === ComposedGestureType.Exclusive && + node.type !== ComposedGestureType.Exclusive + ) { + 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, simultaneousHandlers, waitFor); + + // ..and when we go back we add the tag of the child to the `waitFor` array. + if (node.type === ComposedGestureType.Exclusive) { + waitFor.push(child.tag); + } + } + }); +}; diff --git a/packages/react-native-gesture-handler/src/v3/hooks/events/useGestureHandlerEvent.ts b/packages/react-native-gesture-handler/src/v3/hooks/events/useGestureHandlerEvent.ts index e75e056ee4..c86291cf19 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/events/useGestureHandlerEvent.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/events/useGestureHandlerEvent.ts @@ -12,7 +12,7 @@ import { CallbackHandlers, UpdateEvent } from '../../types'; import { tagMessage } from '../../../utils'; export function useGestureHandlerEvent(handlerTag: number, config: any) { - const { onUpdate } = config; + const { onUpdate, changeEventCalculator } = config; const handlers: CallbackHandlers = { ...(onUpdate && { onUpdate }) }; @@ -35,8 +35,8 @@ export function useGestureHandlerEvent(handlerTag: number, config: any) { runWorkletCallback( CALLBACK_TYPE.UPDATE, handlers, - config.changeEventCalculator - ? config.changeEventCalculator(event, context.lastUpdateEvent) + changeEventCalculator + ? changeEventCalculator(event, context.lastUpdateEvent) : event ); diff --git a/packages/react-native-gesture-handler/src/v3/hooks/relations/useComposedGesture.ts b/packages/react-native-gesture-handler/src/v3/hooks/relations/useComposedGesture.ts new file mode 100644 index 0000000000..243a72b156 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/hooks/relations/useComposedGesture.ts @@ -0,0 +1,89 @@ +import { + NativeGesture, + StateChangeEvent, + UpdateEvent, + TouchEvent, + ComposedGesture, + ComposedGestureType, +} from '../../types'; +import { isComposedGesture } from '../utils'; +import { tagMessage } from '../../../utils'; + +// 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( + (gesture) => gesture.config.shouldUseReanimated + ), + dispatchesAnimatedEvents: gestures.some( + (gesture) => gesture.config.dispatchesAnimatedEvents + ), + }; + + 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> + ) => { + for (const gesture of gestures) { + if (gesture.gestureEvents.onGestureHandlerStateChange) { + gesture.gestureEvents.onGestureHandlerStateChange(event); + } + } + }; + + const onGestureHandlerEvent = ( + event: UpdateEvent> + ) => { + for (const gesture of gestures) { + if (gesture.gestureEvents.onGestureHandlerEvent) { + gesture.gestureEvents.onGestureHandlerEvent(event); + } + } + }; + + const onGestureHandlerTouchEvent = (event: TouchEvent) => { + for (const gesture of gestures) { + if (gesture.gestureEvents.onGestureHandlerTouchEvent) { + gesture.gestureEvents.onGestureHandlerTouchEvent(event); + } + } + }; + + let onGestureHandlerAnimatedEvent; + + for (const gesture of gestures) { + if (gesture.gestureEvents.onGestureHandlerAnimatedEvent) { + onGestureHandlerAnimatedEvent = + gesture.gestureEvents.onGestureHandlerAnimatedEvent; + + break; + } + } + + return { + tags, + type: ComposedGestureType.Race, + config, + gestureEvents: { + onGestureHandlerStateChange, + onGestureHandlerEvent, + onGestureHandlerAnimatedEvent, + onGestureHandlerTouchEvent, + }, + gestures, + }; +} diff --git a/packages/react-native-gesture-handler/src/v3/hooks/relations/useExclusive.ts b/packages/react-native-gesture-handler/src/v3/hooks/relations/useExclusive.ts new file mode 100644 index 0000000000..b402c8080a --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/hooks/relations/useExclusive.ts @@ -0,0 +1,14 @@ +import { + NativeGesture, + ComposedGesture, + ComposedGestureType, +} from '../../types'; +import { useComposedGesture } from './useComposedGesture'; + +export function useExclusive(...gestures: (NativeGesture | ComposedGesture)[]) { + const composedGesture = useComposedGesture(...gestures); + + composedGesture.type = ComposedGestureType.Exclusive; + + return composedGesture; +} diff --git a/packages/react-native-gesture-handler/src/v3/hooks/relations/useRace.ts b/packages/react-native-gesture-handler/src/v3/hooks/relations/useRace.ts new file mode 100644 index 0000000000..a04e21b38d --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/hooks/relations/useRace.ts @@ -0,0 +1,6 @@ +import { ComposedGesture, NativeGesture } from '../../types'; +import { useComposedGesture } from './useComposedGesture'; + +export function useRace(...gestures: (NativeGesture | ComposedGesture)[]) { + return useComposedGesture(...gestures); +} diff --git a/packages/react-native-gesture-handler/src/v3/hooks/relations/useSimultaneous.ts b/packages/react-native-gesture-handler/src/v3/hooks/relations/useSimultaneous.ts new file mode 100644 index 0000000000..24da0154ba --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/hooks/relations/useSimultaneous.ts @@ -0,0 +1,47 @@ +import { + ComposedGesture, + ComposedGestureType, + NativeGesture, +} from '../../types'; +import { useComposedGesture } from './useComposedGesture'; +import { Reanimated } from '../../../handlers/gestures/reanimatedWrapper'; + +export function useSimultaneous( + ...gestures: (NativeGesture | ComposedGesture)[] +) { + const composedGesture = useComposedGesture(...gestures); + + const reanimatedComposedStateChangeEvent = + Reanimated?.useComposedEventHandler( + gestures.map( + (gesture) => gesture.gestureEvents.onGestureHandlerStateChange || null + ) + ); + + const reanimatedComposedUpdateEvent = Reanimated?.useComposedEventHandler( + gestures.map( + (gesture) => gesture.gestureEvents.onGestureHandlerEvent || null + ) + ); + + const reanimatedComposedTouchEvent = Reanimated?.useComposedEventHandler( + gestures.map( + (gesture) => gesture.gestureEvents.onGestureHandlerTouchEvent || null + ) + ); + + // If `shouldUseReanimated` is true, then Reanimated has to be installed, + // therefore those non-null assertions are safe. + if (composedGesture.config.shouldUseReanimated) { + composedGesture.gestureEvents.onGestureHandlerStateChange = + reanimatedComposedStateChangeEvent!; + composedGesture.gestureEvents.onGestureHandlerEvent = + reanimatedComposedUpdateEvent!; + composedGesture.gestureEvents.onGestureHandlerTouchEvent = + reanimatedComposedTouchEvent!; + } + + composedGesture.type = ComposedGestureType.Simultaneous; + + return composedGesture; +} diff --git a/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts b/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts index 35ec1fbe42..4ae32e7e87 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts @@ -7,33 +7,14 @@ import { SharedValue, } from '../../handlers/gestures/reanimatedWrapper'; import { tagMessage } from '../../utils'; -import { AnimatedEvent } from '../types'; -import { hash, prepareConfig } from './utils'; - -type GestureType = - | 'TapGestureHandler' - | 'LongPressGestureHandler' - | 'PanGestureHandler' - | 'PinchGestureHandler' - | 'RotationGestureHandler' - | 'FlingGestureHandler' - | 'ForceTouchGestureHandler' - | 'ManualGestureHandler' - | 'NativeViewGestureHandler'; - -type GestureEvents = { - onGestureHandlerStateChange: (event: any) => void; - onGestureHandlerEvent: undefined | ((event: any) => void); - onGestureHandlerTouchEvent: (event: any) => void; - onGestureHandlerAnimatedEvent: undefined | AnimatedEvent; -}; - -export interface NativeGesture { - tag: number; - name: GestureType; - config: Record; - gestureEvents: GestureEvents; -} +import { + ComposedGesture, + Gesture, + GestureRelations, + NativeGesture, + SingleGestureType, +} from '../types'; +import { hash, isComposedGesture, prepareConfig } from './utils'; function hasWorkletEventHandlers(config: Record) { return Object.values(config).some( @@ -103,8 +84,39 @@ function unbindSharedValues(config: any, handlerTag: number) { } } +function prepareRelations(config: any): GestureRelations { + const extractHandlerTags = (otherHandler: Gesture | Gesture[]): number[] => { + if (!otherHandler) { + return []; + } + + let otherTags: number[]; + + if (Array.isArray(otherHandler)) { + otherTags = otherHandler.flatMap( + (gesture: NativeGesture | ComposedGesture) => + isComposedGesture(gesture) ? gesture.tags : gesture.tag + ); + } else { + otherTags = isComposedGesture(otherHandler) + ? otherHandler.tags + : [otherHandler.tag]; + } + + return otherTags; + }; + + return { + simultaneousHandlers: extractHandlerTags( + config.simultaneousWithExternalGesture + ), + waitFor: extractHandlerTags(config.requireExternalGestureToFail), + blocksHandlers: extractHandlerTags(config.blocksExternalGesture), + }; +} + export function useGesture( - type: GestureType, + type: SingleGestureType, config: Record ): NativeGesture { const tag = useMemo(() => getNextHandlerTag(), []); @@ -145,6 +157,8 @@ export function useGesture( !!onGestureHandlerAnimatedEvent && '__isNative' in onGestureHandlerAnimatedEvent; + const gestureRelations = prepareRelations(config); + useMemo(() => { RNGestureHandlerModule.createGestureHandler(type, tag, {}); RNGestureHandlerModule.flushOperations(); @@ -174,8 +188,8 @@ export function useGesture( }, [config, tag]); return { - tag: tag, - name: type, + tag, + type, config, gestureEvents: { onGestureHandlerStateChange, @@ -183,5 +197,6 @@ export function useGesture( onGestureHandlerTouchEvent, onGestureHandlerAnimatedEvent, }, + gestureRelations, }; } diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils.ts index 3859041cbc..1230330ff6 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/utils.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils.ts @@ -7,6 +7,8 @@ import { GestureHandlerEvent, GestureStateChangeEventWithData, GestureUpdateEventWithData, + NativeGesture, + ComposedGesture, } from '../types'; import { GestureTouchEvent } from '../../handlers/gestureHandlerCommon'; import { tagMessage } from '../../utils'; @@ -111,6 +113,12 @@ export function checkMappingForChangeProperties(obj: Animated.Mapping) { } } +export function isComposedGesture( + gesture: NativeGesture | ComposedGesture +): gesture is ComposedGesture { + return 'tags' in gesture; +} + export function prepareConfig(config: any) { const copy = { ...config }; diff --git a/packages/react-native-gesture-handler/src/v3/types.ts b/packages/react-native-gesture-handler/src/v3/types.ts index 3f04f28a9f..0c59887174 100644 --- a/packages/react-native-gesture-handler/src/v3/types.ts +++ b/packages/react-native-gesture-handler/src/v3/types.ts @@ -5,6 +5,7 @@ import { HandlerStateChangeEventPayload, } from '../handlers/gestureHandlerCommon'; import { HandlerCallbacks } from '../handlers/gestures/gesture'; +import { ValueOf } from '../typeUtils'; export type GestureUpdateEventWithData = GestureEventPayload & { handlerData: T; @@ -48,3 +49,73 @@ export type CallbackHandlers = Omit< export type AnimatedEvent = ((...args: any[]) => void) & { _argMapping?: unknown; }; + +export const SingleGestureType = { + Tap: 'TapGestureHandler', + LongPress: 'LongPressGestureHandler', + Pan: 'PanGestureHandler', + Pinch: 'PinchGestureHandler', + Rotation: 'RotationGestureHandler', + Fling: 'FlingGestureHandler', + Manual: 'ManualGestureHandler', + Native: 'NativeGestureHandler', +} as const; + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type SingleGestureType = ValueOf; + +export const ComposedGestureType = { + Simultaneous: 'SimultaneousGesture', + Exclusive: 'ExclusiveGesture', + Race: 'RaceGesture', +} as const; + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type ComposedGestureType = ValueOf; + +// TODO: Find better name +export const HandlerType = { + ...SingleGestureType, + ...ComposedGestureType, +} as const; + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type HandlerType = ValueOf; + +export type GestureEvents = { + onGestureHandlerStateChange: ( + event: StateChangeEvent> + ) => void; + onGestureHandlerEvent: + | undefined + | ((event: UpdateEvent>) => void); + onGestureHandlerTouchEvent: (event: TouchEvent) => void; + onGestureHandlerAnimatedEvent: undefined | AnimatedEvent; +}; + +export type GestureRelations = { + simultaneousHandlers: number[]; + waitFor: number[]; + blocksHandlers: number[]; +}; + +export type NativeGesture = { + tag: number; + type: HandlerType; + config: Record; + gestureEvents: GestureEvents; + gestureRelations: GestureRelations; +}; + +export type ComposedGesture = { + tags: number[]; + type: ComposedGestureType; + config: { + shouldUseReanimated: boolean; + dispatchesAnimatedEvents: boolean; + }; + gestureEvents: GestureEvents; + gestures: (NativeGesture | ComposedGesture)[]; +}; + +export type Gesture = NativeGesture | ComposedGesture;