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 075c1abcd7..f064f85f90 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 @@ -97,8 +97,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) } @@ -111,6 +109,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/android/src/main/java/com/swmansion/gesturehandler/react/events/RNGestureHandlerEvent.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/events/RNGestureHandlerEvent.kt index 8f909ea2e2..2e16a0a1b8 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/events/RNGestureHandlerEvent.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/events/RNGestureHandlerEvent.kt @@ -59,7 +59,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/apple/RNGestureHandler.h b/packages/react-native-gesture-handler/apple/RNGestureHandler.h index 226130b6a0..3096ef95fa 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandler.h +++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.h @@ -89,6 +89,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 12f563ff0e..a2dd49fd93 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm @@ -103,9 +103,6 @@ - (void)resetConfig self.enabled = YES; self.manualActivation = NO; _shouldCancelWhenOutside = NO; - _handlersToWaitFor = nil; - _simultaneousHandlers = nil; - _handlersThatShouldWait = nil; _hitSlop = RNGHHitSlopEmpty; _needsPointerData = NO; _dispatchesAnimatedEvents = NO; @@ -123,10 +120,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]; @@ -191,6 +184,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 363d0bb3cc..046b7a54e5 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerManager.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerManager.mm @@ -191,6 +191,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 483e401ecd..9875a7f710 100644 --- a/packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper.ts +++ b/packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper.ts @@ -59,6 +59,9 @@ let Reanimated: >( value: unknown ): value is WorkletFunction; + useComposedEventHandler( + handlers: (((event: T) => void) | null)[] + ): (event: T) => void; runOnUI( fn: (...args: A) => R ): (...args: Parameters) => void; diff --git a/packages/react-native-gesture-handler/src/index.ts b/packages/react-native-gesture-handler/src/index.ts index 1f6ad3b622..870a12d980 100644 --- a/packages/react-native-gesture-handler/src/index.ts +++ b/packages/react-native-gesture-handler/src/index.ts @@ -162,9 +162,12 @@ 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'; + +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 61% rename from packages/react-native-gesture-handler/src/v3/NativeDetector.tsx rename to packages/react-native-gesture-handler/src/v3/NativeDetector/NativeDetector.tsx index 2a84d5696b..4532d31497 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 { traverseGestureRelations } from './utils'; +import RNGestureHandlerModule from '../../RNGestureHandlerModule'; +import { isComposedGesture } from '../hooks/utils/relationUtils'; export interface NativeDetectorProps { children?: React.ReactNode; - gesture: NativeGesture; + gesture: NativeGesture | ComposedGesture; } const AnimatedNativeDetector = @@ -34,21 +37,43 @@ export function NativeDetector({ gesture, children }: NativeDetectorProps) { ); } + if (isComposedGesture(gesture)) { + traverseGestureRelations( + gesture, + new Set( + // If root is simultaneous, we want to add its tags to the set + gesture.type === ComposedGestureType.Simultaneous ? gesture.tags : [] + ) + ); + } 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..46b851f4c4 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/NativeDetector/utils.ts @@ -0,0 +1,114 @@ +// 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/relationUtils'; +import { ComposedGesture, ComposedGestureType, NativeGesture } from '../types'; + +// The tree consists of ComposedGestures and NativeGestures. NativeGestures are always leaf nodes. +export const traverseGestureRelations = ( + node: NativeGesture | ComposedGesture, + simultaneousHandlers: 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. + 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`. + traverseGestureRelations(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... + traverseGestureRelations(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/callbacks/js/useGestureStateChangeEvent.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureStateChangeEvent.ts index 4bc8ae7a5c..699bb29c46 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureStateChangeEvent.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureStateChangeEvent.ts @@ -1,4 +1,4 @@ -import { extractStateChangeHandlers } from '../../utils'; +import { extractStateChangeHandlers } from '../../utils/eventHandlersUtils'; import { getStateChangeHandler } from '../stateChangeHandler'; export function useGestureStateChangeEvent(handlerTag: number, config: any) { diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureTouchEvent.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureTouchEvent.ts index c234cfb475..f98068ab68 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureTouchEvent.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureTouchEvent.ts @@ -1,4 +1,4 @@ -import { extractTouchHandlers } from '../../utils'; +import { extractTouchHandlers } from '../../utils/eventHandlersUtils'; import { getTouchEventHandler } from '../touchEventHandler'; export function useGestureTouchEvent(handlerTag: number, config: any) { diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureUpdateEvent.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureUpdateEvent.ts index 81cbac361d..5cc938339f 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureUpdateEvent.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureUpdateEvent.ts @@ -1,5 +1,6 @@ -import { extractUpdateHandlers, isAnimatedEvent } from '../../utils'; +import { isAnimatedEvent } from '../../utils'; import { ReanimatedContext } from '../../../../handlers/gestures/reanimatedWrapper'; +import { extractUpdateHandlers } from '../../utils/eventHandlersUtils'; import { getUpdateHandler } from '../updateHandler'; export function useGestureUpdateEvent(handlerTag: number, config: any) { diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/reanimated/useReanimatedStateChangeEvent.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/reanimated/useReanimatedStateChangeEvent.ts index 615384ed52..86253ae056 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/reanimated/useReanimatedStateChangeEvent.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/reanimated/useReanimatedStateChangeEvent.ts @@ -1,5 +1,5 @@ import { Reanimated } from '../../../../handlers/gestures/reanimatedWrapper'; -import { extractStateChangeHandlers } from '../../utils'; +import { extractStateChangeHandlers } from '../../utils/eventHandlersUtils'; import { getStateChangeHandler } from '../stateChangeHandler'; export function useReanimatedStateChangeEvent(handlerTag: number, config: any) { diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/reanimated/useReanimatedTouchEvent.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/reanimated/useReanimatedTouchEvent.ts index 62f8684145..81e0e3f204 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/reanimated/useReanimatedTouchEvent.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/reanimated/useReanimatedTouchEvent.ts @@ -1,5 +1,5 @@ import { Reanimated } from '../../../../handlers/gestures/reanimatedWrapper'; -import { extractTouchHandlers } from '../../utils'; +import { extractTouchHandlers } from '../../utils/eventHandlersUtils'; import { getTouchEventHandler } from '../touchEventHandler'; export function useReanimatedTouchEvent(handlerTag: number, config: any) { diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/reanimated/useReanimatedUpdateEvent.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/reanimated/useReanimatedUpdateEvent.ts index 0808ebbf42..7f4f0083d1 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/reanimated/useReanimatedUpdateEvent.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/reanimated/useReanimatedUpdateEvent.ts @@ -1,5 +1,5 @@ import { Reanimated } from '../../../../handlers/gestures/reanimatedWrapper'; -import { extractUpdateHandlers } from '../../utils'; +import { extractUpdateHandlers } from '../../utils/eventHandlersUtils'; import { getUpdateHandler } from '../updateHandler'; export function useReanimatedUpdateEvent(handlerTag: number, config: any) { diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/stateChangeHandler.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/stateChangeHandler.ts index c593a6532a..ad6a0ef511 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/stateChangeHandler.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/stateChangeHandler.ts @@ -1,7 +1,8 @@ import { CALLBACK_TYPE } from '../../../handlers/gestures/gesture'; import { State } from '../../../State'; import { CallbackHandlers, StateChangeEvent } from '../../types'; -import { isEventForHandlerWithTag, isNativeEvent, runCallback } from '../utils'; +import { isEventForHandlerWithTag, isNativeEvent } from '../utils'; +import { runCallback } from '../utils/eventHandlersUtils'; export function getStateChangeHandler( handlerTag: number, diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/touchEventHandler.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/touchEventHandler.ts index 4367174fa4..7f58c888ef 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/touchEventHandler.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/touchEventHandler.ts @@ -1,13 +1,12 @@ import { NativeSyntheticEvent } from 'react-native'; import { CallbackHandlers, TouchEvent } from '../../types'; +import { isEventForHandlerWithTag, isNativeEvent } from '../utils'; +import { TouchEventType } from '../../../TouchEventType'; +import { GestureTouchEvent } from '../../../handlers/gestureHandlerCommon'; import { - isEventForHandlerWithTag, - isNativeEvent, runCallback, touchEventTypeToCallbackType, -} from '../utils'; -import { TouchEventType } from '../../../TouchEventType'; -import { GestureTouchEvent } from '../../../handlers/gestureHandlerCommon'; +} from '../utils/eventHandlersUtils'; export function getTouchEventHandler( handlerTag: number, diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/updateHandler.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/updateHandler.ts index 55ed7e5b31..e18dd31c8c 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/updateHandler.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/updateHandler.ts @@ -2,7 +2,8 @@ import { CALLBACK_TYPE } from '../../../handlers/gestures/gesture'; import { tagMessage } from '../../../utils'; import { ReanimatedContext } from '../../../handlers/gestures/reanimatedWrapper'; import { CallbackHandlers, UpdateEvent } from '../../types'; -import { isEventForHandlerWithTag, runCallback } from '../utils'; +import { isEventForHandlerWithTag } from '../utils'; +import { runCallback } from '../utils/eventHandlersUtils'; export function getUpdateHandler( handlerTag: number, diff --git a/packages/react-native-gesture-handler/src/v3/hooks/relations/index.ts b/packages/react-native-gesture-handler/src/v3/hooks/relations/index.ts new file mode 100644 index 0000000000..6c1019125c --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/hooks/relations/index.ts @@ -0,0 +1,3 @@ +export { useSimultaneous } from './useSimultaneous'; +export { useExclusive } from './useExclusive'; +export { useRace } from './useRace'; 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..d14eeea363 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/hooks/relations/useComposedGesture.ts @@ -0,0 +1,110 @@ +import { + NativeGesture, + StateChangeEvent, + UpdateEvent, + TouchEvent, + ComposedGesture, + ComposedGestureType, +} from '../../types'; +import { tagMessage } from '../../../utils'; +import { Reanimated } from '../../../handlers/gestures/reanimatedWrapper'; +import { isComposedGesture } from '../utils/relationUtils'; + +// TODO: Simplify repeated relations (Simultaneous with Simultaneous, Exclusive with Exclusive, etc.) +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); + } + } + }; + + const onReanimatedStateChange = Reanimated?.useComposedEventHandler( + gestures.map( + (gesture) => gesture.gestureEvents.onReanimatedStateChange || null + ) + ); + + const onReanimatedUpdateEvent = Reanimated?.useComposedEventHandler( + gestures.map( + (gesture) => gesture.gestureEvents.onReanimatedUpdateEvent || null + ) + ); + + const onReanimatedTouchEvent = Reanimated?.useComposedEventHandler( + gestures.map( + (gesture) => gesture.gestureEvents.onReanimatedTouchEvent || null + ) + ); + + 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, + onGestureHandlerTouchEvent, + onReanimatedStateChange, + onReanimatedUpdateEvent, + onReanimatedTouchEvent, + onGestureHandlerAnimatedEvent, + }, + 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..f046b05ff7 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/hooks/relations/useSimultaneous.ts @@ -0,0 +1,16 @@ +import { + ComposedGesture, + ComposedGestureType, + NativeGesture, +} from '../../types'; +import { useComposedGesture } from './useComposedGesture'; + +export function useSimultaneous( + ...gestures: (NativeGesture | ComposedGesture)[] +) { + const composedGesture = useComposedGesture(...gestures); + + 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 eaa0c2d619..a432d993e3 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts @@ -2,112 +2,23 @@ import { useEffect, useMemo } from 'react'; import { getNextHandlerTag } from '../../handlers/getNextHandlerTag'; import RNGestureHandlerModule from '../../RNGestureHandlerModule'; import { useGestureCallbacks } from './useGestureCallbacks'; +import { Reanimated } from '../../handlers/gestures/reanimatedWrapper'; import { - Reanimated, - SharedValue, -} from '../../handlers/gestures/reanimatedWrapper'; -import { hash, prepareConfig, isAnimatedEvent } from './utils'; -import { AnimatedEvent } from '../types'; + prepareConfig, + isAnimatedEvent, + shouldHandleTouchEvents, +} from './utils'; import { tagMessage } 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; - onReanimatedStateChange: undefined | ((event: any) => void); - onReanimatedUpdateEvent: undefined | ((event: any) => void); - onReanimatedTouchEvent: undefined | ((event: any) => void); - onGestureHandlerAnimatedEvent: undefined | AnimatedEvent; -}; - -export interface NativeGesture { - tag: number; - name: GestureType; - config: Record; - gestureEvents: GestureEvents; -} - -function hasWorkletEventHandlers(config: Record) { - return Object.values(config).some( - (prop) => typeof prop === 'function' && '__workletHash' in prop - ); -} - -function shouldHandleTouchEvents(config: Record) { - return ( - !!config.onTouchesDown || - !!config.onTouchesMove || - !!config.onTouchesUp || - !!config.onTouchesCancelled - ); -} - -const SHARED_VALUE_OFFSET = 1.618; - -// This is used to obtain HostFunction that can be executed on the UI thread -const { updateGestureHandlerConfig, flushOperations } = RNGestureHandlerModule; - -function bindSharedValues(config: any, handlerTag: number) { - if (Reanimated === undefined) { - return; - } - - const baseListenerId = handlerTag + SHARED_VALUE_OFFSET; - - const attachListener = (sharedValue: SharedValue, configKey: string) => { - 'worklet'; - const keyHash = hash(configKey); - const listenerId = baseListenerId + keyHash; - - sharedValue.addListener(listenerId, (value) => { - updateGestureHandlerConfig(handlerTag, { [configKey]: value }); - flushOperations(); - }); - }; - - for (const [key, maybeSharedValue] of Object.entries(config)) { - if (!Reanimated.isSharedValue(maybeSharedValue)) { - continue; - } - - Reanimated.runOnUI(attachListener)(maybeSharedValue, key); - } -} - -function unbindSharedValues(config: any, handlerTag: number) { - if (Reanimated === undefined) { - return; - } - - const baseListenerId = handlerTag + SHARED_VALUE_OFFSET; - - for (const [key, maybeSharedValue] of Object.entries(config)) { - if (!Reanimated.isSharedValue(maybeSharedValue)) { - continue; - } - - const keyHash = hash(key); - const listenerId = baseListenerId + keyHash; - - Reanimated.runOnUI(() => { - maybeSharedValue.removeListener(listenerId); - })(); - } -} +import { NativeGesture, SingleGestureType } from '../types'; +import { + bindSharedValues, + hasWorkletEventHandlers, + unbindSharedValues, +} from './utils/reanimatedUtils'; +import { prepareRelations } from './utils/relationUtils'; export function useGesture( - type: GestureType, + type: SingleGestureType, config: Record ): NativeGesture { const tag = useMemo(() => getNextHandlerTag(), []); @@ -169,6 +80,8 @@ export function useGesture( throw new Error(tagMessage('Failed to create reanimated event handlers.')); } + const gestureRelations = prepareRelations(config, tag); + useMemo(() => { RNGestureHandlerModule.createGestureHandler(type, tag, {}); RNGestureHandlerModule.flushOperations(); @@ -198,8 +111,8 @@ export function useGesture( }, [config, tag]); return { - tag: tag, - name: type, + tag, + type, config, gestureEvents: { onGestureHandlerStateChange, @@ -210,5 +123,6 @@ export function useGesture( onReanimatedTouchEvent, 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 c55c121f97..57c5bee592 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/utils.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils.ts @@ -1,58 +1,14 @@ import { NativeSyntheticEvent } from 'react-native'; -import { CALLBACK_TYPE } from '../../handlers/gestures/gesture'; -import { TouchEventType } from '../../TouchEventType'; import { AnimatedEvent, - CallbackHandlers, GestureHandlerEvent, GestureStateChangeEventWithData, GestureUpdateEventWithData, - UpdateEvent, } from '../types'; import { GestureTouchEvent } from '../../handlers/gestureHandlerCommon'; import { tagMessage } from '../../utils'; import { Reanimated } from '../../handlers/gestures/reanimatedWrapper'; -export function getHandler(type: CALLBACK_TYPE, config: CallbackHandlers) { - 'worklet'; - switch (type) { - case CALLBACK_TYPE.BEGAN: - return config.onBegin; - case CALLBACK_TYPE.START: - return config.onStart; - case CALLBACK_TYPE.UPDATE: - return config.onUpdate; - case CALLBACK_TYPE.END: - return config.onEnd; - case CALLBACK_TYPE.FINALIZE: - return config.onFinalize; - case CALLBACK_TYPE.TOUCHES_DOWN: - return config.onTouchesDown; - case CALLBACK_TYPE.TOUCHES_MOVE: - return config.onTouchesMove; - case CALLBACK_TYPE.TOUCHES_UP: - return config.onTouchesUp; - case CALLBACK_TYPE.TOUCHES_CANCELLED: - return config.onTouchesCancelled; - } -} - -export function touchEventTypeToCallbackType( - eventType: TouchEventType -): CALLBACK_TYPE { - 'worklet'; - switch (eventType) { - case TouchEventType.TOUCHES_DOWN: - return CALLBACK_TYPE.TOUCHES_DOWN; - case TouchEventType.TOUCHES_MOVE: - return CALLBACK_TYPE.TOUCHES_MOVE; - case TouchEventType.TOUCHES_UP: - return CALLBACK_TYPE.TOUCHES_UP; - case TouchEventType.TOUCHES_CANCELLED: - return CALLBACK_TYPE.TOUCHES_CANCELLED; - } - return CALLBACK_TYPE.UNDEFINED; -} export function isNativeEvent( event: GestureHandlerEvent ): event is @@ -64,20 +20,6 @@ export function isNativeEvent( return 'nativeEvent' in event; } -export function runCallback( - type: CALLBACK_TYPE, - config: CallbackHandlers, - event: GestureHandlerEvent>, - ...args: unknown[] -) { - 'worklet'; - const handler = getHandler(type, config); - - // TODO: add proper types (likely boolean) - // @ts-ignore It works, duh - handler?.(isNativeEvent(event) ? event.nativeEvent : event, ...args); -} - export function isEventForHandlerWithTag( handlerTag: number, event: GestureHandlerEvent> @@ -116,49 +58,6 @@ export function checkMappingForChangeProperties(animatedEvent: AnimatedEvent) { } } -export function extractStateChangeHandlers(config: any): CallbackHandlers { - 'worklet'; - const { onBegin, onStart, onEnd, onFinalize } = config; - - const handlers: CallbackHandlers = { - ...(onBegin ? { onBegin } : {}), - ...(onStart ? { onStart } : {}), - ...(onEnd ? { onEnd } : {}), - ...(onFinalize ? { onFinalize } : {}), - }; - - return handlers; -} - -export function extractUpdateHandlers(config: any): { - handlers: CallbackHandlers; - changeEventCalculator?: ( - current: UpdateEvent>, - previous?: UpdateEvent> - ) => UpdateEvent>; -} { - 'worklet'; - const { onUpdate, changeEventCalculator } = config; - - const handlers: CallbackHandlers = { ...(onUpdate ? { onUpdate } : {}) }; - - return { handlers, changeEventCalculator }; -} - -export function extractTouchHandlers(config: any): CallbackHandlers { - const { onTouchesDown, onTouchesMove, onTouchesUp, onTouchesCancelled } = - config; - - const handlers: CallbackHandlers = { - ...(onTouchesDown ? { onTouchesDown } : {}), - ...(onTouchesMove ? { onTouchesMove } : {}), - ...(onTouchesUp ? { onTouchesUp } : {}), - ...(onTouchesCancelled ? { onTouchesCancelled } : {}), - }; - - return handlers; -} - export function prepareConfig(config: any) { const copy = { ...config }; @@ -171,19 +70,18 @@ export function prepareConfig(config: any) { // TODO: Filter changes - passing functions (and possibly other types) // causes a native crash copy.onUpdate = null; + copy.simultaneousWithExternalGesture = null; + copy.requireExternalGestureToFail = null; + copy.blocksExternalGesture = null; return copy; } -// Variant of djb2 hash function. -// Taken from https://gist.github.com/eplawless/52813b1d8ad9af510d85?permalink_comment_id=3367765#gistcomment-3367765 -export function hash(str: string) { - 'worklet'; - const len = str.length; - let h = 5381; - - for (let i = 0; i < len; i++) { - h = (h * 33) ^ str.charCodeAt(i); - } - return h >>> 0; +export function shouldHandleTouchEvents(config: Record) { + return ( + !!config.onTouchesDown || + !!config.onTouchesMove || + !!config.onTouchesUp || + !!config.onTouchesCancelled + ); } diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/eventHandlersUtils.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/eventHandlersUtils.ts new file mode 100644 index 0000000000..963e5c5ecd --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/eventHandlersUtils.ts @@ -0,0 +1,106 @@ +import { TouchEventType } from '../../../TouchEventType'; +import { CALLBACK_TYPE } from '../../../handlers/gestures/gesture'; +import { + CallbackHandlers, + GestureHandlerEvent, + UpdateEvent, +} from '../../types'; +import { isNativeEvent } from '../utils'; + +export function extractStateChangeHandlers(config: any): CallbackHandlers { + 'worklet'; + const { onBegin, onStart, onEnd, onFinalize } = config; + + const handlers: CallbackHandlers = { + ...(onBegin ? { onBegin } : {}), + ...(onStart ? { onStart } : {}), + ...(onEnd ? { onEnd } : {}), + ...(onFinalize ? { onFinalize } : {}), + }; + + return handlers; +} + +export function extractUpdateHandlers(config: any): { + handlers: CallbackHandlers; + changeEventCalculator?: ( + current: UpdateEvent>, + previous?: UpdateEvent> + ) => UpdateEvent>; +} { + 'worklet'; + const { onUpdate, changeEventCalculator } = config; + + const handlers: CallbackHandlers = { ...(onUpdate ? { onUpdate } : {}) }; + + return { handlers, changeEventCalculator }; +} + +export function extractTouchHandlers(config: any): CallbackHandlers { + const { onTouchesDown, onTouchesMove, onTouchesUp, onTouchesCancelled } = + config; + + const handlers: CallbackHandlers = { + ...(onTouchesDown ? { onTouchesDown } : {}), + ...(onTouchesMove ? { onTouchesMove } : {}), + ...(onTouchesUp ? { onTouchesUp } : {}), + ...(onTouchesCancelled ? { onTouchesCancelled } : {}), + }; + + return handlers; +} + +export function getHandler(type: CALLBACK_TYPE, config: CallbackHandlers) { + 'worklet'; + switch (type) { + case CALLBACK_TYPE.BEGAN: + return config.onBegin; + case CALLBACK_TYPE.START: + return config.onStart; + case CALLBACK_TYPE.UPDATE: + return config.onUpdate; + case CALLBACK_TYPE.END: + return config.onEnd; + case CALLBACK_TYPE.FINALIZE: + return config.onFinalize; + case CALLBACK_TYPE.TOUCHES_DOWN: + return config.onTouchesDown; + case CALLBACK_TYPE.TOUCHES_MOVE: + return config.onTouchesMove; + case CALLBACK_TYPE.TOUCHES_UP: + return config.onTouchesUp; + case CALLBACK_TYPE.TOUCHES_CANCELLED: + return config.onTouchesCancelled; + } +} + +export function touchEventTypeToCallbackType( + eventType: TouchEventType +): CALLBACK_TYPE { + 'worklet'; + switch (eventType) { + case TouchEventType.TOUCHES_DOWN: + return CALLBACK_TYPE.TOUCHES_DOWN; + case TouchEventType.TOUCHES_MOVE: + return CALLBACK_TYPE.TOUCHES_MOVE; + case TouchEventType.TOUCHES_UP: + return CALLBACK_TYPE.TOUCHES_UP; + case TouchEventType.TOUCHES_CANCELLED: + return CALLBACK_TYPE.TOUCHES_CANCELLED; + } + return CALLBACK_TYPE.UNDEFINED; +} + +export function runCallback( + type: CALLBACK_TYPE, + config: CallbackHandlers, + event: GestureHandlerEvent>, + ...args: unknown[] +) { + 'worklet'; + const handler = getHandler(type, config); + + // TODO: add proper types (likely boolean) + // @ts-ignore It works, duh + handler?.(isNativeEvent(event) ? event.nativeEvent : event, ...args); +} diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/reanimatedUtils.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/reanimatedUtils.ts new file mode 100644 index 0000000000..c0cf19cc81 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/reanimatedUtils.ts @@ -0,0 +1,77 @@ +import RNGestureHandlerModule from '../../../RNGestureHandlerModule'; +import { + Reanimated, + SharedValue, +} from '../../../handlers/gestures/reanimatedWrapper'; + +// Variant of djb2 hash function. +// Taken from https://gist.github.com/eplawless/52813b1d8ad9af510d85?permalink_comment_id=3367765#gistcomment-3367765 +function hash(str: string) { + 'worklet'; + const len = str.length; + let h = 5381; + + for (let i = 0; i < len; i++) { + h = (h * 33) ^ str.charCodeAt(i); + } + return h >>> 0; +} + +const SHARED_VALUE_OFFSET = 1.618; + +// This is used to obtain HostFunction that can be executed on the UI thread +const { updateGestureHandlerConfig, flushOperations } = RNGestureHandlerModule; + +export function bindSharedValues(config: any, handlerTag: number) { + if (Reanimated === undefined) { + return; + } + + const baseListenerId = handlerTag + SHARED_VALUE_OFFSET; + + const attachListener = (sharedValue: SharedValue, configKey: string) => { + 'worklet'; + const keyHash = hash(configKey); + const listenerId = baseListenerId + keyHash; + + sharedValue.addListener(listenerId, (value) => { + updateGestureHandlerConfig(handlerTag, { [configKey]: value }); + flushOperations(); + }); + }; + + for (const [key, maybeSharedValue] of Object.entries(config)) { + if (!Reanimated.isSharedValue(maybeSharedValue)) { + continue; + } + + Reanimated.runOnUI(attachListener)(maybeSharedValue, key); + } +} + +export function unbindSharedValues(config: any, handlerTag: number) { + if (Reanimated === undefined) { + return; + } + + const baseListenerId = handlerTag + SHARED_VALUE_OFFSET; + + for (const [key, maybeSharedValue] of Object.entries(config)) { + if (!Reanimated.isSharedValue(maybeSharedValue)) { + continue; + } + + const keyHash = hash(key); + const listenerId = baseListenerId + keyHash; + + Reanimated.runOnUI(() => { + maybeSharedValue.removeListener(listenerId); + })(); + } +} + +export function hasWorkletEventHandlers(config: Record) { + return Object.values(config).some( + (prop) => typeof prop === 'function' && '__workletHash' in prop + ); +} diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/relationUtils.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/relationUtils.ts new file mode 100644 index 0000000000..e94e0a6c96 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/relationUtils.ts @@ -0,0 +1,59 @@ +import { + ComposedGesture, + Gesture, + GestureRelations, + NativeGesture, +} from '../../types'; + +export function isComposedGesture( + gesture: NativeGesture | ComposedGesture +): gesture is ComposedGesture { + return 'tags' in gesture; +} + +export function prepareRelations( + config: any, + handlerTag: number +): GestureRelations { + // TODO: Handle composed gestures passed into external relations + 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; + }; + + if (config.simultaneousWithExternalGesture) { + if (Array.isArray(config.simultaneousWithExternalGesture)) { + for (const gesture of config.simultaneousWithExternalGesture) { + gesture.gestureRelations.simultaneousHandlers.push(handlerTag); + } + } else { + config.simultaneousWithExternalGesture.gestureRelations.simultaneousHandlers.push( + handlerTag + ); + } + } + + return { + simultaneousHandlers: extractHandlerTags( + config.simultaneousWithExternalGesture + ), + waitFor: extractHandlerTags(config.requireExternalGestureToFail), + blocksHandlers: extractHandlerTags(config.blocksExternalGesture), + }; +} diff --git a/packages/react-native-gesture-handler/src/v3/types.ts b/packages/react-native-gesture-handler/src/v3/types.ts index 03611a4157..3118cf66c1 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,80 @@ export type CallbackHandlers = Omit< export type AnimatedEvent = ((...args: any[]) => void) & { _argMapping: (Animated.Mapping | null)[]; }; + +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; + onReanimatedStateChange: + | undefined + | ((event: StateChangeEvent>) => void); + onReanimatedUpdateEvent: + | undefined + | ((event: UpdateEvent>) => void); + onReanimatedTouchEvent: undefined | ((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;