diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerDetector.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerDetector.mm index b5e0059608..60078f6c62 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerDetector.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerDetector.mm @@ -211,7 +211,7 @@ - (void)attachHandlers:(const std::vector &)handlerTags } // This covers the case where `NativeViewGestureHandlers` are attached after child views were created. - if (self.subviews[0]) { + if (self.subviews.count != 0) { [self tryAttachNativeHandlersToChildView]; } } diff --git a/packages/react-native-gesture-handler/src/v3/detectors/GestureDetector.tsx b/packages/react-native-gesture-handler/src/v3/detectors/GestureDetector.tsx index 0111b0d317..5807e49a6d 100644 --- a/packages/react-native-gesture-handler/src/v3/detectors/GestureDetector.tsx +++ b/packages/react-native-gesture-handler/src/v3/detectors/GestureDetector.tsx @@ -6,7 +6,7 @@ import { GestureDetectorProps as LegacyGestureDetectorProps, GestureDetector as LegacyGestureDetector, } from '../../handlers/gestures/GestureDetector'; -import { DetectorContext } from './VirtualDetector/useDetectorContext'; +import { InterceptingDetectorContext } from './VirtualDetector/useInterceptingDetectorContext'; import { VirtualDetector } from './VirtualDetector/VirtualDetector'; import { use } from 'react'; import { isTestEnv } from '../../utils'; @@ -31,7 +31,7 @@ export function GestureDetector( return ; } - const context = use(DetectorContext); + const context = use(InterceptingDetectorContext); return context ? ( )} diff --git a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/InterceptingGestureDetector.tsx b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/InterceptingGestureDetector.tsx index 83ec28be86..e8ae57a345 100644 --- a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/InterceptingGestureDetector.tsx +++ b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/InterceptingGestureDetector.tsx @@ -1,11 +1,15 @@ -import React, { RefObject, useCallback, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import HostGestureDetector from '../HostGestureDetector'; import { - VirtualChildren, + VirtualChild, GestureHandlerEvent, DetectorCallbacks, } from '../../types'; -import { DetectorContext } from './useDetectorContext'; +import { + InterceptingDetectorContext, + InterceptingDetectorContextValue, + InterceptingDetectorMode, +} from './useInterceptingDetectorContext'; import { Reanimated } from '../../../handlers/gestures/reanimatedWrapper'; import { configureRelations, ensureNativeDetectorComponent } from '../utils'; import { isComposedGesture } from '../../hooks/utils/relationUtils'; @@ -17,64 +21,98 @@ import { } from '../common'; import { tagMessage } from '../../../utils'; +interface VirtualChildrenForNative { + viewTag: number; + handlerTags: number[]; + viewRef: unknown; +} + export function InterceptingGestureDetector({ gesture, children, }: InterceptingGestureDetectorProps) { - const [virtualChildren, setVirtualChildren] = useState([]); - - const virtualMethods = useRef< - Map>> - >(new Map()); - - const [shouldUseReanimated, setShouldUseReanimated] = useState( - gesture ? gesture.config.shouldUseReanimatedDetector : false + const [virtualChildren, setVirtualChildren] = useState>( + () => new Set() ); - const [dispatchesAnimatedEvents, setDispatchesAnimatedEvents] = useState( - gesture ? gesture.config.dispatchesAnimatedEvents : false + const virtualChildrenForNativeComponent: VirtualChildrenForNative[] = useMemo( + () => + Array.from(virtualChildren).map((child) => ({ + viewTag: child.viewTag, + handlerTags: child.handlerTags, + viewRef: child.viewRef, + })), + [virtualChildren] ); + const [mode, setMode] = useState( + gesture?.config.shouldUseReanimatedDetector + ? InterceptingDetectorMode.REANIMATED + : gesture?.config.dispatchesAnimatedEvents + ? InterceptingDetectorMode.ANIMATED + : InterceptingDetectorMode.DEFAULT + ); + + const shouldUseReanimatedDetector = + mode === InterceptingDetectorMode.REANIMATED; + const dispatchesAnimatedEvents = mode === InterceptingDetectorMode.ANIMATED; const NativeDetectorComponent = dispatchesAnimatedEvents ? AnimatedNativeDetector - : shouldUseReanimated + : shouldUseReanimatedDetector ? ReanimatedNativeDetector : HostGestureDetector; - const register = useCallback( - ( - child: VirtualChildren, - methods: RefObject>, - forReanimated: boolean | undefined, - forAnimated: boolean | undefined - ) => { - setShouldUseReanimated(!!forReanimated); - setDispatchesAnimatedEvents(!!forAnimated); - - setVirtualChildren((prev) => { - const index = prev.findIndex((c) => c.viewTag === child.viewTag); - if (index !== -1) { - const updated = [...prev]; - updated[index] = child; - return updated; - } + const register = useCallback((child: VirtualChild) => { + setVirtualChildren((prev) => { + const newSet = new Set(prev); + newSet.add(child); + return newSet; + }); + }, []); - return [...prev, child]; - }); + const unregister = useCallback((child: VirtualChild) => { + setVirtualChildren((prev) => { + const newSet = new Set(prev); + newSet.delete(child); + return newSet; + }); + }, []); - child.handlerTags.forEach((tag) => { - virtualMethods.current.set(tag, methods); - }); - }, - [] - ); + const contextValue: InterceptingDetectorContextValue = useMemo( + () => ({ + mode, + setMode: (newMode: InterceptingDetectorMode) => { + if ( + (newMode === InterceptingDetectorMode.REANIMATED && + mode === InterceptingDetectorMode.ANIMATED) || + (newMode === InterceptingDetectorMode.ANIMATED && + mode === InterceptingDetectorMode.REANIMATED) + ) { + throw new Error( + tagMessage( + 'InterceptingGestureDetector can only handle either Reanimated or Animated events.' + ) + ); + } - const unregister = useCallback((childTag: number, handlerTags: number[]) => { - handlerTags.forEach((tag) => { - virtualMethods.current.delete(tag); - }); + setMode(newMode); + }, + register, + unregister, + }), + [mode, register, unregister] + ); - setVirtualChildren((prev) => prev.filter((c) => c.viewTag !== childTag)); - }, []); + useEffect(() => { + if (gesture?.config?.dispatchesAnimatedEvents) { + contextValue.setMode(InterceptingDetectorMode.ANIMATED); + } else if (gesture?.config?.shouldUseReanimatedDetector) { + contextValue.setMode(InterceptingDetectorMode.REANIMATED); + } + }, [ + contextValue, + gesture?.config?.dispatchesAnimatedEvents, + gesture?.config?.shouldUseReanimatedDetector, + ]); // It might happen only with ReanimatedNativeDetector if (!NativeDetectorComponent) { @@ -85,20 +123,23 @@ export function InterceptingGestureDetector({ ); } - const handleGestureEvent = (key: keyof DetectorCallbacks) => { - return (e: GestureHandlerEvent) => { - if (gesture?.detectorCallbacks[key]) { - gesture.detectorCallbacks[key](e); - } - - virtualMethods.current.forEach((ref) => { - const method = ref.current?.[key]; - if (method) { - method(e); + const createGestureEventHandler = useCallback( + (key: keyof DetectorCallbacks) => { + return (e: GestureHandlerEvent) => { + if (gesture?.detectorCallbacks[key]) { + gesture.detectorCallbacks[key](e); } - }); - }; - }; + + virtualChildren.forEach((child) => { + const method = child.methods[key]; + if (method) { + method(e); + } + }); + }; + }, + [gesture, virtualChildren] + ); const getHandlers = useCallback( (key: keyof DetectorCallbacks) => { @@ -112,8 +153,8 @@ export function InterceptingGestureDetector({ ); } - virtualMethods.current.forEach((ref) => { - const handler = ref.current?.[key]; + virtualChildren.forEach((child) => { + const handler = child.methods[key]; if (handler) { handlers.push( handler as (e: GestureHandlerEvent) => void @@ -126,14 +167,28 @@ export function InterceptingGestureDetector({ [virtualChildren, gesture?.detectorCallbacks] ); + const reanimatedUpdateEvents = useMemo( + () => getHandlers('onReanimatedUpdateEvent'), + [getHandlers] + ); const reanimatedEventHandler = Reanimated?.useComposedEventHandler( - getHandlers('onReanimatedUpdateEvent') + reanimatedUpdateEvents + ); + + const reanimatedStateChangeEvents = useMemo( + () => getHandlers('onReanimatedStateChange'), + [getHandlers] ); const reanimatedStateChangeHandler = Reanimated?.useComposedEventHandler( - getHandlers('onReanimatedStateChange') + reanimatedStateChangeEvents + ); + + const reanimatedTouchEvents = useMemo( + () => getHandlers('onReanimatedTouchEvent'), + [getHandlers] ); const reanimatedTouchEventHandler = Reanimated?.useComposedEventHandler( - getHandlers('onReanimatedTouchEvent') + reanimatedTouchEvents ); ensureNativeDetectorComponent(NativeDetectorComponent); @@ -142,47 +197,53 @@ export function InterceptingGestureDetector({ configureRelations(gesture); } + const handlerTags = useMemo(() => { + if (gesture) { + return isComposedGesture(gesture) ? gesture.tags : [gesture.tag]; + } + return []; + }, [gesture]); + return ( - + createGestureEventHandler('onGestureHandlerStateChange'), + [createGestureEventHandler] )} // @ts-ignore This is a type mismatch between RNGH types and RN Codegen types - onGestureHandlerEvent={handleGestureEvent('onGestureHandlerEvent')} + onGestureHandlerEvent={useMemo( + () => createGestureEventHandler('onGestureHandlerEvent'), + [createGestureEventHandler] + )} // @ts-ignore This is a type mismatch between RNGH types and RN Codegen types onGestureHandlerAnimatedEvent={ gesture?.detectorCallbacks.onGestureHandlerAnimatedEvent } // @ts-ignore This is a type mismatch between RNGH types and RN Codegen types - onGestureHandlerTouchEvent={handleGestureEvent( - 'onGestureHandlerTouchEvent' + onGestureHandlerTouchEvent={useMemo( + () => createGestureEventHandler('onGestureHandlerTouchEvent'), + [createGestureEventHandler] )} // @ts-ignore This is a type mismatch between RNGH types and RN Codegen types onGestureHandlerReanimatedStateChange={ - shouldUseReanimated ? reanimatedStateChangeHandler : undefined + shouldUseReanimatedDetector ? reanimatedStateChangeHandler : undefined } // @ts-ignore This is a type mismatch between RNGH types and RN Codegen types onGestureHandlerReanimatedEvent={ - shouldUseReanimated ? reanimatedEventHandler : undefined + shouldUseReanimatedDetector ? reanimatedEventHandler : undefined } // @ts-ignore This is a type mismatch between RNGH types and RN Codegen types onGestureHandlerReanimatedTouchEvent={ - shouldUseReanimated ? reanimatedTouchEventHandler : undefined - } - handlerTags={ - gesture - ? isComposedGesture(gesture) - ? gesture.tags - : [gesture.tag] - : [] + shouldUseReanimatedDetector ? reanimatedTouchEventHandler : undefined } + handlerTags={handlerTags} style={nativeDetectorStyles.detector} - virtualChildren={virtualChildren} + virtualChildren={virtualChildrenForNativeComponent} moduleId={globalThis._RNGH_MODULE_ID}> {children} - + ); } diff --git a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx index 7e7e486ecb..706bf92fa6 100644 --- a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx +++ b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx @@ -1,17 +1,18 @@ -import { RefObject, useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { Wrap } from '../../../handlers/gestures/GestureDetector/Wrap'; import { findNodeHandle, Platform } from 'react-native'; -import { useDetectorContext } from './useDetectorContext'; +import { + InterceptingDetectorMode, + useInterceptingDetectorContext, +} from './useInterceptingDetectorContext'; import { isComposedGesture } from '../../hooks/utils/relationUtils'; import { NativeDetectorProps } from '../common'; import { configureRelations } from '../utils'; import { tagMessage } from '../../../utils'; -import { DetectorCallbacks } from '../../types'; +import { DetectorCallbacks, VirtualChild } from '../../types'; -export function VirtualDetector( - props: NativeDetectorProps -) { - const context = useDetectorContext(); +function useRequiredInterceptingDetectorContext() { + const context = useInterceptingDetectorContext(); if (!context) { throw new Error( tagMessage( @@ -19,43 +20,37 @@ export function VirtualDetector( ) ); } - const { register, unregister } = context; + return context; +} + +export function VirtualDetector( + props: NativeDetectorProps +) { + // Don't memoize virtual detectors to be able to listen to changes in children + // TODO: replace with MutationObserver when it rolls out in React Native + 'use no memo'; + + const { register, unregister, setMode } = + useRequiredInterceptingDetectorContext(); const viewRef = useRef(null); const [viewTag, setViewTag] = useState(-1); - const virtualMethods = useRef(props.gesture.detectorCallbacks); - const handleRef = useCallback( (node: any) => { viewRef.current = node; - if (!node) { - return; - } - - const tag = Platform.OS === 'web' ? node : findNodeHandle(node); - - if (tag != null) { - setViewTag(tag); + if (node) { + const tag: number = Platform.OS === 'web' ? node : findNodeHandle(node); + setViewTag(tag ?? -1); + } else { + setViewTag(-1); } - - return () => { - if (tag != null) { - const handlerTags = isComposedGesture(props.gesture) - ? props.gesture.tags - : [props.gesture.tag]; - - unregister(tag, handlerTags); - } - }; }, + // Invalid dependency array to change the function when children change + // eslint-disable-next-line react-hooks/exhaustive-deps [props.children] ); - useEffect(() => { - virtualMethods.current = props.gesture.detectorCallbacks; - }, [props.gesture.detectorCallbacks]); - useEffect(() => { if (viewTag === -1) { return; @@ -65,26 +60,30 @@ export function VirtualDetector( ? props.gesture.tags : [props.gesture.tag]; - const virtualProps = { + if (props.gesture.config.dispatchesAnimatedEvents) { + throw new Error( + tagMessage( + 'VirtualGestureDetector cannot handle Animated events with native driver when used inside InterceptingGestureDetector. Use Reanimated or Animated events without native driver instead.' + ) + ); + } else if (props.gesture.config.shouldUseReanimatedDetector) { + setMode(InterceptingDetectorMode.REANIMATED); + } + + const virtualChild: VirtualChild = { viewTag, handlerTags, + methods: props.gesture.detectorCallbacks as DetectorCallbacks, + // used by HostGestureDetector on web + viewRef: Platform.OS === 'web' ? viewRef : undefined, }; - if (Platform.OS === 'web') { - Object.assign(virtualProps, { viewRef }); - } - - register( - virtualProps, - virtualMethods as RefObject>, - props.gesture.config.shouldUseReanimatedDetector, - props.gesture.config.dispatchesAnimatedEvents - ); + register(virtualChild); return () => { - unregister(viewTag, handlerTags); + unregister(virtualChild); }; - }, [viewTag, props.gesture, register, unregister]); + }, [viewTag, props.gesture, register, unregister, setMode]); configureRelations(props.gesture); diff --git a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useDetectorContext.ts b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useDetectorContext.ts deleted file mode 100644 index 2eecca7129..0000000000 --- a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useDetectorContext.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { createContext, RefObject, use } from 'react'; -import { DetectorCallbacks, VirtualChildren } from '../../types'; - -type DetectorContextType = { - register: ( - child: VirtualChildren, - methods: RefObject>, - forReanimated: boolean | undefined, - forAnimated: boolean | undefined - ) => void; - unregister: (child: number, handlerTags: number[]) => void; -}; - -export const DetectorContext = createContext(null); - -export function useDetectorContext() { - const ctx = use(DetectorContext); - if (!ctx) { - return null; - } - return ctx; -} diff --git a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useInterceptingDetectorContext.ts b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useInterceptingDetectorContext.ts new file mode 100644 index 0000000000..105f4aaa75 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/useInterceptingDetectorContext.ts @@ -0,0 +1,22 @@ +import { createContext, use } from 'react'; +import { VirtualChild } from '../../types'; + +export enum InterceptingDetectorMode { + DEFAULT, + ANIMATED, + REANIMATED, +} + +export type InterceptingDetectorContextValue = { + mode: InterceptingDetectorMode; + setMode: (mode: InterceptingDetectorMode) => void; + register: (child: VirtualChild) => void; + unregister: (child: VirtualChild) => void; +}; + +export const InterceptingDetectorContext = + createContext(null); + +export function useInterceptingDetectorContext() { + return use(InterceptingDetectorContext); +} diff --git a/packages/react-native-gesture-handler/src/v3/types/DetectorTypes.ts b/packages/react-native-gesture-handler/src/v3/types/DetectorTypes.ts index a14fd365cc..e7416b24ba 100644 --- a/packages/react-native-gesture-handler/src/v3/types/DetectorTypes.ts +++ b/packages/react-native-gesture-handler/src/v3/types/DetectorTypes.ts @@ -21,7 +21,11 @@ export type DetectorCallbacks = { onGestureHandlerAnimatedEvent: undefined | AnimatedEvent; }; -export type VirtualChildren = { +export type VirtualChild = { viewTag: number; handlerTags: number[]; + methods: DetectorCallbacks; + + // only set on web + viewRef: unknown; };