diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/eventHandler.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/eventHandler.ts new file mode 100644 index 0000000000..b01b90fd84 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/eventHandler.ts @@ -0,0 +1,127 @@ +import { + flattenAndFilterEvent, + isEventForHandlerWithTag, + maybeExtractNativeEvent, + runCallback, + touchEventTypeToCallbackType, +} from '../utils'; +import { tagMessage } from '../../../utils'; +import { ReanimatedContext } from '../../../handlers/gestures/reanimatedWrapper'; +import { + ChangeCalculatorType, + GestureCallbacks, + GestureHandlerEventWithHandlerData, + GestureStateChangeEventWithHandlerData, + GestureUpdateEventWithHandlerData, +} from '../../types'; +import { CALLBACK_TYPE } from '../../../handlers/gestures/gesture'; +import { State } from '../../../State'; +import { TouchEventType } from '../../../TouchEventType'; +import { GestureTouchEvent } from '../../../handlers/gestureHandlerCommon'; + +function handleStateChangeEvent( + eventWithData: GestureStateChangeEventWithHandlerData, + callbacks: GestureCallbacks, + context: ReanimatedContext +) { + 'worklet'; + const { oldState, state } = eventWithData; + const event = flattenAndFilterEvent(eventWithData); + + if (oldState === State.UNDETERMINED && state === State.BEGAN) { + runCallback(CALLBACK_TYPE.BEGAN, callbacks, event); + } else if ( + (oldState === State.BEGAN || oldState === State.UNDETERMINED) && + state === State.ACTIVE + ) { + runCallback(CALLBACK_TYPE.START, callbacks, event); + } else if (oldState !== state && state === State.END) { + if (oldState === State.ACTIVE) { + runCallback(CALLBACK_TYPE.END, callbacks, event, true); + } + runCallback(CALLBACK_TYPE.FINALIZE, callbacks, event, true); + + if (context) { + context.lastUpdateEvent = undefined; + } + } else if ( + (state === State.FAILED || state === State.CANCELLED) && + state !== oldState + ) { + if (oldState === State.ACTIVE) { + runCallback(CALLBACK_TYPE.END, callbacks, event, false); + } + runCallback(CALLBACK_TYPE.FINALIZE, callbacks, event, false); + + if (context) { + context.lastUpdateEvent = undefined; + } + } +} + +export function handleUpdateEvent( + eventWithData: GestureUpdateEventWithHandlerData, + handlers: GestureCallbacks, + changeEventCalculator: ChangeCalculatorType | undefined, + context: ReanimatedContext +) { + 'worklet'; + const eventWithChanges = changeEventCalculator + ? changeEventCalculator( + eventWithData, + context ? context.lastUpdateEvent : undefined + ) + : eventWithData; + + const event = flattenAndFilterEvent(eventWithChanges); + + // This should never happen, but since we don't want to call hooks conditionally, we have to mark + // context as possibly undefined to make TypeScript happy. + if (!context) { + throw new Error(tagMessage('Event handler context is not defined')); + } + + runCallback(CALLBACK_TYPE.UPDATE, handlers, event); + + context.lastUpdateEvent = eventWithData; +} + +export function handleTouchEvent( + event: GestureTouchEvent, + handlers: GestureCallbacks +) { + 'worklet'; + + if (event.eventType !== TouchEventType.UNDETERMINED) { + runCallback(touchEventTypeToCallbackType(event.eventType), handlers, event); + } +} + +export function eventHandler( + handlerTag: number, + sourceEvent: GestureHandlerEventWithHandlerData, + handlers: GestureCallbacks, + changeEventCalculator: ChangeCalculatorType | undefined, + jsContext: ReanimatedContext, + dispatchesAnimatedEvents: boolean +) { + 'worklet'; + const eventWithData = maybeExtractNativeEvent(sourceEvent); + + if (!isEventForHandlerWithTag(handlerTag, eventWithData)) { + return; + } + + if ('oldState' in eventWithData && eventWithData.oldState !== undefined) { + handleStateChangeEvent(eventWithData, handlers, jsContext); + } else if ('allTouches' in eventWithData) { + handleTouchEvent(eventWithData, handlers); + } else if (!dispatchesAnimatedEvents) { + handleUpdateEvent( + eventWithData, + handlers, + changeEventCalculator, + jsContext + ); + } +} 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 deleted file mode 100644 index 22120f8af4..0000000000 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureStateChangeEvent.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useMemo } from 'react'; -import { BaseGestureConfig } from '../../../types'; -import { prepareStateChangeHandlers } from '../../utils'; -import { getStateChangeHandler } from '../stateChangeHandler'; -import { ReanimatedContext } from '../../../../handlers/gestures/reanimatedWrapper'; - -export function useGestureStateChangeEvent( - handlerTag: number, - config: BaseGestureConfig, - context: ReanimatedContext -) { - return useMemo(() => { - const handlers = prepareStateChangeHandlers({ - onBegin: config.onBegin, - onActivate: config.onActivate, - onDeactivate: config.onDeactivate, - onFinalize: config.onFinalize, - }); - return getStateChangeHandler(handlerTag, handlers, context); - }, [ - handlerTag, - config.onBegin, - config.onActivate, - config.onDeactivate, - config.onFinalize, - context, - ]); -} 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 deleted file mode 100644 index aee137df1d..0000000000 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureTouchEvent.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { useMemo } from 'react'; -import { BaseGestureConfig } from '../../../types'; -import { prepareTouchHandlers } from '../../utils'; -import { getTouchEventHandler } from '../touchEventHandler'; - -export function useGestureTouchEvent( - handlerTag: number, - config: BaseGestureConfig -) { - return useMemo(() => { - const handlers = prepareTouchHandlers({ - onTouchesDown: config.onTouchesDown, - onTouchesMove: config.onTouchesMove, - onTouchesUp: config.onTouchesUp, - onTouchesCancel: config.onTouchesCancel, - }); - return getTouchEventHandler(handlerTag, handlers); - }, [ - handlerTag, - config.onTouchesDown, - config.onTouchesMove, - config.onTouchesUp, - config.onTouchesCancel, - ]); -} 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 deleted file mode 100644 index 587d28b683..0000000000 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/js/useGestureUpdateEvent.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { prepareUpdateHandlers } from '../../utils'; -import { ReanimatedContext } from '../../../../handlers/gestures/reanimatedWrapper'; -import { getUpdateHandler } from '../updateHandler'; -import { BaseGestureConfig } from '../../../types'; -import { useMemo } from 'react'; - -export function useGestureUpdateEvent( - handlerTag: number, - config: BaseGestureConfig, - jsContext: ReanimatedContext -) { - return useMemo(() => { - const { handlers, changeEventCalculator } = prepareUpdateHandlers( - { - onUpdate: config.onUpdate, - }, - config.changeEventCalculator - ); - - return config.dispatchesAnimatedEvents - ? undefined - : getUpdateHandler( - handlerTag, - handlers, - jsContext, - changeEventCalculator - ); - }, [ - handlerTag, - config.onUpdate, - config.dispatchesAnimatedEvents, - config.changeEventCalculator, - jsContext, - ]); -} diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useGestureEventHandler.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useGestureEventHandler.ts new file mode 100644 index 0000000000..442b0207cf --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useGestureEventHandler.ts @@ -0,0 +1,39 @@ +import { ReanimatedContext } from '../../../handlers/gestures/reanimatedWrapper'; +import { + BaseGestureConfig, + GestureCallbacks, + GestureHandlerEventWithHandlerData, +} from '../../types'; +import { useMemo } from 'react'; +import { eventHandler } from './eventHandler'; + +export function useGestureEventHandler( + handlerTag: number, + handlers: GestureCallbacks, + config: BaseGestureConfig +) { + const jsContext: ReanimatedContext = useMemo(() => { + return { + lastUpdateEvent: undefined, + }; + }, []); + + return useMemo(() => { + return (event: GestureHandlerEventWithHandlerData) => { + eventHandler( + handlerTag, + event, + handlers, + config.changeEventCalculator, + jsContext, + !!config.dispatchesAnimatedEvents + ); + }; + }, [ + handlerTag, + handlers, + config.changeEventCalculator, + config.dispatchesAnimatedEvents, + jsContext, + ]); +} diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts index edfbbfdb3d..012d18d3dc 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { Reanimated, ReanimatedHandler, @@ -7,9 +8,12 @@ import { GestureCallbacks, UnpackedGestureHandlerEventWithHandlerData, } from '../../types'; -import { getStateChangeHandler } from './stateChangeHandler'; -import { getTouchEventHandler } from './touchEventHandler'; -import { getUpdateHandler } from './updateHandler'; +import { eventHandler } from './eventHandler'; + +const workletNOOP = () => { + 'worklet'; + // no-op +}; export function useReanimatedEventHandler( handlerTag: number, @@ -17,43 +21,34 @@ export function useReanimatedEventHandler( reanimatedHandler: ReanimatedHandler | undefined, changeEventCalculator: ChangeCalculatorType | undefined ) { - // We don't want to call hooks conditionally, `useEvent` will be always called. - // The only difference is whether we will send events to Reanimated or not. - // The problem here is that if someone passes `Animated.event` as `onUpdate` prop, - // it won't be workletized and therefore `useHandler` will throw. In that case we override it to empty `worklet`. - if (!Reanimated?.isWorkletFunction(handlers.onUpdate)) { - handlers.onUpdate = () => { - 'worklet'; - // no-op - }; - } - - const stateChangeCallback = getStateChangeHandler( - handlerTag, - handlers, - reanimatedHandler?.context - ); - - const updateCallback = getUpdateHandler( - handlerTag, - handlers, - reanimatedHandler?.context, - changeEventCalculator - ); + const workletizedHandlers = useMemo(() => { + // We don't want to call hooks conditionally, `useEvent` will be always called. + // The only difference is whether we will send events to Reanimated or not. + // The problem here is that if someone passes `Animated.event` as `onUpdate` prop, + // it won't be workletized and therefore `useHandler` will throw. In that case we override it to empty `worklet`. + if (!Reanimated?.isWorkletFunction(handlers.onUpdate)) { + return { + ...handlers, + onUpdate: workletNOOP, + }; + } - const touchCallback = getTouchEventHandler(handlerTag, handlers); + return handlers; + }, [handlers]); const callback = ( event: UnpackedGestureHandlerEventWithHandlerData ) => { 'worklet'; - if ('oldState' in event && event.oldState !== undefined) { - stateChangeCallback(event); - } else if ('allTouches' in event) { - touchCallback(event); - } else { - updateCallback(event); - } + eventHandler( + handlerTag, + event, + workletizedHandlers, + changeEventCalculator, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain + reanimatedHandler?.context!, + false + ); }; const reanimatedEvent = Reanimated?.useEvent( 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 4c6a920299..3f0bdbee05 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts @@ -33,24 +33,11 @@ export function useGesture( // TODO: Call only necessary hooks depending on which callbacks are defined (?) const { - onGestureHandlerStateChange, onGestureHandlerEvent, - onGestureHandlerTouchEvent, onReanimatedEvent, onGestureHandlerAnimatedEvent, } = useGestureCallbacks(tag, config); - // This should never happen, but since we don't want to call hooks conditionally, - // we have to mark these as possibly undefined to make TypeScript happy. - if ( - !onGestureHandlerStateChange || - // If onUpdate is an AnimatedEvent, `onGestureHandlerEvent` will be undefined and vice versa. - (!onGestureHandlerEvent && !onGestureHandlerAnimatedEvent) || - !onGestureHandlerTouchEvent - ) { - throw new Error(tagMessage('Failed to create event handlers.')); - } - if (config.shouldUseReanimatedDetector && !onReanimatedEvent) { throw new Error(tagMessage('Failed to create reanimated event handlers.')); } @@ -95,14 +82,14 @@ export function useGesture( }, [tag, config, type]); return useMemo( - () => ({ + (): SingleGesture => ({ tag, type, config, detectorCallbacks: { - onGestureHandlerStateChange, - onGestureHandlerEvent, - onGestureHandlerTouchEvent, + onGestureHandlerStateChange: onGestureHandlerEvent, + onGestureHandlerEvent: onGestureHandlerEvent, + onGestureHandlerTouchEvent: onGestureHandlerEvent, onGestureHandlerAnimatedEvent, // On web, we're triggering Reanimated callbacks ourselves, based on the type. // To handle this properly, we need to provide all three callbacks, so we set @@ -128,9 +115,7 @@ export function useGesture( tag, type, config, - onGestureHandlerStateChange, onGestureHandlerEvent, - onGestureHandlerTouchEvent, onReanimatedEvent, onGestureHandlerAnimatedEvent, gestureRelations, diff --git a/packages/react-native-gesture-handler/src/v3/hooks/useGestureCallbacks.ts b/packages/react-native-gesture-handler/src/v3/hooks/useGestureCallbacks.ts index 40f45bdb6b..4704b83999 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/useGestureCallbacks.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/useGestureCallbacks.ts @@ -1,6 +1,4 @@ -import { useGestureStateChangeEvent } from './callbacks/js/useGestureStateChangeEvent'; -import { useGestureUpdateEvent } from './callbacks/js/useGestureUpdateEvent'; -import { useGestureTouchEvent } from './callbacks/js/useGestureTouchEvent'; +import { useGestureEventHandler } from './callbacks/useGestureEventHandler'; import { AnimatedEvent, BaseGestureConfig, @@ -9,17 +7,11 @@ import { import { checkMappingForChangeProperties, isNativeAnimatedEvent, - prepareStateChangeHandlers, - prepareTouchHandlers, - prepareUpdateHandlers, + useMemoizedGestureCallbacks, } from './utils'; import { useReanimatedEventHandler } from './callbacks/useReanimatedEventHandler'; import { tagMessage } from '../../utils'; -import { - Reanimated, - ReanimatedContext, -} from '../../handlers/gestures/reanimatedWrapper'; -import { useMemo } from 'react'; +import { Reanimated } from '../../handlers/gestures/reanimatedWrapper'; function guardJSAnimatedEvent(handler: (...args: unknown[]) => void) { return (...args: unknown[]) => { @@ -48,38 +40,23 @@ export function useGestureCallbacks( handlerTag: number, config: BaseGestureConfig ) { - const jsContext: ReanimatedContext = useMemo(() => { - return { - lastUpdateEvent: undefined, - }; - }, []); + const callbacks = useMemoizedGestureCallbacks(config); - const onGestureHandlerStateChange = useGestureStateChangeEvent( - handlerTag, - config, - jsContext - ); - const onGestureHandlerEvent = useGestureUpdateEvent( + const onGestureHandlerEvent = useGestureEventHandler( handlerTag, - config, - jsContext + callbacks, + config ); - const onGestureHandlerTouchEvent = useGestureTouchEvent(handlerTag, config); let onReanimatedEvent; if (!config.disableReanimated) { - const handlers = { - ...prepareStateChangeHandlers(config), - ...prepareUpdateHandlers(config).handlers, - ...prepareTouchHandlers(config), - }; // eslint-disable-next-line react-hooks/rules-of-hooks - const reanimatedHandler = Reanimated?.useHandler(handlers); + const reanimatedHandler = Reanimated?.useHandler(callbacks); // eslint-disable-next-line react-hooks/rules-of-hooks onReanimatedEvent = useReanimatedEventHandler( handlerTag, - handlers, + callbacks, reanimatedHandler, config.changeEventCalculator ); @@ -104,9 +81,7 @@ export function useGestureCallbacks( } return { - onGestureHandlerStateChange, onGestureHandlerEvent, - onGestureHandlerTouchEvent, onReanimatedEvent, onGestureHandlerAnimatedEvent, }; 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 index ad3d006dbb..2d3fab7cf5 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/utils/eventHandlersUtils.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/eventHandlersUtils.ts @@ -1,60 +1,43 @@ +import { useMemo } from 'react'; import { TouchEventType } from '../../../TouchEventType'; import { CALLBACK_TYPE } from '../../../handlers/gestures/gesture'; -import { - ChangeCalculatorType, - GestureCallbacks, - UnpackedGestureHandlerEvent, -} from '../../types'; +import { GestureCallbacks, UnpackedGestureHandlerEvent } from '../../types'; -export function prepareStateChangeHandlers( +export function useMemoizedGestureCallbacks( callbacks: GestureCallbacks ): GestureCallbacks { - 'worklet'; - const { onBegin, onActivate, onDeactivate, onFinalize } = callbacks; - - const handlers: GestureCallbacks = { - ...(onBegin ? { onBegin } : {}), - ...(onActivate ? { onActivate } : {}), - ...(onDeactivate ? { onDeactivate } : {}), - ...(onFinalize ? { onFinalize } : {}), - }; - - return handlers; -} - -type UpdateHandlersReturnType = { - handlers: GestureCallbacks; - changeEventCalculator?: ChangeCalculatorType; -}; - -export function prepareUpdateHandlers( - callbacks: GestureCallbacks, - changeEventCalculator?: ChangeCalculatorType -): UpdateHandlersReturnType { - 'worklet'; - const { onUpdate } = callbacks; - - const handlers: GestureCallbacks = { - ...(onUpdate ? { onUpdate } : {}), - }; - - return { handlers, changeEventCalculator }; -} - -export function prepareTouchHandlers( - callbacks: GestureCallbacks -): GestureCallbacks { - const { onTouchesDown, onTouchesMove, onTouchesUp, onTouchesCancel } = - callbacks; - - const handlers: GestureCallbacks = { - ...(onTouchesDown ? { onTouchesDown } : {}), - ...(onTouchesMove ? { onTouchesMove } : {}), - ...(onTouchesUp ? { onTouchesUp } : {}), - ...(onTouchesCancel ? { onTouchesCancel } : {}), - }; - - return handlers; + return useMemo( + () => ({ + ...(callbacks.onBegin ? { onBegin: callbacks.onBegin } : {}), + ...(callbacks.onActivate ? { onActivate: callbacks.onActivate } : {}), + ...(callbacks.onDeactivate + ? { onDeactivate: callbacks.onDeactivate } + : {}), + ...(callbacks.onFinalize ? { onFinalize: callbacks.onFinalize } : {}), + ...(callbacks.onUpdate ? { onUpdate: callbacks.onUpdate } : {}), + ...(callbacks.onTouchesDown + ? { onTouchesDown: callbacks.onTouchesDown } + : {}), + ...(callbacks.onTouchesMove + ? { onTouchesMove: callbacks.onTouchesMove } + : {}), + ...(callbacks.onTouchesUp ? { onTouchesUp: callbacks.onTouchesUp } : {}), + ...(callbacks.onTouchesCancel + ? { onTouchesCancel: callbacks.onTouchesCancel } + : {}), + }), + [ + callbacks.onActivate, + callbacks.onBegin, + callbacks.onDeactivate, + callbacks.onFinalize, + callbacks.onTouchesCancel, + callbacks.onTouchesDown, + callbacks.onTouchesMove, + callbacks.onTouchesUp, + callbacks.onUpdate, + ] + ); } export function getHandler( diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/eventUtils.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/eventUtils.ts index 43ebdd6c72..98113996df 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/utils/eventUtils.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/eventUtils.ts @@ -25,7 +25,10 @@ export function isNativeEvent( export function maybeExtractNativeEvent( event: GestureHandlerEventWithHandlerData -) { +): + | GestureTouchEvent + | GestureUpdateEventWithHandlerData + | GestureStateChangeEventWithHandlerData { 'worklet'; return isNativeEvent(event) ? event.nativeEvent : event;