From ea0d25b213f5905c2244301d43000908e945a182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacy=20=C5=81=C4=85tka?= Date: Wed, 4 Dec 2024 13:33:07 +0100 Subject: [PATCH 001/236] Remove all `Hammer.JS` usages & references (#3229) Removed legacy `Hammer.JS` web implementation, all it's usages and references. Removed `enableLegacyWebImplementation`. Removed `isNewWebImplementationEnabled`. It's not abundantly clear at first, but it's an internal function. --- .../docs/fundamentals/installation.md | 10 +- .../react-native-gesture-handler/package.json | 1 - .../src/EnableNewWebImplementation.ts | 53 -- .../src/RNGestureHandlerModule.web.ts | 82 +-- .../gestures/GestureDetector/utils.ts | 9 +- .../react-native-gesture-handler/src/index.ts | 5 - .../src/web/Gestures.ts | 19 - .../src/web_hammer/DiscreteGestureHandler.ts | 82 --- .../src/web_hammer/DraggingGestureHandler.ts | 34 - .../src/web_hammer/Errors.ts | 7 - .../src/web_hammer/FlingGestureHandler.ts | 134 ---- .../src/web_hammer/GestureHandler.ts | 600 ------------------ .../web_hammer/IndiscreteGestureHandler.ts | 33 - .../src/web_hammer/LongPressGestureHandler.ts | 56 -- .../web_hammer/NativeViewGestureHandler.ts | 47 -- .../src/web_hammer/NodeManager.ts | 33 - .../src/web_hammer/PanGestureHandler.ts | 226 ------- .../src/web_hammer/PinchGestureHandler.ts | 25 - .../src/web_hammer/PressGestureHandler.ts | 170 ----- .../src/web_hammer/RotationGestureHandler.ts | 25 - .../src/web_hammer/TapGestureHandler.ts | 172 ----- .../src/web_hammer/constants.ts | 48 -- .../src/web_hammer/utils.ts | 24 - yarn.lock | 17 - 24 files changed, 27 insertions(+), 1885 deletions(-) delete mode 100644 packages/react-native-gesture-handler/src/EnableNewWebImplementation.ts delete mode 100644 packages/react-native-gesture-handler/src/web_hammer/DiscreteGestureHandler.ts delete mode 100644 packages/react-native-gesture-handler/src/web_hammer/DraggingGestureHandler.ts delete mode 100644 packages/react-native-gesture-handler/src/web_hammer/Errors.ts delete mode 100644 packages/react-native-gesture-handler/src/web_hammer/FlingGestureHandler.ts delete mode 100644 packages/react-native-gesture-handler/src/web_hammer/GestureHandler.ts delete mode 100644 packages/react-native-gesture-handler/src/web_hammer/IndiscreteGestureHandler.ts delete mode 100644 packages/react-native-gesture-handler/src/web_hammer/LongPressGestureHandler.ts delete mode 100644 packages/react-native-gesture-handler/src/web_hammer/NativeViewGestureHandler.ts delete mode 100644 packages/react-native-gesture-handler/src/web_hammer/NodeManager.ts delete mode 100644 packages/react-native-gesture-handler/src/web_hammer/PanGestureHandler.ts delete mode 100644 packages/react-native-gesture-handler/src/web_hammer/PinchGestureHandler.ts delete mode 100644 packages/react-native-gesture-handler/src/web_hammer/PressGestureHandler.ts delete mode 100644 packages/react-native-gesture-handler/src/web_hammer/RotationGestureHandler.ts delete mode 100644 packages/react-native-gesture-handler/src/web_hammer/TapGestureHandler.ts delete mode 100644 packages/react-native-gesture-handler/src/web_hammer/constants.ts delete mode 100644 packages/react-native-gesture-handler/src/web_hammer/utils.ts diff --git a/packages/docs-gesture-handler/docs/fundamentals/installation.md b/packages/docs-gesture-handler/docs/fundamentals/installation.md index df25dc2ff2..4bd3485355 100644 --- a/packages/docs-gesture-handler/docs/fundamentals/installation.md +++ b/packages/docs-gesture-handler/docs/fundamentals/installation.md @@ -121,15 +121,7 @@ cd ios && pod install && cd .. #### Web -There is no additional configuration required for the web, however, since the Gesture Handler 2.10.0 the new web implementation is enabled by default. We recommend you to check if the gestures in your app are working as expected since their behavior should now resemble the native platforms. If you don't want to use the new implementation, you can still revert back to the legacy one by enabling it at the beginning of your `index.js` file: - -```js -import { enableLegacyWebImplementation } from 'react-native-gesture-handler'; - -enableLegacyWebImplementation(true); -``` - -Nonetheless, it's recommended to adapt to the new implementation, as the legacy one will be dropped in the next major release of Gesture Handler. +There is no additional configuration required for the web. #### With [wix/react-native-navigation](https://github.com/wix/react-native-navigation) diff --git a/packages/react-native-gesture-handler/package.json b/packages/react-native-gesture-handler/package.json index 21e4ef2ef6..9cb308a533 100644 --- a/packages/react-native-gesture-handler/package.json +++ b/packages/react-native-gesture-handler/package.json @@ -70,7 +70,6 @@ }, "homepage": "https://github.com/software-mansion/react-native-gesture-handler#readme", "dependencies": { - "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, diff --git a/packages/react-native-gesture-handler/src/EnableNewWebImplementation.ts b/packages/react-native-gesture-handler/src/EnableNewWebImplementation.ts deleted file mode 100644 index c41099eaae..0000000000 --- a/packages/react-native-gesture-handler/src/EnableNewWebImplementation.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Platform } from 'react-native'; -import { tagMessage } from './utils'; - -let useNewWebImplementation = true; -let getWasCalled = false; - -/** - * @deprecated new web implementation is enabled by default. This function will be removed in Gesture Handler 3 - */ -export function enableExperimentalWebImplementation( - _shouldEnable = true -): void { - // NO-OP since the new implementation is now the default - console.warn( - tagMessage( - 'New web implementation is enabled by default. This function will be removed in Gesture Handler 3.' - ) - ); -} - -/** - * @deprecated legacy implementation is no longer supported. This function will be removed in Gesture Handler 3 - */ -export function enableLegacyWebImplementation( - shouldUseLegacyImplementation = true -): void { - console.warn( - tagMessage( - 'Legacy web implementation is deprecated. This function will be removed in Gesture Handler 3.' - ) - ); - - if ( - Platform.OS !== 'web' || - useNewWebImplementation === !shouldUseLegacyImplementation - ) { - return; - } - - if (getWasCalled) { - console.error( - 'Some parts of this application have already started using the new gesture handler implementation. No changes will be applied. You can try enabling legacy implementation earlier.' - ); - return; - } - - useNewWebImplementation = !shouldUseLegacyImplementation; -} - -export function isNewWebImplementationEnabled(): boolean { - getWasCalled = true; - return useNewWebImplementation; -} diff --git a/packages/react-native-gesture-handler/src/RNGestureHandlerModule.web.ts b/packages/react-native-gesture-handler/src/RNGestureHandlerModule.web.ts index a9584bad2d..35c1d611ca 100644 --- a/packages/react-native-gesture-handler/src/RNGestureHandlerModule.web.ts +++ b/packages/react-native-gesture-handler/src/RNGestureHandlerModule.web.ts @@ -1,12 +1,10 @@ import React from 'react'; import type { ActionType } from './ActionType'; -import { isNewWebImplementationEnabled } from './EnableNewWebImplementation'; -import { Gestures, HammerGestures } from './web/Gestures'; +import { Gestures } from './web/Gestures'; import type { Config } from './web/interfaces'; import InteractionManager from './web/tools/InteractionManager'; import NodeManager from './web/tools/NodeManager'; -import * as HammerNodeManager from './web_hammer/NodeManager'; import { GestureHandlerWebDelegate } from './web/tools/GestureHandlerWebDelegate'; // init method is called inside attachGestureHandler function. However, this function may @@ -28,36 +26,21 @@ export default { handlerTag: number, config: T ) { - if (isNewWebImplementationEnabled()) { - if (!(handlerName in Gestures)) { - throw new Error( - `react-native-gesture-handler: ${handlerName} is not supported on web.` - ); - } - - const GestureClass = Gestures[handlerName]; - NodeManager.createGestureHandler( - handlerTag, - new GestureClass(new GestureHandlerWebDelegate()) - ); - InteractionManager.instance.configureInteractions( - NodeManager.getHandler(handlerTag), - config as unknown as Config + if (!(handlerName in Gestures)) { + throw new Error( + `react-native-gesture-handler: ${handlerName} is not supported on web.` ); - } else { - if (!(handlerName in HammerGestures)) { - throw new Error( - `react-native-gesture-handler: ${handlerName} is not supported on web.` - ); - } - - // @ts-ignore If it doesn't exist, the error is thrown - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const GestureClass = HammerGestures[handlerName]; - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - HammerNodeManager.createGestureHandler(handlerTag, new GestureClass()); } + const GestureClass = Gestures[handlerName]; + NodeManager.createGestureHandler( + handlerTag, + new GestureClass(new GestureHandlerWebDelegate()) + ); + InteractionManager.instance.configureInteractions( + NodeManager.getHandler(handlerTag), + config as unknown as Config + ); this.updateGestureHandler(handlerTag, config as unknown as Config); }, attachGestureHandler( @@ -70,9 +53,7 @@ export default { if (!(newView instanceof Element || newView instanceof React.Component)) { shouldPreventDrop = true; - const handler = isNewWebImplementationEnabled() - ? NodeManager.getHandler(handlerTag) - : HammerNodeManager.getHandler(handlerTag); + const handler = NodeManager.getHandler(handlerTag); const handlerName = handler.constructor.name; @@ -81,43 +62,26 @@ export default { ); } - if (isNewWebImplementationEnabled()) { - // @ts-ignore Types should be HTMLElement or React.Component - NodeManager.getHandler(handlerTag).init(newView, propsRef); - } else { - // @ts-ignore Types should be HTMLElement or React.Component - HammerNodeManager.getHandler(handlerTag).setView(newView, propsRef); - } + // @ts-ignore Types should be HTMLElement or React.Component + NodeManager.getHandler(handlerTag).init(newView, propsRef); }, updateGestureHandler(handlerTag: number, newConfig: Config) { - if (isNewWebImplementationEnabled()) { - NodeManager.getHandler(handlerTag).updateGestureConfig(newConfig); + NodeManager.getHandler(handlerTag).updateGestureConfig(newConfig); - InteractionManager.instance.configureInteractions( - NodeManager.getHandler(handlerTag), - newConfig - ); - } else { - HammerNodeManager.getHandler(handlerTag).updateGestureConfig(newConfig); - } + InteractionManager.instance.configureInteractions( + NodeManager.getHandler(handlerTag), + newConfig + ); }, getGestureHandlerNode(handlerTag: number) { - if (isNewWebImplementationEnabled()) { - return NodeManager.getHandler(handlerTag); - } else { - return HammerNodeManager.getHandler(handlerTag); - } + return NodeManager.getHandler(handlerTag); }, dropGestureHandler(handlerTag: number) { if (shouldPreventDrop) { return; } - if (isNewWebImplementationEnabled()) { - NodeManager.dropGestureHandler(handlerTag); - } else { - HammerNodeManager.dropGestureHandler(handlerTag); - } + NodeManager.dropGestureHandler(handlerTag); }, // eslint-disable-next-line @typescript-eslint/no-empty-function flushOperations() {}, diff --git a/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/utils.ts b/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/utils.ts index 70c84bed4e..f63e7a41ab 100644 --- a/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/utils.ts +++ b/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/utils.ts @@ -17,7 +17,6 @@ import { HandlerStateChangeEvent, baseGestureHandlerWithDetectorProps, } from '../../gestureHandlerCommon'; -import { isNewWebImplementationEnabled } from '../../../EnableNewWebImplementation'; import { RNRenderer } from '../../../RNRenderer'; import { useCallback, useRef, useState } from 'react'; import { Reanimated } from '../reanimatedWrapper'; @@ -172,10 +171,8 @@ export function useWebEventHandlers() { onGestureHandlerEvent: (e: HandlerStateChangeEvent) => { onGestureHandlerEvent(e.nativeEvent); }, - onGestureHandlerStateChange: isNewWebImplementationEnabled() - ? (e: HandlerStateChangeEvent) => { - onGestureHandlerEvent(e.nativeEvent); - } - : undefined, + onGestureHandlerStateChange: (e: HandlerStateChangeEvent) => { + onGestureHandlerEvent(e.nativeEvent); + }, }); } diff --git a/packages/react-native-gesture-handler/src/index.ts b/packages/react-native-gesture-handler/src/index.ts index dcf7998e59..7ad400a7a7 100644 --- a/packages/react-native-gesture-handler/src/index.ts +++ b/packages/react-native-gesture-handler/src/index.ts @@ -161,9 +161,4 @@ export type { } from './components/DrawerLayout'; export { default as DrawerLayout } from './components/DrawerLayout'; -export { - enableExperimentalWebImplementation, - enableLegacyWebImplementation, -} from './EnableNewWebImplementation'; - initialize(); diff --git a/packages/react-native-gesture-handler/src/web/Gestures.ts b/packages/react-native-gesture-handler/src/web/Gestures.ts index 279b1152d1..fb57ba2f90 100644 --- a/packages/react-native-gesture-handler/src/web/Gestures.ts +++ b/packages/react-native-gesture-handler/src/web/Gestures.ts @@ -9,15 +9,6 @@ import NativeViewGestureHandler from './handlers/NativeViewGestureHandler'; import ManualGestureHandler from './handlers/ManualGestureHandler'; import HoverGestureHandler from './handlers/HoverGestureHandler'; -// Hammer Handlers -import HammerNativeViewGestureHandler from '../web_hammer/NativeViewGestureHandler'; -import HammerPanGestureHandler from '../web_hammer/PanGestureHandler'; -import HammerTapGestureHandler from '../web_hammer/TapGestureHandler'; -import HammerLongPressGestureHandler from '../web_hammer/LongPressGestureHandler'; -import HammerPinchGestureHandler from '../web_hammer/PinchGestureHandler'; -import HammerRotationGestureHandler from '../web_hammer/RotationGestureHandler'; -import HammerFlingGestureHandler from '../web_hammer/FlingGestureHandler'; - export const Gestures = { NativeViewGestureHandler, PanGestureHandler, @@ -29,13 +20,3 @@ export const Gestures = { ManualGestureHandler, HoverGestureHandler, }; - -export const HammerGestures = { - NativeViewGestureHandler: HammerNativeViewGestureHandler, - PanGestureHandler: HammerPanGestureHandler, - TapGestureHandler: HammerTapGestureHandler, - LongPressGestureHandler: HammerLongPressGestureHandler, - PinchGestureHandler: HammerPinchGestureHandler, - RotationGestureHandler: HammerRotationGestureHandler, - FlingGestureHandler: HammerFlingGestureHandler, -}; diff --git a/packages/react-native-gesture-handler/src/web_hammer/DiscreteGestureHandler.ts b/packages/react-native-gesture-handler/src/web_hammer/DiscreteGestureHandler.ts deleted file mode 100644 index d87937057a..0000000000 --- a/packages/react-native-gesture-handler/src/web_hammer/DiscreteGestureHandler.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* eslint-disable @eslint-community/eslint-comments/no-unlimited-disable */ -/* eslint-disable */ -import GestureHandler from './GestureHandler'; -import { TEST_MAX_IF_NOT_NAN } from './utils'; - -abstract class DiscreteGestureHandler extends GestureHandler { - get isDiscrete() { - return true; - } - - get shouldEnableGestureOnSetup() { - return true; - } - - shouldFailUnderCustomCriteria( - { x, y, deltaX, deltaY }: any, - { maxDeltaX, maxDeltaY, maxDistSq, shouldCancelWhenOutside }: any - ) { - if (shouldCancelWhenOutside) { - if (!this.isPointInView({ x, y })) { - return true; - } - } - return ( - TEST_MAX_IF_NOT_NAN(Math.abs(deltaX), maxDeltaX) || - TEST_MAX_IF_NOT_NAN(Math.abs(deltaY), maxDeltaY) || - TEST_MAX_IF_NOT_NAN( - Math.abs(deltaY * deltaY + deltaX * deltaX), - maxDistSq - ) - ); - } - - transformNativeEvent({ center: { x, y } }: any) { - // @ts-ignore FIXME(TS) - const rect = this.view!.getBoundingClientRect(); - - return { - absoluteX: x, - absoluteY: y, - x: x - rect.left, - y: y - rect.top, - }; - } - - isGestureEnabledForEvent( - { - minPointers, - maxPointers, - maxDeltaX, - maxDeltaY, - maxDistSq, - shouldCancelWhenOutside, - }: any, - _recognizer: any, - { maxPointers: pointerLength, center, deltaX, deltaY }: any - ) { - const validPointerCount = - pointerLength >= minPointers && pointerLength <= maxPointers; - - if ( - this.shouldFailUnderCustomCriteria( - { ...center, deltaX, deltaY }, - { - maxDeltaX, - maxDeltaY, - maxDistSq, - shouldCancelWhenOutside, - } - ) || - // A user probably won't land a multi-pointer tap on the first tick (so we cannot just cancel each time) - // but if the gesture is running and the user adds or subtracts another pointer then it should fail. - (!validPointerCount && this.isGestureRunning) - ) { - return { failed: true }; - } - - return { success: validPointerCount }; - } -} - -export default DiscreteGestureHandler; diff --git a/packages/react-native-gesture-handler/src/web_hammer/DraggingGestureHandler.ts b/packages/react-native-gesture-handler/src/web_hammer/DraggingGestureHandler.ts deleted file mode 100644 index 2b9424e16e..0000000000 --- a/packages/react-native-gesture-handler/src/web_hammer/DraggingGestureHandler.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* eslint-disable @eslint-community/eslint-comments/no-unlimited-disable */ -/* eslint-disable */ -import GestureHandler, { HammerInputExt } from './GestureHandler'; -import { PixelRatio } from 'react-native'; - -abstract class DraggingGestureHandler extends GestureHandler { - get shouldEnableGestureOnSetup() { - return true; - } - - transformNativeEvent({ - deltaX, - deltaY, - velocityX, - velocityY, - center: { x, y }, - }: HammerInputExt) { - // @ts-ignore FIXME(TS) - const rect = this.view!.getBoundingClientRect(); - const ratio = PixelRatio.get(); - return { - translationX: deltaX - (this.__initialX || 0), - translationY: deltaY - (this.__initialY || 0), - absoluteX: x, - absoluteY: y, - velocityX: velocityX * ratio, - velocityY: velocityY * ratio, - x: x - rect.left, - y: y - rect.top, - }; - } -} - -export default DraggingGestureHandler; diff --git a/packages/react-native-gesture-handler/src/web_hammer/Errors.ts b/packages/react-native-gesture-handler/src/web_hammer/Errors.ts deleted file mode 100644 index a0e1a52a98..0000000000 --- a/packages/react-native-gesture-handler/src/web_hammer/Errors.ts +++ /dev/null @@ -1,7 +0,0 @@ -export class GesturePropError extends Error { - constructor(name: string, value: unknown, expectedType: string) { - super( - `Invalid property \`${name}: ${value}\` expected \`${expectedType}\`` - ); - } -} diff --git a/packages/react-native-gesture-handler/src/web_hammer/FlingGestureHandler.ts b/packages/react-native-gesture-handler/src/web_hammer/FlingGestureHandler.ts deleted file mode 100644 index 107c61b545..0000000000 --- a/packages/react-native-gesture-handler/src/web_hammer/FlingGestureHandler.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* eslint-disable @eslint-community/eslint-comments/no-unlimited-disable */ -/* eslint-disable */ -import Hammer from '@egjs/hammerjs'; - -import { Direction } from './constants'; -import { GesturePropError } from './Errors'; -import DraggingGestureHandler from './DraggingGestureHandler'; -import { isnan } from './utils'; -import { HammerInputExt } from './GestureHandler'; - -class FlingGestureHandler extends DraggingGestureHandler { - get name() { - return 'swipe'; - } - - get NativeGestureClass() { - return Hammer.Swipe; - } - - onGestureActivated(event: HammerInputExt) { - this.sendEvent({ - ...event, - eventType: Hammer.INPUT_MOVE, - isFinal: false, - isFirst: true, - }); - this.isGestureRunning = false; - this.hasGestureFailed = false; - this.sendEvent({ - ...event, - eventType: Hammer.INPUT_END, - isFinal: true, - }); - } - - onRawEvent(ev: HammerInputExt) { - super.onRawEvent(ev); - if (this.hasGestureFailed) { - return; - } - // Hammer doesn't send a `cancel` event for taps. - // Manually fail the event. - if (ev.isFinal) { - setTimeout(() => { - if (this.isGestureRunning) { - this.cancelEvent(ev); - } - }); - } else if (!this.hasGestureFailed && !this.isGestureRunning) { - // Tap Gesture start event - const gesture = this.hammer!.get(this.name); - // @ts-ignore FIXME(TS) - if (gesture.options.enable(gesture, ev)) { - this.onStart(ev); - this.sendEvent(ev); - } - } - } - - getHammerConfig() { - return { - // @ts-ignore FIXME(TS) - pointers: this.config.numberOfPointers, - direction: this.getDirection(), - }; - } - - getTargetDirections(direction: number) { - const directions = []; - if (direction & Direction.RIGHT) { - directions.push(Hammer.DIRECTION_RIGHT); - } - if (direction & Direction.LEFT) { - directions.push(Hammer.DIRECTION_LEFT); - } - if (direction & Direction.UP) { - directions.push(Hammer.DIRECTION_UP); - } - if (direction & Direction.DOWN) { - directions.push(Hammer.DIRECTION_DOWN); - } - // const hammerDirection = directions.reduce((a, b) => a | b, 0); - return directions; - } - - getDirection() { - // @ts-ignore FIXME(TS) - const { direction } = this.getConfig(); - - let directions = []; - if (direction & Direction.RIGHT) { - directions.push(Hammer.DIRECTION_HORIZONTAL); - } - if (direction & Direction.LEFT) { - directions.push(Hammer.DIRECTION_HORIZONTAL); - } - if (direction & Direction.UP) { - directions.push(Hammer.DIRECTION_VERTICAL); - } - if (direction & Direction.DOWN) { - directions.push(Hammer.DIRECTION_VERTICAL); - } - directions = [...new Set(directions)]; - - if (directions.length === 0) return Hammer.DIRECTION_NONE; - if (directions.length === 1) return directions[0]; - return Hammer.DIRECTION_ALL; - } - - isGestureEnabledForEvent( - { numberOfPointers }: any, - _recognizer: any, - { maxPointers: pointerLength }: any - ) { - const validPointerCount = pointerLength === numberOfPointers; - if (!validPointerCount && this.isGestureRunning) { - return { failed: true }; - } - return { success: validPointerCount }; - } - - updateGestureConfig({ numberOfPointers = 1, direction, ...props }: any) { - if (isnan(direction) || typeof direction !== 'number') { - throw new GesturePropError('direction', direction, 'number'); - } - return super.updateGestureConfig({ - numberOfPointers, - direction, - ...props, - }); - } -} - -export default FlingGestureHandler; diff --git a/packages/react-native-gesture-handler/src/web_hammer/GestureHandler.ts b/packages/react-native-gesture-handler/src/web_hammer/GestureHandler.ts deleted file mode 100644 index e55d28beb1..0000000000 --- a/packages/react-native-gesture-handler/src/web_hammer/GestureHandler.ts +++ /dev/null @@ -1,600 +0,0 @@ -/* eslint-disable @eslint-community/eslint-comments/no-unlimited-disable */ -/* eslint-disable */ -import Hammer from '@egjs/hammerjs'; -import { findNodeHandle } from 'react-native'; - -import { State } from '../State'; -import { EventMap } from './constants'; -import * as NodeManager from './NodeManager'; -import { ghQueueMicrotask } from '../ghQueueMicrotask'; - -// TODO(TS) Replace with HammerInput if https://github.com/DefinitelyTyped/DefinitelyTyped/pull/50438/files is merged -export type HammerInputExt = Omit; - -export type Config = Partial<{ - enabled: boolean; - minPointers: number; - maxPointers: number; - minDist: number; - minDistSq: number; - minVelocity: number; - minVelocitySq: number; - maxDist: number; - maxDistSq: number; - failOffsetXStart: number; - failOffsetYStart: number; - failOffsetXEnd: number; - failOffsetYEnd: number; - activeOffsetXStart: number; - activeOffsetXEnd: number; - activeOffsetYStart: number; - activeOffsetYEnd: number; - waitFor: any[] | null; - simultaneousHandlers: any[] | null; -}>; - -type NativeEvent = ReturnType; - -let gestureInstances = 0; - -abstract class GestureHandler { - public handlerTag: any; - public isGestureRunning = false; - public view: number | null = null; - protected hasCustomActivationCriteria: boolean; - protected hasGestureFailed = false; - protected hammer: HammerManager | null = null; - protected initialRotation: number | null = null; - protected __initialX: any; - protected __initialY: any; - protected config: Config = {}; - protected previousState: State = State.UNDETERMINED; - private pendingGestures: Record = {}; - private oldState: State = State.UNDETERMINED; - private lastSentState: State | null = null; - private gestureInstance: number; - private _stillWaiting: any; - private propsRef: any; - private ref: any; - - abstract get name(): string; - - get id() { - return `${this.name}${this.gestureInstance}`; - } - - // a simple way to check if GestureHandler is NativeViewGestureHandler, since importing it - // here to use instanceof would cause import cycle - get isNative() { - return false; - } - - get isDiscrete() { - return false; - } - - get shouldEnableGestureOnSetup(): boolean { - throw new Error('Must override GestureHandler.shouldEnableGestureOnSetup'); - } - - constructor() { - this.gestureInstance = gestureInstances++; - this.hasCustomActivationCriteria = false; - } - - getConfig() { - return this.config; - } - - onWaitingEnded(_gesture: this) {} - - removePendingGesture(id: string) { - delete this.pendingGestures[id]; - } - - addPendingGesture(gesture: this) { - this.pendingGestures[gesture.id] = gesture; - } - - isGestureEnabledForEvent( - _config: any, - _recognizer: any, - _event: any - ): { failed?: boolean; success?: boolean } { - return { success: true }; - } - - get NativeGestureClass(): RecognizerStatic { - throw new Error('Must override GestureHandler.NativeGestureClass'); - } - - updateHasCustomActivationCriteria(_config: Config) { - return true; - } - - clearSelfAsPending = () => { - if (Array.isArray(this.config.waitFor)) { - for (const gesture of this.config.waitFor) { - gesture.removePendingGesture(this.id); - } - } - }; - - updateGestureConfig({ enabled = true, ...props }) { - this.clearSelfAsPending(); - - this.config = this.ensureConfig({ enabled, ...props }); - this.hasCustomActivationCriteria = this.updateHasCustomActivationCriteria( - this.config - ); - if (Array.isArray(this.config.waitFor)) { - for (const gesture of this.config.waitFor) { - gesture.addPendingGesture(this); - } - } - - if (this.hammer) { - this.sync(); - } - return this.config; - } - - destroy = () => { - this.clearSelfAsPending(); - - if (this.hammer) { - this.hammer.stop(false); - this.hammer.destroy(); - } - this.hammer = null; - }; - - isPointInView = ({ x, y }: { x: number; y: number }) => { - // @ts-ignore FIXME(TS) - const rect = this.view!.getBoundingClientRect(); - const pointerInside = - x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom; - return pointerInside; - }; - - getState(type: keyof typeof EventMap): State { - // @ts-ignore TODO(TS) check if this is needed - if (type == 0) { - return 0; - } - return EventMap[type]; - } - - transformEventData(event: HammerInputExt) { - const { eventType, maxPointers: numberOfPointers } = event; - // const direction = DirectionMap[ev.direction]; - const changedTouch = event.changedPointers[0]; - const pointerInside = this.isPointInView({ - x: changedTouch.clientX, - y: changedTouch.clientY, - }); - - // TODO(TS) Remove cast after https://github.com/DefinitelyTyped/DefinitelyTyped/pull/50966 is merged. - const state = this.getState(eventType as 1 | 2 | 4 | 8); - if (state !== this.previousState) { - this.oldState = this.previousState; - this.previousState = state; - } - - return { - nativeEvent: { - numberOfPointers, - state, - pointerInside, - ...this.transformNativeEvent(event), - // onHandlerStateChange only - handlerTag: this.handlerTag, - target: this.ref, - // send oldState only when the state was changed, or is different than ACTIVE - // GestureDetector relies on the presence of `oldState` to differentiate between - // update events and state change events - oldState: - state !== this.previousState || state != 4 - ? this.oldState - : undefined, - }, - timeStamp: Date.now(), - }; - } - - transformNativeEvent(_event: HammerInputExt) { - return {}; - } - - sendEvent = (nativeEvent: HammerInputExt) => { - const { onGestureHandlerEvent, onGestureHandlerStateChange } = - this.propsRef.current; - - const event = this.transformEventData(nativeEvent); - - invokeNullableMethod(onGestureHandlerEvent, event); - if (this.lastSentState !== event.nativeEvent.state) { - this.lastSentState = event.nativeEvent.state as State; - invokeNullableMethod(onGestureHandlerStateChange, event); - } - }; - - cancelPendingGestures(event: HammerInputExt) { - for (const gesture of Object.values(this.pendingGestures)) { - if (gesture && gesture.isGestureRunning) { - gesture.hasGestureFailed = true; - gesture.cancelEvent(event); - } - } - } - - notifyPendingGestures() { - for (const gesture of Object.values(this.pendingGestures)) { - if (gesture) { - gesture.onWaitingEnded(this); - } - } - } - - // FIXME event is undefined in runtime when firstly invoked (see Draggable example), check other functions taking event as input - onGestureEnded(event: HammerInputExt) { - this.isGestureRunning = false; - this.cancelPendingGestures(event); - } - - forceInvalidate(event: HammerInputExt) { - if (this.isGestureRunning) { - this.hasGestureFailed = true; - this.cancelEvent(event); - } - } - - cancelEvent(event: HammerInputExt) { - this.notifyPendingGestures(); - this.sendEvent({ - ...event, - eventType: Hammer.INPUT_CANCEL, - isFinal: true, - }); - this.onGestureEnded(event); - } - - onRawEvent({ isFirst }: HammerInputExt) { - if (isFirst) { - this.hasGestureFailed = false; - } - } - - shouldUseTouchEvents(config: Config) { - return ( - config.simultaneousHandlers?.some((handler) => handler.isNative) ?? false - ); - } - - setView(ref: Parameters['0'], propsRef: any) { - if (ref == null) { - this.destroy(); - this.view = null; - return; - } - - // @ts-ignore window doesn't exist on global type as we don't want to use Node types - const SUPPORTS_TOUCH = 'ontouchstart' in window; - this.propsRef = propsRef; - this.ref = ref; - - // @ts-ignore - this.view = findNodeHandle(ref); - - // When the browser starts handling the gesture (e.g. scrolling), it sends a pointercancel event and stops - // sending additional pointer events. This is not the case with touch events, so if the gesture is simultaneous - // with a NativeGestureHandler, we need to check if touch events are supported and use them if possible. - this.hammer = - SUPPORTS_TOUCH && this.shouldUseTouchEvents(this.config) - ? new Hammer.Manager(this.view as any, { - inputClass: Hammer.TouchInput, - }) - : new Hammer.Manager(this.view as any); - - this.oldState = State.UNDETERMINED; - this.previousState = State.UNDETERMINED; - this.lastSentState = null; - - const { NativeGestureClass } = this; - // @ts-ignore TODO(TS) - const gesture = new NativeGestureClass(this.getHammerConfig()); - this.hammer.add(gesture); - - this.hammer.on('hammer.input', (ev: HammerInput) => { - if (!this.config.enabled) { - this.hasGestureFailed = false; - this.isGestureRunning = false; - return; - } - - this.onRawEvent(ev as unknown as HammerInputExt); - - // TODO: Bacon: Check against something other than null - // The isFirst value is not called when the first rotation is calculated. - if (this.initialRotation === null && ev.rotation !== 0) { - this.initialRotation = ev.rotation; - } - if (ev.isFinal) { - // in favor of a willFail otherwise the last frame of the gesture will be captured. - setTimeout(() => { - this.initialRotation = null; - this.hasGestureFailed = false; - }); - } - }); - - this.setupEvents(); - this.sync(); - } - - setupEvents() { - // TODO(TS) Hammer types aren't exactly that what we get in runtime - if (!this.isDiscrete) { - this.hammer!.on(`${this.name}start`, (event: HammerInput) => - this.onStart(event as unknown as HammerInputExt) - ); - this.hammer!.on( - `${this.name}end ${this.name}cancel`, - (event: HammerInput) => { - this.onGestureEnded(event as unknown as HammerInputExt); - } - ); - } - this.hammer!.on(this.name, (ev: HammerInput) => - this.onGestureActivated(ev as unknown as HammerInputExt) - ); // TODO(TS) remove cast after https://github.com/DefinitelyTyped/DefinitelyTyped/pull/50438 is merged - } - - onStart({ deltaX, deltaY, rotation }: HammerInputExt) { - // Reset the state for the next gesture - this.oldState = State.UNDETERMINED; - this.previousState = State.UNDETERMINED; - this.lastSentState = null; - - this.isGestureRunning = true; - this.__initialX = deltaX; - this.__initialY = deltaY; - this.initialRotation = rotation; - } - - onGestureActivated(ev: HammerInputExt) { - this.sendEvent(ev); - } - - onSuccess() {} - - _getPendingGestures() { - if (Array.isArray(this.config.waitFor) && this.config.waitFor.length) { - // Get the list of gestures that this gesture is still waiting for. - // Use `=== false` in case a ref that isn't a gesture handler is used. - const stillWaiting = this.config.waitFor.filter( - ({ hasGestureFailed }) => hasGestureFailed === false - ); - return stillWaiting; - } - return []; - } - - getHammerConfig() { - const pointers = - this.config.minPointers === this.config.maxPointers - ? this.config.minPointers - : 0; - return { - pointers, - }; - } - - sync = () => { - const gesture = this.hammer!.get(this.name); - if (!gesture) return; - - const enable = (recognizer: any, inputData: any) => { - if (!this.config.enabled) { - this.isGestureRunning = false; - this.hasGestureFailed = false; - return false; - } - - // Prevent events before the system is ready. - if ( - !inputData || - !recognizer.options || - typeof inputData.maxPointers === 'undefined' - ) { - return this.shouldEnableGestureOnSetup; - } - - if (this.hasGestureFailed) { - return false; - } - - if (!this.isDiscrete) { - if (this.isGestureRunning) { - return true; - } - // The built-in hammer.js "waitFor" doesn't work across multiple views. - // Only process if there are views to wait for. - this._stillWaiting = this._getPendingGestures(); - // This gesture should continue waiting. - if (this._stillWaiting.length) { - // Check to see if one of the gestures you're waiting for has started. - // If it has then the gesture should fail. - for (const gesture of this._stillWaiting) { - // When the target gesture has started, this gesture must force fail. - if (!gesture.isDiscrete && gesture.isGestureRunning) { - this.hasGestureFailed = true; - this.isGestureRunning = false; - return false; - } - } - // This gesture shouldn't start until the others have finished. - return false; - } - } - - // Use default behaviour - if (!this.hasCustomActivationCriteria) { - return true; - } - - const deltaRotation = - this.initialRotation == null - ? 0 - : inputData.rotation - this.initialRotation; - // @ts-ignore FIXME(TS) - const { success, failed } = this.isGestureEnabledForEvent( - this.getConfig(), - recognizer, - { - ...inputData, - deltaRotation, - } - ); - - if (failed) { - this.simulateCancelEvent(inputData); - this.hasGestureFailed = true; - } - return success; - }; - - const params = this.getHammerConfig(); - // @ts-ignore FIXME(TS) - gesture.set({ ...params, enable }); - }; - - simulateCancelEvent(_inputData: any) {} - - // Validate the props - ensureConfig(config: Config): Required { - const props = { ...config }; - - // TODO(TS) We use ! to assert that if property is present then value is not empty (null, undefined) - if ('minDist' in config) { - props.minDist = config.minDist; - props.minDistSq = props.minDist! * props.minDist!; - } - if ('minVelocity' in config) { - props.minVelocity = config.minVelocity; - props.minVelocitySq = props.minVelocity! * props.minVelocity!; - } - if ('maxDist' in config) { - props.maxDist = config.maxDist; - props.maxDistSq = config.maxDist! * config.maxDist!; - } - if ('waitFor' in config) { - props.waitFor = asArray(config.waitFor) - .map(({ handlerTag }: { handlerTag: number }) => - NodeManager.getHandler(handlerTag) - ) - .filter((v) => v); - } else { - props.waitFor = null; - } - if ('simultaneousHandlers' in config) { - const shouldUseTouchEvents = this.shouldUseTouchEvents(this.config); - props.simultaneousHandlers = asArray(config.simultaneousHandlers) - .map((handler: number | GestureHandler) => { - if (typeof handler === 'number') { - return NodeManager.getHandler(handler); - } else { - return NodeManager.getHandler(handler.handlerTag); - } - }) - .filter((v) => v); - - if (shouldUseTouchEvents !== this.shouldUseTouchEvents(props)) { - ghQueueMicrotask(() => { - // if the undelying event API needs to be changed, we need to unmount and mount - // the hammer instance again. - this.destroy(); - this.setView(this.ref, this.propsRef); - }); - } - } else { - props.simultaneousHandlers = null; - } - - const configProps = [ - 'minPointers', - 'maxPointers', - 'minDist', - 'maxDist', - 'maxDistSq', - 'minVelocitySq', - 'minDistSq', - 'minVelocity', - 'failOffsetXStart', - 'failOffsetYStart', - 'failOffsetXEnd', - 'failOffsetYEnd', - 'activeOffsetXStart', - 'activeOffsetXEnd', - 'activeOffsetYStart', - 'activeOffsetYEnd', - ] as const; - configProps.forEach((prop: (typeof configProps)[number]) => { - if (typeof props[prop] === 'undefined') { - props[prop] = Number.NaN; - } - }); - return props as Required; // TODO(TS) how to convince TS that props are filled? - } -} - -// TODO(TS) investigate this method -// Used for sending data to a callback or AnimatedEvent -function invokeNullableMethod( - method: - | ((event: NativeEvent) => void) - | { __getHandler: () => (event: NativeEvent) => void } - | { __nodeConfig: { argMapping: any } }, - event: NativeEvent -) { - if (method) { - if (typeof method === 'function') { - method(event); - } else { - // For use with reanimated's AnimatedEvent - if ( - '__getHandler' in method && - typeof method.__getHandler === 'function' - ) { - const handler = method.__getHandler(); - invokeNullableMethod(handler, event); - } else { - if ('__nodeConfig' in method) { - const { argMapping } = method.__nodeConfig; - if (Array.isArray(argMapping)) { - for (const [index, [key, value]] of argMapping.entries()) { - if (key in event.nativeEvent) { - // @ts-ignore fix method type - const nativeValue = event.nativeEvent[key]; - if (value && value.setValue) { - // Reanimated API - value.setValue(nativeValue); - } else { - // RN Animated API - method.__nodeConfig.argMapping[index] = [key, nativeValue]; - } - } - } - } - } - } - } - } -} - -function asArray(value: T | T[]) { - // TODO(TS) use config.waitFor type - return value == null ? [] : Array.isArray(value) ? value : [value]; -} - -export default GestureHandler; diff --git a/packages/react-native-gesture-handler/src/web_hammer/IndiscreteGestureHandler.ts b/packages/react-native-gesture-handler/src/web_hammer/IndiscreteGestureHandler.ts deleted file mode 100644 index d63ba2fee8..0000000000 --- a/packages/react-native-gesture-handler/src/web_hammer/IndiscreteGestureHandler.ts +++ /dev/null @@ -1,33 +0,0 @@ -import GestureHandler from './GestureHandler'; - -/** - * The base class for **Rotation** and **Pinch** gesture handlers. - */ -abstract class IndiscreteGestureHandler extends GestureHandler { - get shouldEnableGestureOnSetup() { - return false; - } - - updateGestureConfig({ minPointers = 2, maxPointers = 2, ...props }) { - return super.updateGestureConfig({ - minPointers, - maxPointers, - ...props, - }); - } - - isGestureEnabledForEvent( - { minPointers, maxPointers }: any, - _recognizer: any, - { maxPointers: pointerLength }: any - ) { - if (pointerLength > maxPointers) { - return { failed: true }; - } - const validPointerCount = pointerLength >= minPointers; - return { - success: validPointerCount, - }; - } -} -export default IndiscreteGestureHandler; diff --git a/packages/react-native-gesture-handler/src/web_hammer/LongPressGestureHandler.ts b/packages/react-native-gesture-handler/src/web_hammer/LongPressGestureHandler.ts deleted file mode 100644 index 733274c00b..0000000000 --- a/packages/react-native-gesture-handler/src/web_hammer/LongPressGestureHandler.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* eslint-disable @eslint-community/eslint-comments/no-unlimited-disable */ -/* eslint-disable */ -import Hammer from '@egjs/hammerjs'; - -import { State } from '../State'; -import PressGestureHandler from './PressGestureHandler'; -import { isnan, isValidNumber } from './utils'; -import { Config } from './GestureHandler'; -import { HammerInputNames } from './constants'; - -class LongPressGestureHandler extends PressGestureHandler { - get minDurationMs(): number { - // @ts-ignore FIXNE(TS) - return isnan(this.config.minDurationMs) ? 251 : this.config.minDurationMs; - } - - get maxDist() { - // @ts-ignore FIXNE(TS) - return isnan(this.config.maxDist) ? 9 : this.config.maxDist; - } - - updateHasCustomActivationCriteria({ maxDistSq }: Config) { - return !isValidNumber(maxDistSq); - } - - getConfig() { - if (!this.hasCustomActivationCriteria) { - // Default config - // If no params have been defined then this config should emulate the native gesture as closely as possible. - return { - shouldCancelWhenOutside: true, - maxDistSq: 10, - }; - } - return this.config; - } - - getHammerConfig() { - return { - ...super.getHammerConfig(), - // threshold: this.maxDist, - time: this.minDurationMs, - }; - } - - getState(type: keyof typeof HammerInputNames) { - return { - [Hammer.INPUT_START]: State.ACTIVE, - [Hammer.INPUT_MOVE]: State.ACTIVE, - [Hammer.INPUT_END]: State.END, - [Hammer.INPUT_CANCEL]: State.FAILED, - }[type]; - } -} - -export default LongPressGestureHandler; diff --git a/packages/react-native-gesture-handler/src/web_hammer/NativeViewGestureHandler.ts b/packages/react-native-gesture-handler/src/web_hammer/NativeViewGestureHandler.ts deleted file mode 100644 index 7deb5bddc4..0000000000 --- a/packages/react-native-gesture-handler/src/web_hammer/NativeViewGestureHandler.ts +++ /dev/null @@ -1,47 +0,0 @@ -import DiscreteGestureHandler from './DiscreteGestureHandler'; -import { HammerInputExt } from './GestureHandler'; -import * as NodeManager from './NodeManager'; -import PressGestureHandler from './PressGestureHandler'; -import { TEST_MIN_IF_NOT_NAN, VEC_LEN_SQ } from './utils'; - -class NativeViewGestureHandler extends PressGestureHandler { - get isNative() { - return true; - } - - onRawEvent(ev: HammerInputExt) { - super.onRawEvent(ev); - if (!ev.isFinal) { - // if (this.ref instanceof ScrollView) { - if (TEST_MIN_IF_NOT_NAN(VEC_LEN_SQ({ x: ev.deltaX, y: ev.deltaY }), 10)) { - // @ts-ignore FIXME(TS) config type - if (this.config.disallowInterruption) { - const gestures = Object.values(NodeManager.getNodes()).filter( - (gesture) => { - const { handlerTag, view, isGestureRunning } = gesture; - return ( - // Check if this gesture isn't self - handlerTag !== this.handlerTag && - // Ensure the gesture needs to be cancelled - isGestureRunning && - // ScrollView can cancel discrete gestures like taps and presses - gesture instanceof DiscreteGestureHandler && - // Ensure a view exists and is a child of the current view - view && - // @ts-ignore FIXME(TS) view type - this.view.contains(view) - ); - } - ); - // Cancel all of the gestures that passed the filter - for (const gesture of gestures) { - // TODO: Bacon: Send some cached event. - gesture.forceInvalidate(ev); - } - } - } - } - } -} - -export default NativeViewGestureHandler; diff --git a/packages/react-native-gesture-handler/src/web_hammer/NodeManager.ts b/packages/react-native-gesture-handler/src/web_hammer/NodeManager.ts deleted file mode 100644 index f5ec70a43b..0000000000 --- a/packages/react-native-gesture-handler/src/web_hammer/NodeManager.ts +++ /dev/null @@ -1,33 +0,0 @@ -export const gestures: Record = {}; - -export function getHandler(tag: number) { - if (tag in gestures) { - return gestures[tag]; - } - - throw new Error(`No handler for tag ${tag}`); -} - -export function createGestureHandler(handlerTag: number, handler: any) { - if (handlerTag in gestures) { - throw new Error(`Handler with tag ${handlerTag} already exists`); - } - gestures[handlerTag] = handler; - // @ts-ignore no types for web handlers yet - gestures[handlerTag].handlerTag = handlerTag; -} - -export function dropGestureHandler(handlerTag: number) { - // Since React 18, there are cases where componentWillUnmount gets called twice in a row - // so skip this if the tag was already removed. - if (!(handlerTag in gestures)) { - return; - } - getHandler(handlerTag).destroy(); - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete gestures[handlerTag]; -} - -export function getNodes() { - return { ...gestures }; -} diff --git a/packages/react-native-gesture-handler/src/web_hammer/PanGestureHandler.ts b/packages/react-native-gesture-handler/src/web_hammer/PanGestureHandler.ts deleted file mode 100644 index 4a7e17da88..0000000000 --- a/packages/react-native-gesture-handler/src/web_hammer/PanGestureHandler.ts +++ /dev/null @@ -1,226 +0,0 @@ -import Hammer from '@egjs/hammerjs'; - -import { - EventMap, - MULTI_FINGER_PAN_MAX_PINCH_THRESHOLD, - MULTI_FINGER_PAN_MAX_ROTATION_THRESHOLD, -} from './constants'; -import DraggingGestureHandler from './DraggingGestureHandler'; -import { isValidNumber, isnan, TEST_MIN_IF_NOT_NAN, VEC_LEN_SQ } from './utils'; -import { State } from '../State'; - -import { Config, HammerInputExt } from './GestureHandler'; -class PanGestureHandler extends DraggingGestureHandler { - get name() { - return 'pan'; - } - - get NativeGestureClass() { - return Hammer.Pan; - } - - getHammerConfig() { - return { - ...super.getHammerConfig(), - direction: this.getDirection(), - }; - } - - getState(type: keyof typeof EventMap) { - const nextState = super.getState(type); - // Ensure that the first state sent is `BEGAN` and not `ACTIVE` - if ( - this.previousState === State.UNDETERMINED && - nextState === State.ACTIVE - ) { - return State.BEGAN; - } - return nextState; - } - - getDirection() { - const config = this.getConfig(); - const { - activeOffsetXStart, - activeOffsetXEnd, - activeOffsetYStart, - activeOffsetYEnd, - minDist, - } = config; - let directions: number[] = []; - let horizontalDirections = []; - - if (!isnan(minDist)) { - return Hammer.DIRECTION_ALL; - } - - if (!isnan(activeOffsetXStart)) { - horizontalDirections.push(Hammer.DIRECTION_LEFT); - } - if (!isnan(activeOffsetXEnd)) { - horizontalDirections.push(Hammer.DIRECTION_RIGHT); - } - if (horizontalDirections.length === 2) { - horizontalDirections = [Hammer.DIRECTION_HORIZONTAL]; - } - - directions = directions.concat(horizontalDirections); - let verticalDirections = []; - - if (!isnan(activeOffsetYStart)) { - verticalDirections.push(Hammer.DIRECTION_UP); - } - if (!isnan(activeOffsetYEnd)) { - verticalDirections.push(Hammer.DIRECTION_DOWN); - } - - if (verticalDirections.length === 2) { - verticalDirections = [Hammer.DIRECTION_VERTICAL]; - } - - directions = directions.concat(verticalDirections); - - if (!directions.length) { - return Hammer.DIRECTION_NONE; - } - if ( - directions[0] === Hammer.DIRECTION_HORIZONTAL && - directions[1] === Hammer.DIRECTION_VERTICAL - ) { - return Hammer.DIRECTION_ALL; - } - if (horizontalDirections.length && verticalDirections.length) { - return Hammer.DIRECTION_ALL; - } - - return directions[0]; - } - - getConfig() { - if (!this.hasCustomActivationCriteria) { - // Default config - // If no params have been defined then this config should emulate the native gesture as closely as possible. - return { - minDistSq: 10, - }; - } - return this.config; - } - - shouldFailUnderCustomCriteria( - { deltaX, deltaY }: HammerInputExt, - criteria: any - ) { - return ( - (!isnan(criteria.failOffsetXStart) && - deltaX < criteria.failOffsetXStart) || - (!isnan(criteria.failOffsetXEnd) && deltaX > criteria.failOffsetXEnd) || - (!isnan(criteria.failOffsetYStart) && - deltaY < criteria.failOffsetYStart) || - (!isnan(criteria.failOffsetYEnd) && deltaY > criteria.failOffsetYEnd) - ); - } - - shouldActivateUnderCustomCriteria( - { deltaX, deltaY, velocity }: any, - criteria: any - ) { - return ( - (!isnan(criteria.activeOffsetXStart) && - deltaX < criteria.activeOffsetXStart) || - (!isnan(criteria.activeOffsetXEnd) && - deltaX > criteria.activeOffsetXEnd) || - (!isnan(criteria.activeOffsetYStart) && - deltaY < criteria.activeOffsetYStart) || - (!isnan(criteria.activeOffsetYEnd) && - deltaY > criteria.activeOffsetYEnd) || - TEST_MIN_IF_NOT_NAN( - VEC_LEN_SQ({ x: deltaX, y: deltaY }), - criteria.minDistSq - ) || - TEST_MIN_IF_NOT_NAN(velocity.x, criteria.minVelocityX) || - TEST_MIN_IF_NOT_NAN(velocity.y, criteria.minVelocityY) || - TEST_MIN_IF_NOT_NAN(VEC_LEN_SQ(velocity), criteria.minVelocitySq) - ); - } - - shouldMultiFingerPanFail({ - pointerLength, - scale, - deltaRotation, - }: { - deltaRotation: number; - pointerLength: number; - scale: number; - }) { - if (pointerLength <= 1) { - return false; - } - - // Test if the pan had too much pinching or rotating. - const deltaScale = Math.abs(scale - 1); - const absDeltaRotation = Math.abs(deltaRotation); - if (deltaScale > MULTI_FINGER_PAN_MAX_PINCH_THRESHOLD) { - // > If the threshold doesn't seem right. - // You can log the value which it failed at here: - return true; - } - if (absDeltaRotation > MULTI_FINGER_PAN_MAX_ROTATION_THRESHOLD) { - // > If the threshold doesn't seem right. - // You can log the value which it failed at here: - return true; - } - - return false; - } - - updateHasCustomActivationCriteria( - criteria: Config & { minVelocityX?: number; minVelocityY?: number } - ) { - return ( - isValidNumber(criteria.minDistSq) || - isValidNumber(criteria.minVelocityX) || - isValidNumber(criteria.minVelocityY) || - isValidNumber(criteria.minVelocitySq) || - isValidNumber(criteria.activeOffsetXStart) || - isValidNumber(criteria.activeOffsetXEnd) || - isValidNumber(criteria.activeOffsetYStart) || - isValidNumber(criteria.activeOffsetYEnd) - ); - } - - isGestureEnabledForEvent( - props: any, - _recognizer: any, - inputData: HammerInputExt & { deltaRotation: number } - ) { - if (this.shouldFailUnderCustomCriteria(inputData, props)) { - return { failed: true }; - } - - const velocity = { x: inputData.velocityX, y: inputData.velocityY }; - if ( - this.hasCustomActivationCriteria && - this.shouldActivateUnderCustomCriteria( - { deltaX: inputData.deltaX, deltaY: inputData.deltaY, velocity }, - props - ) - ) { - if ( - this.shouldMultiFingerPanFail({ - pointerLength: inputData.maxPointers, - scale: inputData.scale, - deltaRotation: inputData.deltaRotation, - }) - ) { - return { - failed: true, - }; - } - return { success: true }; - } - return { success: false }; - } -} - -export default PanGestureHandler; diff --git a/packages/react-native-gesture-handler/src/web_hammer/PinchGestureHandler.ts b/packages/react-native-gesture-handler/src/web_hammer/PinchGestureHandler.ts deleted file mode 100644 index 3adf5b9bd1..0000000000 --- a/packages/react-native-gesture-handler/src/web_hammer/PinchGestureHandler.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Hammer from '@egjs/hammerjs'; -import { HammerInputExt } from './GestureHandler'; - -import IndiscreteGestureHandler from './IndiscreteGestureHandler'; - -class PinchGestureHandler extends IndiscreteGestureHandler { - get name() { - return 'pinch'; - } - - get NativeGestureClass() { - return Hammer.Pinch; - } - - transformNativeEvent({ scale, velocity, center }: HammerInputExt) { - return { - focalX: center.x, - focalY: center.y, - velocity, - scale, - }; - } -} - -export default PinchGestureHandler; diff --git a/packages/react-native-gesture-handler/src/web_hammer/PressGestureHandler.ts b/packages/react-native-gesture-handler/src/web_hammer/PressGestureHandler.ts deleted file mode 100644 index a343fefa36..0000000000 --- a/packages/react-native-gesture-handler/src/web_hammer/PressGestureHandler.ts +++ /dev/null @@ -1,170 +0,0 @@ -import Hammer from '@egjs/hammerjs'; - -import { State } from '../State'; -import { - CONTENT_TOUCHES_DELAY, - CONTENT_TOUCHES_QUICK_TAP_END_DELAY, - HammerInputNames, -} from './constants'; -import DiscreteGestureHandler from './DiscreteGestureHandler'; -import { Config, HammerInputExt } from './GestureHandler'; -import { fireAfterInterval, isValidNumber, isnan } from './utils'; - -class PressGestureHandler extends DiscreteGestureHandler { - private visualFeedbackTimer: any; - private initialEvent: HammerInputExt | null = null; - get name() { - return 'press'; - } - - get minDurationMs() { - // @ts-ignore FIXME(TS) - return isnan(this.config.minDurationMs) ? 5 : this.config.minDurationMs; - } - - get maxDist() { - return isnan(this.config.maxDist) ? 9 : this.config.maxDist; - } - - get NativeGestureClass() { - return Hammer.Press; - } - - shouldDelayTouches = true; - - simulateCancelEvent(inputData: HammerInputExt) { - // Long press never starts so we can't rely on the running event boolean. - this.hasGestureFailed = true; - this.cancelEvent(inputData); - } - - updateHasCustomActivationCriteria({ - shouldCancelWhenOutside, - maxDistSq, - }: Config & { shouldCancelWhenOutside: boolean }) { - return shouldCancelWhenOutside || !isValidNumber(maxDistSq); - } - - getState(type: keyof typeof HammerInputNames): State { - return { - [Hammer.INPUT_START]: State.BEGAN, - [Hammer.INPUT_MOVE]: State.ACTIVE, - [Hammer.INPUT_END]: State.END, - [Hammer.INPUT_CANCEL]: State.CANCELLED, - }[type]; - } - - getConfig() { - if (!this.hasCustomActivationCriteria) { - // Default config - // If no params have been defined then this config should emulate the native gesture as closely as possible. - return { - shouldCancelWhenOutside: true, - maxDistSq: 10, - }; - } - return this.config; - } - - getHammerConfig() { - return { - ...super.getHammerConfig(), - // threshold: this.maxDist, - time: this.minDurationMs, - }; - } - - onGestureActivated(ev: HammerInputExt) { - this.onGestureStart(ev); - } - - shouldDelayTouchForEvent({ pointerType }: HammerInputExt) { - // Don't disable event for mouse input - return this.shouldDelayTouches && pointerType === 'touch'; - } - - onGestureStart(ev: HammerInputExt) { - this.isGestureRunning = true; - clearTimeout(this.visualFeedbackTimer); - this.initialEvent = ev; - this.visualFeedbackTimer = fireAfterInterval( - () => { - this.sendGestureStartedEvent(this.initialEvent as HammerInputExt); - this.initialEvent = null; - }, - this.shouldDelayTouchForEvent(ev) && CONTENT_TOUCHES_DELAY - ); - } - - sendGestureStartedEvent(ev: HammerInputExt) { - clearTimeout(this.visualFeedbackTimer); - this.visualFeedbackTimer = null; - this.sendEvent({ - ...ev, - eventType: Hammer.INPUT_MOVE, - isFirst: true, - }); - } - - forceInvalidate(event: HammerInputExt) { - super.forceInvalidate(event); - clearTimeout(this.visualFeedbackTimer); - this.visualFeedbackTimer = null; - this.initialEvent = null; - } - - onRawEvent(ev: HammerInputExt) { - super.onRawEvent(ev); - if (this.isGestureRunning) { - if (ev.isFinal) { - let timeout; - if (this.visualFeedbackTimer) { - // Aesthetic timing for a quick tap. - // We haven't activated the tap right away to emulate iOS `delaysContentTouches` - // Now we must send the initial activation event and wait a set amount of time before firing the end event. - timeout = CONTENT_TOUCHES_QUICK_TAP_END_DELAY; - this.sendGestureStartedEvent(this.initialEvent as HammerInputExt); - this.initialEvent = null; - } - fireAfterInterval(() => { - this.sendEvent({ - ...ev, - eventType: Hammer.INPUT_END, - isFinal: true, - }); - // @ts-ignore -- this should explicitly support undefined - this.onGestureEnded(); - }, timeout); - } else { - this.sendEvent({ - ...ev, - eventType: Hammer.INPUT_MOVE, - isFinal: false, - }); - } - } - } - - updateGestureConfig({ - shouldActivateOnStart = false, - disallowInterruption = false, - shouldCancelWhenOutside = true, - minDurationMs = Number.NaN, - maxDist = Number.NaN, - minPointers = 1, - maxPointers = 1, - ...props - }) { - return super.updateGestureConfig({ - shouldActivateOnStart, - disallowInterruption, - shouldCancelWhenOutside, - minDurationMs, - maxDist, - minPointers, - maxPointers, - ...props, - }); - } -} -export default PressGestureHandler; diff --git a/packages/react-native-gesture-handler/src/web_hammer/RotationGestureHandler.ts b/packages/react-native-gesture-handler/src/web_hammer/RotationGestureHandler.ts deleted file mode 100644 index c1404366a0..0000000000 --- a/packages/react-native-gesture-handler/src/web_hammer/RotationGestureHandler.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Hammer from '@egjs/hammerjs'; - -import { DEG_RAD } from './constants'; -import { HammerInputExt } from './GestureHandler'; -import IndiscreteGestureHandler from './IndiscreteGestureHandler'; - -class RotationGestureHandler extends IndiscreteGestureHandler { - get name() { - return 'rotate'; - } - - get NativeGestureClass() { - return Hammer.Rotate; - } - - transformNativeEvent({ rotation, velocity, center }: HammerInputExt) { - return { - rotation: (rotation - (this.initialRotation ?? 0)) * DEG_RAD, - anchorX: center.x, - anchorY: center.y, - velocity, - }; - } -} -export default RotationGestureHandler; diff --git a/packages/react-native-gesture-handler/src/web_hammer/TapGestureHandler.ts b/packages/react-native-gesture-handler/src/web_hammer/TapGestureHandler.ts deleted file mode 100644 index 312049c95e..0000000000 --- a/packages/react-native-gesture-handler/src/web_hammer/TapGestureHandler.ts +++ /dev/null @@ -1,172 +0,0 @@ -import Hammer from '@egjs/hammerjs'; - -import DiscreteGestureHandler from './DiscreteGestureHandler'; -import { HammerInputExt } from './GestureHandler'; -import { isnan } from './utils'; - -class TapGestureHandler extends DiscreteGestureHandler { - private _shouldFireEndEvent: HammerInputExt | null = null; - private _timer: any; - private _multiTapTimer: any; // TODO unused? - get name() { - return 'tap'; - } - - get NativeGestureClass() { - return Hammer.Tap; - } - - get maxDelayMs() { - // @ts-ignore TODO(TS) trace down config - return isnan(this.config.maxDelayMs) ? 300 : this.config.maxDelayMs; - } - - simulateCancelEvent(inputData: HammerInputExt) { - if (this.isGestureRunning) { - this.cancelEvent(inputData); - } - } - - onGestureActivated(ev: HammerInputExt) { - if (this.isGestureRunning) { - this.onSuccessfulTap(ev); - } - } - - onSuccessfulTap = (ev: HammerInputExt) => { - if (this._getPendingGestures().length) { - this._shouldFireEndEvent = ev; - return; - } - if (ev.eventType === Hammer.INPUT_END) { - this.sendEvent({ ...ev, eventType: Hammer.INPUT_MOVE }); - } - // When handler gets activated it will turn into State.END immediately. - this.sendEvent({ ...ev, isFinal: true }); - this.onGestureEnded(ev); - }; - - onRawEvent(ev: HammerInput) { - super.onRawEvent(ev); - - // Attempt to create a touch-down event by checking if a valid tap hasn't started yet, then validating the input. - if ( - !this.hasGestureFailed && - !this.isGestureRunning && - // Prevent multi-pointer events from misfiring. - !ev.isFinal - ) { - // Tap Gesture start event - const gesture = this.hammer!.get(this.name); - // @ts-ignore TODO(TS) trace down config - if (gesture.options.enable(gesture, ev)) { - clearTimeout(this._multiTapTimer); - - this.onStart(ev); - this.sendEvent(ev); - } - } - if (ev.isFinal && ev.maxPointers > 1) { - setTimeout(() => { - // Handle case where one finger presses slightly - // after the first finger on a multi-tap event - if (this.isGestureRunning) { - this.cancelEvent(ev); - } - }); - } - - if (this.hasGestureFailed) { - return; - } - // Hammer doesn't send a `cancel` event for taps. - // Manually fail the event. - if (ev.isFinal) { - // Handle case where one finger presses slightly - // after the first finger on a multi-tap event - if (ev.maxPointers > 1) { - setTimeout(() => { - if (this.isGestureRunning) { - this.cancelEvent(ev); - } - }); - } - - // Clear last timer - clearTimeout(this._timer); - // Create time out for multi-taps. - this._timer = setTimeout(() => { - this.hasGestureFailed = true; - this.cancelEvent(ev); - }, this.maxDelayMs); - } else if (!this.hasGestureFailed && !this.isGestureRunning) { - // Tap Gesture start event - const gesture = this.hammer!.get(this.name); - // @ts-ignore TODO(TS) trace down config - if (gesture.options.enable(gesture, ev)) { - clearTimeout(this._multiTapTimer); - - this.onStart(ev); - this.sendEvent(ev); - } - } - } - - getHammerConfig() { - return { - ...super.getHammerConfig(), - event: this.name, - // @ts-ignore TODO(TS) trace down config - taps: isnan(this.config.numberOfTaps) ? 1 : this.config.numberOfTaps, - interval: this.maxDelayMs, - time: - // @ts-ignore TODO(TS) trace down config - isnan(this.config.maxDurationMs) || this.config.maxDurationMs == null - ? 250 - : // @ts-ignore TODO(TS) trace down config - this.config.maxDurationMs, - }; - } - - updateGestureConfig({ - shouldCancelWhenOutside = true, - maxDeltaX = Number.NaN, - maxDeltaY = Number.NaN, - numberOfTaps = 1, - minDurationMs = 525, - maxDelayMs = Number.NaN, - // eslint-disable-next-line @typescript-eslint/no-unused-vars -- TODO possibly forgotten to use in updateGestureConfig? - maxDurationMs = Number.NaN, - maxDist = 2, - minPointers = 1, - maxPointers = 1, - ...props - }) { - return super.updateGestureConfig({ - shouldCancelWhenOutside, - numberOfTaps, - maxDeltaX, - maxDeltaY, - minDurationMs, - maxDelayMs, - maxDist, - minPointers, - maxPointers, - ...props, - }); - } - - onGestureEnded(...props: any) { - clearTimeout(this._timer); - // @ts-ignore TODO(TS) check how onGestureEnded works - super.onGestureEnded(...props); - } - - onWaitingEnded(_gesture: any) { - if (this._shouldFireEndEvent) { - this.onSuccessfulTap(this._shouldFireEndEvent); - this._shouldFireEndEvent = null; - } - } -} -export default TapGestureHandler; diff --git a/packages/react-native-gesture-handler/src/web_hammer/constants.ts b/packages/react-native-gesture-handler/src/web_hammer/constants.ts deleted file mode 100644 index 71b792b7c7..0000000000 --- a/packages/react-native-gesture-handler/src/web_hammer/constants.ts +++ /dev/null @@ -1,48 +0,0 @@ -import Hammer from '@egjs/hammerjs'; - -import { State } from '../State'; - -export const CONTENT_TOUCHES_DELAY = 240; -export const CONTENT_TOUCHES_QUICK_TAP_END_DELAY = 50; -export const MULTI_FINGER_PAN_MAX_PINCH_THRESHOLD = 0.1; -export const MULTI_FINGER_PAN_MAX_ROTATION_THRESHOLD = 7; -export const DEG_RAD = Math.PI / 180; - -// Map Hammer values to RNGH -export const EventMap = { - [Hammer.INPUT_START]: State.BEGAN, - [Hammer.INPUT_MOVE]: State.ACTIVE, - [Hammer.INPUT_END]: State.END, - [Hammer.INPUT_CANCEL]: State.FAILED, -} as const; - -export const Direction = { - RIGHT: 1, - LEFT: 2, - UP: 4, - DOWN: 8, -}; - -export const DirectionMap = { - [Hammer.DIRECTION_RIGHT]: Direction.RIGHT, - [Hammer.DIRECTION_LEFT]: Direction.LEFT, - [Hammer.DIRECTION_UP]: Direction.UP, - [Hammer.DIRECTION_DOWN]: Direction.DOWN, -}; - -export const HammerInputNames = { - [Hammer.INPUT_START]: 'START', - [Hammer.INPUT_MOVE]: 'MOVE', - [Hammer.INPUT_END]: 'END', - [Hammer.INPUT_CANCEL]: 'CANCEL', -}; -export const HammerDirectionNames = { - [Hammer.DIRECTION_HORIZONTAL]: 'HORIZONTAL', - [Hammer.DIRECTION_UP]: 'UP', - [Hammer.DIRECTION_DOWN]: 'DOWN', - [Hammer.DIRECTION_VERTICAL]: 'VERTICAL', - [Hammer.DIRECTION_NONE]: 'NONE', - [Hammer.DIRECTION_ALL]: 'ALL', - [Hammer.DIRECTION_RIGHT]: 'RIGHT', - [Hammer.DIRECTION_LEFT]: 'LEFT', -}; diff --git a/packages/react-native-gesture-handler/src/web_hammer/utils.ts b/packages/react-native-gesture-handler/src/web_hammer/utils.ts deleted file mode 100644 index 8886395db2..0000000000 --- a/packages/react-native-gesture-handler/src/web_hammer/utils.ts +++ /dev/null @@ -1,24 +0,0 @@ -// TODO(TS) remove if not necessary after rewrite -export const isnan = (v: unknown) => Number.isNaN(v); - -// TODO(TS) remove if not necessary after rewrite -export const isValidNumber = (v: unknown) => - typeof v === 'number' && !Number.isNaN(v); - -export const TEST_MIN_IF_NOT_NAN = (value: number, limit: number): boolean => - !isnan(limit) && - ((limit < 0 && value <= limit) || (limit >= 0 && value >= limit)); -export const VEC_LEN_SQ = ({ x = 0, y = 0 } = {}) => x * x + y * y; -export const TEST_MAX_IF_NOT_NAN = (value: number, max: number) => - !isnan(max) && ((max < 0 && value < max) || (max >= 0 && value > max)); - -export function fireAfterInterval( - method: () => void, - interval?: number | boolean -) { - if (!interval) { - method(); - return null; - } - return setTimeout(() => method(), interval as number); -} diff --git a/yarn.lock b/yarn.lock index 54bdc54db4..e73f74fb90 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1590,15 +1590,6 @@ __metadata: languageName: node linkType: hard -"@egjs/hammerjs@npm:^2.0.17": - version: 2.0.17 - resolution: "@egjs/hammerjs@npm:2.0.17" - dependencies: - "@types/hammerjs": "npm:^2.0.36" - checksum: 10c0/dbedc15a0e633f887c08394bd636faf6a3abd05726dc0909a0e01209d5860a752d9eca5e512da623aecfabe665f49f1d035de3103eb2f9022c5cea692f9cc9be - languageName: node - linkType: hard - "@eslint-community/eslint-plugin-eslint-comments@npm:^4.3.0": version: 4.5.0 resolution: "@eslint-community/eslint-plugin-eslint-comments@npm:4.5.0" @@ -4642,13 +4633,6 @@ __metadata: languageName: node linkType: hard -"@types/hammerjs@npm:^2.0.36": - version: 2.0.46 - resolution: "@types/hammerjs@npm:2.0.46" - checksum: 10c0/f3c1cb20dc2f0523f7b8c76065078544d50d8ae9b0edc1f62fed657210ed814266ff2dfa835d2c157a075991001eec3b64c88bf92e3e6e895c0db78d05711d06 - languageName: node - linkType: hard - "@types/hoist-non-react-statics@npm:^3.3.1": version: 3.3.6 resolution: "@types/hoist-non-react-statics@npm:3.3.6" @@ -14037,7 +14021,6 @@ __metadata: "@babel/core": "npm:^7.25.2" "@babel/preset-env": "npm:^7.25.3" "@babel/preset-typescript": "npm:^7.12.7" - "@egjs/hammerjs": "npm:^2.0.17" "@react-native/babel-preset": "npm:0.80.0" "@testing-library/react-native": "npm:^12.5.1" "@types/hoist-non-react-statics": "npm:^3.3.1" From 8a8b5062116f7192345f9378dd3a96d48f987894 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 7 Jul 2025 09:40:18 +0200 Subject: [PATCH 002/236] Update how Gesture Handler exposes `setGestureState` to the Reanimated UI runtime (#3207) ## Description Changes how `setGestureState` is exposed to the UI runtime. Instead of the weird conditionally adding Reanimated as a dependency on Android and the weird cast on iOS it uses `_WORKLET_RUNTIME` const injected by Reanimated into the JS runtime. This allows Gesture Handler to decorate the UI runtime without direct dependencies between the libraries. The new approach relies on two methods being added to the global object: - `_setGestureStateAsync` on the JS runtime - `_setGestureStateSync` on the UI runtime which allows for state manipulation also from the JS thread. The basic example has been modified to easily test the new functionality. > [!CAUTION] > This works only on the New Architecture (and breaks the old one) ## Test plan Test the expo example app and the modified basic example app --- .lintstagedrc.json | 1 + apps/basic-example/Gemfile.lock | 13 +- apps/basic-example/android/gradle.properties | 2 +- apps/basic-example/babel.config.js | 1 + apps/basic-example/ios/Podfile.lock | 226 ++++++++++++++++- apps/basic-example/package.json | 4 +- apps/basic-example/src/HomeScreen.tsx | 228 +++++------------- .../RNGestureHandler.podspec | 2 +- .../common/GestureHandlerStateManager.kt | 5 - ...dEventDispatcher.kt => ReanimatedProxy.kt} | 6 +- .../NativeRNGestureHandlerModuleSpec.java | 8 +- ...dEventDispatcher.kt => ReanimatedProxy.kt} | 6 +- .../react/RNGestureHandlerEventDispatcher.kt | 6 +- .../react/RNGestureHandlerModule.kt | 64 +++-- .../android/src/main/jni/CMakeLists.txt | 10 +- .../android/src/main/jni/OnLoad.cpp | 7 + .../src/main/jni/RNGestureHandlerModule.cpp | 67 +++++ .../src/main/jni/RNGestureHandlerModule.h | 34 +++ .../android/src/main/jni/cpp-adapter.cpp | 47 ---- .../apple/RNGestureHandlerModule.mm | 117 ++++----- .../apple/RNGestureHandlerStateManager.h | 5 - .../react-native-gesture-handler/package.json | 4 +- .../shared/RNGHRuntimeDecorator.cpp | 124 ++++++++++ .../shared/RNGHRuntimeDecorator.h | 17 ++ .../GestureHandlerRootView.android.tsx | 6 - .../src/components/GestureHandlerRootView.tsx | 6 - .../GestureDetector/useViewRefHandler.ts | 6 +- .../handlers/gestures/gestureStateManager.ts | 56 ++--- .../react-native-gesture-handler/src/init.ts | 13 - .../src/specs/NativeRNGestureHandlerModule.ts | 5 +- .../react-native-gesture-handler/src/utils.ts | 1 + scripts/{format-apple.js => format-cpp.js} | 11 +- yarn.lock | 111 ++++++--- 33 files changed, 790 insertions(+), 429 deletions(-) delete mode 100644 packages/react-native-gesture-handler/android/noreanimated/src/main/java/com/swmansion/common/GestureHandlerStateManager.kt rename packages/react-native-gesture-handler/android/noreanimated/src/main/java/com/swmansion/gesturehandler/{ReanimatedEventDispatcher.kt => ReanimatedProxy.kt} (78%) rename packages/react-native-gesture-handler/android/reanimated/src/main/java/com/swmansion/gesturehandler/{ReanimatedEventDispatcher.kt => ReanimatedProxy.kt} (85%) create mode 100644 packages/react-native-gesture-handler/android/src/main/jni/OnLoad.cpp create mode 100644 packages/react-native-gesture-handler/android/src/main/jni/RNGestureHandlerModule.cpp create mode 100644 packages/react-native-gesture-handler/android/src/main/jni/RNGestureHandlerModule.h delete mode 100644 packages/react-native-gesture-handler/android/src/main/jni/cpp-adapter.cpp delete mode 100644 packages/react-native-gesture-handler/apple/RNGestureHandlerStateManager.h create mode 100644 packages/react-native-gesture-handler/shared/RNGHRuntimeDecorator.cpp create mode 100644 packages/react-native-gesture-handler/shared/RNGHRuntimeDecorator.h rename scripts/{format-apple.js => format-cpp.js} (75%) diff --git a/.lintstagedrc.json b/.lintstagedrc.json index 0e38d4e378..57b3460940 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -6,5 +6,6 @@ "yarn format:android" ], "packages/react-native-gesture-handler/apple/**/*.{h,m,mm,cpp}": "yarn format:apple", + "packages/react-native-gesture-handler/{shared,android/src}/**/*.{h,cpp}": "yarn format:cpp", "packages/react-native-gesture-handler/src/specs/*.ts": "yarn workspace react-native-gesture-handler sync-architectures" } diff --git a/apps/basic-example/Gemfile.lock b/apps/basic-example/Gemfile.lock index f2345fa62b..3e46c77c15 100644 --- a/apps/basic-example/Gemfile.lock +++ b/apps/basic-example/Gemfile.lock @@ -5,18 +5,16 @@ GEM base64 nkf rexml - activesupport (7.2.2.1) + activesupport (7.1.4.2) base64 - benchmark (>= 0.3) bigdecimal - concurrent-ruby (~> 1.0, >= 1.3.1) + concurrent-ruby (~> 1.0, >= 1.0.2) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) - logger (>= 1.4.2) minitest (>= 5.1) - securerandom (>= 0.3) - tzinfo (~> 2.0, >= 2.0.5) + mutex_m + tzinfo (~> 2.0) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) algoliasearch (1.27.5) @@ -91,7 +89,6 @@ GEM public_suffix (4.0.7) rexml (3.4.1) ruby-macho (2.5.1) - securerandom (0.4.1) typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.6) @@ -121,4 +118,4 @@ RUBY VERSION ruby 3.3.5p100 BUNDLED WITH - 2.5.23 + 2.4.7 diff --git a/apps/basic-example/android/gradle.properties b/apps/basic-example/android/gradle.properties index b6e15b48c1..5e24e3aa8d 100644 --- a/apps/basic-example/android/gradle.properties +++ b/apps/basic-example/android/gradle.properties @@ -10,7 +10,7 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m -org.gradle.jvmargs=-Xmx512m -XX:MaxMetaspaceSize=512m +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit diff --git a/apps/basic-example/babel.config.js b/apps/basic-example/babel.config.js index f7b3da3b33..8ba8eb658c 100644 --- a/apps/basic-example/babel.config.js +++ b/apps/basic-example/babel.config.js @@ -1,3 +1,4 @@ module.exports = { presets: ['module:@react-native/babel-preset'], + plugins: ['react-native-worklets/plugin'], }; diff --git a/apps/basic-example/ios/Podfile.lock b/apps/basic-example/ios/Podfile.lock index 4c160edffb..27ba11a5e7 100644 --- a/apps/basic-example/ios/Podfile.lock +++ b/apps/basic-example/ios/Podfile.lock @@ -2151,6 +2151,218 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga + - RNReanimated (4.0.0-beta.5): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNReanimated/reanimated (= 4.0.0-beta.5) + - RNWorklets + - SocketRocket + - Yoga + - RNReanimated/reanimated (4.0.0-beta.5): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNReanimated/reanimated/apple (= 4.0.0-beta.5) + - RNReanimated/reanimated/view (= 4.0.0-beta.5) + - RNWorklets + - SocketRocket + - Yoga + - RNReanimated/reanimated/apple (4.0.0-beta.5): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNWorklets + - SocketRocket + - Yoga + - RNReanimated/reanimated/view (4.0.0-beta.5): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNWorklets + - SocketRocket + - Yoga + - RNWorklets (0.3.0): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNWorklets/worklets (= 0.3.0) + - SocketRocket + - Yoga + - RNWorklets/worklets (0.3.0): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNWorklets/worklets/apple (= 0.3.0) + - SocketRocket + - Yoga + - RNWorklets/worklets/apple (0.3.0): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - SocketRocket (0.7.1) - Yoga (0.0.0) @@ -2228,6 +2440,8 @@ DEPENDENCIES: - ReactCodegen (from `build/generated/ios`) - ReactCommon/turbomodule/core (from `../../../node_modules/react-native/ReactCommon`) - RNGestureHandler (from `../../../node_modules/react-native-gesture-handler`) + - RNReanimated (from `../node_modules/react-native-reanimated`) + - RNWorklets (from `../../../node_modules/react-native-worklets`) - SocketRocket (~> 0.7.1) - Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`) @@ -2381,6 +2595,10 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native/ReactCommon" RNGestureHandler: :path: "../../../node_modules/react-native-gesture-handler" + RNReanimated: + :path: "../node_modules/react-native-reanimated" + RNWorklets: + :path: "../../../node_modules/react-native-worklets" Yoga: :path: "../../../node_modules/react-native/ReactCommon/yoga" @@ -2392,7 +2610,7 @@ SPEC CHECKSUMS: fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 7068e976238b29e97b3bafd09a994542af7d5c0b - RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f + RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCTDeprecation: ff787f6c860a1b97dd1bc27264b61d23ad1994da RCTRequired: 664eb8399ed8a83e26ab65af7c2ad390f7e61696 RCTTypeSafety: a5cf7a7e80baf972e331dc028e5d5c19bb2535a4 @@ -2456,10 +2674,12 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: 3267432b637c9b38e86961b287f784ee1b08dde0 ReactCodegen: d82f538f70f00484d418803f74b5a0ea09cc8689 ReactCommon: b028d09a66e60ebd83ca59d8cc9a1216360db147 - RNGestureHandler: 042bf47f34946da9ae3c15a8d28b2ffb22c1000d + RNGestureHandler: f867857acbb6a519c2d6651c7a8fdb7f7d2ae8f4 + RNReanimated: 25060745a200605462ff56cf488411db066631ce + RNWorklets: 9bb08cb0ef718ce063f61ca18f95f57aec9b9673 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: 0c4b7d2aacc910a1f702694fa86be830386f4ceb PODFILE CHECKSUM: d05778d3a61b8d49242579ea0aa864580fbb1f64 -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 diff --git a/apps/basic-example/package.json b/apps/basic-example/package.json index 3a741605d8..d279068300 100644 --- a/apps/basic-example/package.json +++ b/apps/basic-example/package.json @@ -19,7 +19,9 @@ "dependencies": { "react": "19.1.0", "react-native": "0.80.0", - "react-native-gesture-handler": "workspace:*" + "react-native-gesture-handler": "workspace:*", + "react-native-reanimated": "4.0.0-beta.5", + "react-native-worklets": "^0.3.0" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/apps/basic-example/src/HomeScreen.tsx b/apps/basic-example/src/HomeScreen.tsx index 0c96155a61..d729810f01 100644 --- a/apps/basic-example/src/HomeScreen.tsx +++ b/apps/basic-example/src/HomeScreen.tsx @@ -1,172 +1,84 @@ import * as React from 'react'; -import { Animated, StyleSheet, Text, View } from 'react-native'; -import { - Gesture, - GestureDetector, - PanGestureHandler, - PanGestureHandlerStateChangeEvent, - State, -} from 'react-native-gesture-handler'; +import { StyleSheet, View } from 'react-native'; +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import { isFabric, isHermes } from './utils'; import { COLORS } from './colors'; +import Animated, { + useAnimatedStyle, + useSharedValue, +} from 'react-native-reanimated'; -declare const performance: { - now: () => number; -}; - -interface GestureDetectorDemoProps { - color: string; -} +export default function HomeScreen() { + const pressed = useSharedValue(false); + const active = useSharedValue(false); + const posX = useSharedValue(0); + const posY = useSharedValue(0); + + const start = useSharedValue({ x: 0, y: 0 }); + + const style = useAnimatedStyle(() => { + return { + transform: [ + { translateX: posX.value }, + { translateY: posY.value }, + { scale: pressed.value ? 1.2 : 1 }, + ], + backgroundColor: active.value ? COLORS.KINDA_GREEN : COLORS.KINDA_BLUE, + }; + }); -export function GestureDetectorDemo({ color }: GestureDetectorDemoProps) { - const gesture = Gesture.Pan() - .onBegin(() => { - console.log(performance.now(), 'onBegin'); + const gesture = Gesture.Manual() + .onTouchesDown((e) => { + if (!pressed.value) { + pressed.value = true; + start.value = { + x: e.allTouches[0].absoluteX, + y: e.allTouches[0].absoluteY, + }; + } + console.log(_WORKLET); }) - .onStart(() => { - console.log(performance.now(), 'onStart'); + .onTouchesMove((e, state) => { + const dist = Math.sqrt( + Math.pow(e.allTouches[0].absoluteX - start.value.x, 2) + + Math.pow(e.allTouches[0].absoluteY - start.value.y, 2) + ); + + if (active.value) { + posX.value = e.allTouches[0].absoluteX - start.value.x; + posY.value = e.allTouches[0].absoluteY - start.value.y; + } else { + if (dist > 10) { + state.activate(); + start.value = { + x: e.allTouches[0].absoluteX, + y: e.allTouches[0].absoluteY, + }; + } + } }) - .onUpdate(() => { - console.log(performance.now(), 'onUpdate'); + .onTouchesUp((e, state) => { + if (e.allTouches.length === e.changedTouches.length) { + state.end(); + } }) - .onEnd(() => { - console.log(performance.now(), 'onEnd'); + .onStart(() => { + console.log('Gesture started'); + active.value = true; }) .onFinalize(() => { - console.log(performance.now(), 'onFinalize'); + console.log('Gesture finalized'); + pressed.value = false; + active.value = false; }); return ( - - Gesture.Pan - - - - - ); -} - -interface ManualGestureDemoProps { - color: string; -} - -export function ManualGestureDemo({ color }: ManualGestureDemoProps) { - const gesture = Gesture.Manual() - .onTouchesDown(() => { - console.log(performance.now(), 'onTouchesDown'); - }) - .onTouchesMove(() => { - console.log(performance.now(), 'onTouchesMove'); - }) - .onTouchesUp(() => { - console.log(performance.now(), 'onTouchesUp'); - }); - - return ( - - Gesture.Manual + - - - - ); -} - -interface PanGestureHandlerDemoProps { - color: string; -} - -export function PanGestureHandlerDemo({ color }: PanGestureHandlerDemoProps) { - const onGestureEvent = () => { - console.log(performance.now(), 'onGestureEvent'); - }; - - const onHandlerStateChange = () => { - console.log(performance.now(), 'onHandlerStateChange'); - }; - - return ( - - PanGestureHandler - - - - - ); -} - -type AnimatedEventDemoProps = { - useNativeDriver: boolean; - color: string; -}; - -export function AnimatedEventDemo({ - useNativeDriver, - color, -}: AnimatedEventDemoProps) { - const drag = React.useRef(new Animated.Value(0)); - - const onGestureEvent = Animated.event( - [{ nativeEvent: { translationX: drag.current } }], - { useNativeDriver } - ); - - const onHandlerStateChange = (event: PanGestureHandlerStateChangeEvent) => { - if ( - event.nativeEvent.state === State.FAILED || - event.nativeEvent.state === State.CANCELLED || - event.nativeEvent.state === State.END - ) { - Animated.spring(drag.current, { - velocity: event.nativeEvent.velocityX, - tension: 10, - friction: 2, - toValue: 0, - useNativeDriver, - }).start(); - } - }; - - return ( - - - Animated.event useNativeDriver: {useNativeDriver.toString()} - - - - - ); -} - -export default function HomeScreen() { - return ( - - Hello from React Native Gesture Handler! - - This example app runs on {isHermes() ? 'Hermes' : 'JSC'} with Fabric{' '} - {isFabric() ? 'enabled' : 'disabled'}. - - - - - - + ); } @@ -177,16 +89,6 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, - bold: { - fontWeight: 'bold', - }, - text: { - marginVertical: 3, - }, - demo: { - marginVertical: 3, - alignItems: 'center', - }, box: { width: 50, height: 50, diff --git a/packages/react-native-gesture-handler/RNGestureHandler.podspec b/packages/react-native-gesture-handler/RNGestureHandler.podspec index 7b51e49356..1b1979d847 100644 --- a/packages/react-native-gesture-handler/RNGestureHandler.podspec +++ b/packages/react-native-gesture-handler/RNGestureHandler.podspec @@ -17,7 +17,7 @@ Pod::Spec.new do |s| s.license = "MIT" s.author = { package["author"]["name"] => package["author"]["email"] } s.source = { :git => "https://github.com/software-mansion/react-native-gesture-handler", :tag => "#{s.version}" } - s.source_files = "apple/**/*.{h,m,mm}" + s.source_files = "apple/**/*.{h,m,mm}", "shared/**/*.{h,cpp}" s.requires_arc = true s.platforms = { ios: '11.0', tvos: '11.0', osx: '10.15', visionos: '1.0' } s.xcconfig = { diff --git a/packages/react-native-gesture-handler/android/noreanimated/src/main/java/com/swmansion/common/GestureHandlerStateManager.kt b/packages/react-native-gesture-handler/android/noreanimated/src/main/java/com/swmansion/common/GestureHandlerStateManager.kt deleted file mode 100644 index 4e754cdbf1..0000000000 --- a/packages/react-native-gesture-handler/android/noreanimated/src/main/java/com/swmansion/common/GestureHandlerStateManager.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.swmansion.common - -interface GestureHandlerStateManager { - fun setGestureHandlerState(handlerTag: Int, newState: Int) -} diff --git a/packages/react-native-gesture-handler/android/noreanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedEventDispatcher.kt b/packages/react-native-gesture-handler/android/noreanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedProxy.kt similarity index 78% rename from packages/react-native-gesture-handler/android/noreanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedEventDispatcher.kt rename to packages/react-native-gesture-handler/android/noreanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedProxy.kt index 22336322f2..e98bbb390f 100644 --- a/packages/react-native-gesture-handler/android/noreanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedEventDispatcher.kt +++ b/packages/react-native-gesture-handler/android/noreanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedProxy.kt @@ -3,10 +3,14 @@ package com.swmansion.gesturehandler import com.facebook.react.bridge.ReactContext import com.facebook.react.uimanager.events.Event -class ReanimatedEventDispatcher { +class ReanimatedProxy { // This is necessary on new architecture @Suppress("UNUSED_PARAMETER", "COMMENT_IN_SUPPRESSION") fun > sendEvent(event: T, reactApplicationContext: ReactContext) { // no-op } + + companion object { + const val REANIMATED_INSTALLED = false + } } diff --git a/packages/react-native-gesture-handler/android/paper/src/main/java/com/swmansion/gesturehandler/NativeRNGestureHandlerModuleSpec.java b/packages/react-native-gesture-handler/android/paper/src/main/java/com/swmansion/gesturehandler/NativeRNGestureHandlerModuleSpec.java index 5c12b84ba2..3fedeea9a9 100644 --- a/packages/react-native-gesture-handler/android/paper/src/main/java/com/swmansion/gesturehandler/NativeRNGestureHandlerModuleSpec.java +++ b/packages/react-native-gesture-handler/android/paper/src/main/java/com/swmansion/gesturehandler/NativeRNGestureHandlerModuleSpec.java @@ -41,9 +41,9 @@ public NativeRNGestureHandlerModuleSpec(ReactApplicationContext reactContext) { @DoNotStrip public abstract void handleClearJSResponder(); - @ReactMethod + @ReactMethod(isBlockingSynchronousMethod = true) @DoNotStrip - public abstract void createGestureHandler(String handlerName, double handlerTag, ReadableMap config); + public abstract boolean createGestureHandler(String handlerName, double handlerTag, ReadableMap config); @ReactMethod @DoNotStrip @@ -57,10 +57,6 @@ public NativeRNGestureHandlerModuleSpec(ReactApplicationContext reactContext) { @DoNotStrip public abstract void dropGestureHandler(double handlerTag); - @ReactMethod(isBlockingSynchronousMethod = true) - @DoNotStrip - public abstract boolean install(); - @ReactMethod @DoNotStrip public abstract void flushOperations(); diff --git a/packages/react-native-gesture-handler/android/reanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedEventDispatcher.kt b/packages/react-native-gesture-handler/android/reanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedProxy.kt similarity index 85% rename from packages/react-native-gesture-handler/android/reanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedEventDispatcher.kt rename to packages/react-native-gesture-handler/android/reanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedProxy.kt index a7bf6aa39e..c11fea2a9b 100644 --- a/packages/react-native-gesture-handler/android/reanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedEventDispatcher.kt +++ b/packages/react-native-gesture-handler/android/reanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedProxy.kt @@ -4,7 +4,7 @@ import com.facebook.react.bridge.ReactContext import com.facebook.react.uimanager.events.Event import com.swmansion.reanimated.ReanimatedModule -class ReanimatedEventDispatcher { +class ReanimatedProxy { private var reanimatedModule: ReanimatedModule? = null fun > sendEvent(event: T, reactApplicationContext: ReactContext) { @@ -14,4 +14,8 @@ class ReanimatedEventDispatcher { reanimatedModule?.nodesManager?.onEventDispatch(event) } + + companion object { + const val REANIMATED_INSTALLED = true + } } diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerEventDispatcher.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerEventDispatcher.kt index a047c2c8b2..923747dbbd 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerEventDispatcher.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerEventDispatcher.kt @@ -5,14 +5,14 @@ import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.WritableMap import com.facebook.react.uimanager.events.Event import com.swmansion.gesturehandler.BuildConfig -import com.swmansion.gesturehandler.ReanimatedEventDispatcher +import com.swmansion.gesturehandler.ReanimatedProxy import com.swmansion.gesturehandler.core.GestureHandler import com.swmansion.gesturehandler.core.OnTouchEventListener import com.swmansion.gesturehandler.dispatchEvent class RNGestureHandlerEventDispatcher(private val reactApplicationContext: ReactApplicationContext) : OnTouchEventListener { - private val reanimatedEventDispatcher = ReanimatedEventDispatcher() + private val reanimatedProxy = ReanimatedProxy() override fun onHandlerUpdate(handler: T, event: MotionEvent) { this.dispatchHandlerUpdateEvent(handler) @@ -162,7 +162,7 @@ class RNGestureHandlerEventDispatcher(private val reactApplicationContext: React // Delivers the event to Reanimated. if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { // Send event directly to Reanimated - reanimatedEventDispatcher.sendEvent(event, reactApplicationContext) + reanimatedProxy.sendEvent(event, reactApplicationContext) } else { // In the old architecture, Reanimated subscribes for specific direct events. sendEventForDirectEvent(event) 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 e149fa49fc..8b781ac0ed 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 @@ -1,15 +1,19 @@ package com.swmansion.gesturehandler.react -import android.util.Log +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip import com.facebook.react.ReactRootView import com.facebook.react.bridge.JSApplicationIllegalArgumentException import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.turbomodule.core.interfaces.BindingsInstallerHolder +import com.facebook.react.turbomodule.core.interfaces.TurboModuleWithJSIBindings import com.facebook.soloader.SoLoader -import com.swmansion.common.GestureHandlerStateManager import com.swmansion.gesturehandler.NativeRNGestureHandlerModuleSpec +import com.swmansion.gesturehandler.ReanimatedProxy import com.swmansion.gesturehandler.core.GestureHandler // UIManagerModule.resolveRootTagFromReactTag() was deprecated and will be removed in the next RN release @@ -18,13 +22,18 @@ import com.swmansion.gesturehandler.core.GestureHandler @ReactModule(name = RNGestureHandlerModule.NAME) class RNGestureHandlerModule(reactContext: ReactApplicationContext?) : NativeRNGestureHandlerModuleSpec(reactContext), - GestureHandlerStateManager { + TurboModuleWithJSIBindings { val registry: RNGestureHandlerRegistry = RNGestureHandlerRegistry() private val eventDispatcher = RNGestureHandlerEventDispatcher(reactApplicationContext) private val interactionManager = RNGestureHandlerInteractionManager() private val roots: MutableList = ArrayList() + @DoNotStrip + @Suppress("unused") + private var mHybridData: HybridData = initHybrid() + private var uiRuntimeDecorated = false + override fun getName() = NAME private fun createGestureHandlerHelper( @@ -49,10 +58,16 @@ class RNGestureHandlerModule(reactContext: ReactApplicationContext?) : } @ReactMethod - override fun createGestureHandler(handlerName: String, handlerTagDouble: Double, config: ReadableMap) { + override fun createGestureHandler(handlerName: String, handlerTagDouble: Double, config: ReadableMap): Boolean { + if (ReanimatedProxy.REANIMATED_INSTALLED && !uiRuntimeDecorated) { + uiRuntimeDecorated = decorateUIRuntime() + } + val handlerTag = handlerTagDouble.toInt() createGestureHandlerHelper(handlerName, handlerTag, config) + + return true } @ReactMethod @@ -104,7 +119,21 @@ class RNGestureHandlerModule(reactContext: ReactApplicationContext?) : @ReactMethod override fun flushOperations() = Unit - override fun setGestureHandlerState(handlerTag: Int, newState: Int) { + @DoNotStrip + @Suppress("unused") + fun setGestureHandlerState(handlerTag: Int, newState: Int) { + if (UiThreadUtil.isOnUiThread()) { + setGestureStateSync(handlerTag, newState) + } else { + UiThreadUtil.runOnUiThread { + setGestureStateSync(handlerTag, newState) + } + } + } + + private fun setGestureStateSync(handlerTag: Int, newState: Int) { + UiThreadUtil.assertOnUiThread() + registry.getHandler(handlerTag)?.let { handler -> when (newState) { GestureHandler.STATE_ACTIVE -> handler.activate(force = true) @@ -116,22 +145,12 @@ class RNGestureHandlerModule(reactContext: ReactApplicationContext?) : } } - @ReactMethod(isBlockingSynchronousMethod = true) - override fun install(): Boolean { - reactApplicationContext.runOnJSQueueThread { - try { - SoLoader.loadLibrary("gesturehandler") - val jsContext = reactApplicationContext.javaScriptContextHolder!! - decorateRuntime(jsContext.get()) - } catch (exception: Exception) { - Log.w("[RNGestureHandler]", "Could not install JSI bindings.") - } - } - - return true - } + private external fun initHybrid(): HybridData + private external fun getBindingsInstallerCxx(): BindingsInstallerHolder + private external fun decorateUIRuntime(): Boolean + private external fun invalidateNative(): Unit - private external fun decorateRuntime(jsiPtr: Long) + override fun getBindingsInstaller() = getBindingsInstallerCxx() override fun invalidate() { registry.dropAllHandlers() @@ -147,6 +166,7 @@ class RNGestureHandlerModule(reactContext: ReactApplicationContext?) : } } } + invalidateNative() super.invalidate() } @@ -177,5 +197,9 @@ class RNGestureHandlerModule(reactContext: ReactApplicationContext?) : companion object { const val NAME = "RNGestureHandlerModule" + + init { + SoLoader.loadLibrary("gesturehandler") + } } } diff --git a/packages/react-native-gesture-handler/android/src/main/jni/CMakeLists.txt b/packages/react-native-gesture-handler/android/src/main/jni/CMakeLists.txt index 9f05cff333..7b9566f4bd 100644 --- a/packages/react-native-gesture-handler/android/src/main/jni/CMakeLists.txt +++ b/packages/react-native-gesture-handler/android/src/main/jni/CMakeLists.txt @@ -9,18 +9,26 @@ else() endif() set(PACKAGE_NAME "gesturehandler") +set(RNGH_DIR "${CMAKE_SOURCE_DIR}/../../../../") set(REACT_ANDROID_DIR "${REACT_NATIVE_DIR}/ReactAndroid") +file(GLOB_RECURSE gesture_handler_SRCS CONFIGURE_DEPENDS "./*.cpp") +file(GLOB_RECURSE gesture_handler_shared_SRCS CONFIGURE_DEPENDS "${RNGH_DIR}/shared/*.cpp") + include(${REACT_ANDROID_DIR}/cmake-utils/folly-flags.cmake) add_compile_options(${folly_FLAGS}) add_library(${PACKAGE_NAME} SHARED - cpp-adapter.cpp + ${gesture_handler_SRCS} + ${gesture_handler_shared_SRCS} ) target_include_directories( ${PACKAGE_NAME} + PUBLIC + "${CMAKE_SOURCE_DIR}" + "${RNGH_DIR}/shared" PRIVATE "${REACT_NATIVE_DIR}/ReactCommon" ) diff --git a/packages/react-native-gesture-handler/android/src/main/jni/OnLoad.cpp b/packages/react-native-gesture-handler/android/src/main/jni/OnLoad.cpp new file mode 100644 index 0000000000..00c060f009 --- /dev/null +++ b/packages/react-native-gesture-handler/android/src/main/jni/OnLoad.cpp @@ -0,0 +1,7 @@ +#include +#include "RNGestureHandlerModule.h" + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { + return facebook::jni::initialize( + vm, [] { gesturehandler::RNGestureHandlerModule::registerNatives(); }); +} diff --git a/packages/react-native-gesture-handler/android/src/main/jni/RNGestureHandlerModule.cpp b/packages/react-native-gesture-handler/android/src/main/jni/RNGestureHandlerModule.cpp new file mode 100644 index 0000000000..faa2f2cd18 --- /dev/null +++ b/packages/react-native-gesture-handler/android/src/main/jni/RNGestureHandlerModule.cpp @@ -0,0 +1,67 @@ +#include +#include + +#include "RNGestureHandlerModule.h" + +namespace gesturehandler { +using namespace facebook; +using namespace facebook::react; + +RNGestureHandlerModule::RNGestureHandlerModule( + jni::alias_ref jThis) + : javaPart_(jni::make_global(jThis)) {} + +RNGestureHandlerModule::~RNGestureHandlerModule() {} + +void RNGestureHandlerModule::registerNatives() { + registerHybrid( + {makeNativeMethod("initHybrid", RNGestureHandlerModule::initHybrid), + makeNativeMethod( + "getBindingsInstallerCxx", + RNGestureHandlerModule::getBindingsInstallerCxx), + makeNativeMethod( + "decorateUIRuntime", RNGestureHandlerModule::decorateUIRuntime), + makeNativeMethod( + "invalidateNative", RNGestureHandlerModule::invalidateNative)}); +} + +jni::local_ref +RNGestureHandlerModule::initHybrid(jni::alias_ref jThis) { + return makeCxxInstance(jThis); +} + +jni::local_ref +RNGestureHandlerModule::getBindingsInstallerCxx() { + return jni::make_local(BindingsInstallerHolder::newObjectCxxArgs( + [&, this](jsi::Runtime &runtime) { + this->rnRuntime_ = &runtime; + RNGHRuntimeDecorator::installRNRuntimeBindings( + runtime, [&](int handlerTag, int state) { + setGestureState(handlerTag, state); + }); + })); +} + +void RNGestureHandlerModule::setGestureState( + const int handlerTag, + const int state) { + static const auto method = + javaClassLocal()->getMethod("setGestureHandlerState"); + + method(this->javaPart_, handlerTag, state); +} + +bool RNGestureHandlerModule::decorateUIRuntime() { + return RNGHRuntimeDecorator::installUIRuntimeBindings( + *rnRuntime_, [&](int handlerTag, int state) { + this->setGestureState(handlerTag, state); + }); +} + +void RNGestureHandlerModule::invalidateNative() { + // This is called when the module is being destroyed, so we need to clear + // the reference to the java part to avoid memory leaks. + javaPart_ = nullptr; +} + +} // namespace gesturehandler diff --git a/packages/react-native-gesture-handler/android/src/main/jni/RNGestureHandlerModule.h b/packages/react-native-gesture-handler/android/src/main/jni/RNGestureHandlerModule.h new file mode 100644 index 0000000000..09749dac3b --- /dev/null +++ b/packages/react-native-gesture-handler/android/src/main/jni/RNGestureHandlerModule.h @@ -0,0 +1,34 @@ +#pragma once +#include +#include +#include + +namespace gesturehandler { +using namespace facebook; +using namespace facebook::jni; +using namespace facebook::react; + +class RNGestureHandlerModule : public jni::HybridClass { + public: + static auto constexpr kJavaDescriptor = + "Lcom/swmansion/gesturehandler/react/RNGestureHandlerModule;"; + static jni::local_ref initHybrid( + jni::alias_ref jThis); + static void registerNatives(); + ~RNGestureHandlerModule(); + + private: + friend HybridBase; + jsi::Runtime *rnRuntime_ = nullptr; + + jni::global_ref javaPart_; + explicit RNGestureHandlerModule( + jni::alias_ref jThis); + jni::local_ref getBindingsInstallerCxx(); + + void setGestureState(const int handlerTag, const int state); + void decorateRuntime(jsi::Runtime &runtime); + bool decorateUIRuntime(); + void invalidateNative(); +}; +} // namespace gesturehandler diff --git a/packages/react-native-gesture-handler/android/src/main/jni/cpp-adapter.cpp b/packages/react-native-gesture-handler/android/src/main/jni/cpp-adapter.cpp deleted file mode 100644 index cd2dfa72ff..0000000000 --- a/packages/react-native-gesture-handler/android/src/main/jni/cpp-adapter.cpp +++ /dev/null @@ -1,47 +0,0 @@ -#include -#include - -#include - -using namespace facebook; -using namespace react; - -void decorateRuntime(jsi::Runtime &runtime) { - auto isViewFlatteningDisabled = jsi::Function::createFromHostFunction( - runtime, - jsi::PropNameID::forAscii(runtime, "isViewFlatteningDisabled"), - 1, - [](jsi::Runtime &runtime, - const jsi::Value &thisValue, - const jsi::Value *arguments, - size_t count) -> jsi::Value { - if (!arguments[0].isObject()) { - return jsi::Value::null(); - } - - auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); - bool isViewFlatteningDisabled = shadowNode->getTraits().check( - ShadowNodeTraits::FormsStackingContext); - - // This is done using component names instead of type checking because - // of duplicate symbols for RN types, which prevent RTTI from working. - const char *componentName = shadowNode->getComponentName(); - bool isTextComponent = strcmp(componentName, "Paragraph") == 0 || - strcmp(componentName, "Text") == 0; - - return jsi::Value(isViewFlatteningDisabled || isTextComponent); - }); - runtime.global().setProperty( - runtime, "isViewFlatteningDisabled", std::move(isViewFlatteningDisabled)); -} - -extern "C" JNIEXPORT void JNICALL -Java_com_swmansion_gesturehandler_react_RNGestureHandlerModule_decorateRuntime( - JNIEnv *env, - jobject clazz, - jlong jsiPtr) { - jsi::Runtime *runtime = reinterpret_cast(jsiPtr); - if (runtime) { - decorateRuntime(*runtime); - } -} diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm index 594eba35a1..5d19446a44 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm @@ -13,33 +13,33 @@ #import #import #import - -#import -#import -#import +#import #endif // RCT_NEW_ARCH_ENABLED +#import "RNGHRuntimeDecorator.h" + #import "RNGestureHandler.h" #import "RNGestureHandlerDirection.h" #import "RNGestureHandlerManager.h" #import "RNGestureHandlerState.h" #import "RNGestureHandlerButton.h" -#import "RNGestureHandlerStateManager.h" #import +#import -#ifdef RCT_NEW_ARCH_ENABLED +using namespace gesturehandler; using namespace facebook; +#ifdef RCT_NEW_ARCH_ENABLED using namespace react; #endif // RCT_NEW_ARCH_ENABLED #ifdef RCT_NEW_ARCH_ENABLED -@interface RNGestureHandlerModule () +@interface RNGestureHandlerModule () @end #else -@interface RNGestureHandlerModule () +@interface RNGestureHandlerModule () @end #endif // RCT_NEW_ARCH_ENABLED @@ -51,11 +51,16 @@ @implementation RNGestureHandlerModule { // Oparations called after views have been updated. NSMutableArray *_operations; + + jsi::Runtime *_rnRuntime; + + bool _checkedIfReanimatedIsAvailable; + bool _isReanimatedAvailable; + bool _uiRuntimeDecorated; } #ifdef RCT_NEW_ARCH_ENABLED @synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED; -@synthesize dispatchToJSThread = _dispatchToJSThread; #endif // RCT_NEW_ARCH_ENABLED RCT_EXPORT_MODULE() @@ -91,31 +96,18 @@ - (dispatch_queue_t)methodQueue } #ifdef RCT_NEW_ARCH_ENABLED -void decorateRuntime(jsi::Runtime &runtime) +- (void)installJSIBindingsWithRuntime:(jsi::Runtime &)rnRuntime + callInvoker:(const std::shared_ptr &)callinvoker { - auto isViewFlatteningDisabled = jsi::Function::createFromHostFunction( - runtime, - jsi::PropNameID::forAscii(runtime, "isViewFlatteningDisabled"), - 1, - [](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *arguments, size_t count) -> jsi::Value { - if (!arguments[0].isObject()) { - return jsi::Value::null(); - } - auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); - - if (dynamic_pointer_cast(shadowNode)) { - return jsi::Value(true); - } - - if (dynamic_pointer_cast(shadowNode)) { - return jsi::Value(true); - } - - bool isViewFlatteningDisabled = shadowNode->getTraits().check(ShadowNodeTraits::FormsStackingContext); + _rnRuntime = &rnRuntime; + __weak RNGestureHandlerModule *weakSelf = self; - return jsi::Value(isViewFlatteningDisabled); - }); - runtime.global().setProperty(runtime, "isViewFlatteningDisabled", std::move(isViewFlatteningDisabled)); + RNGHRuntimeDecorator::installRNRuntimeBindings(rnRuntime, [weakSelf](int handlerTag, int state) { + RNGestureHandlerModule *strongSelf = weakSelf; + if (strongSelf != nil) { + [strongSelf setGestureState:state forHandler:handlerTag]; + } + }); } #endif // RCT_NEW_ARCH_ENABLED @@ -139,35 +131,36 @@ - (void)setBridge:(RCTBridge *)bridge } #endif // RCT_NEW_ARCH_ENABLED -#ifdef RCT_NEW_ARCH_ENABLED -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install) +- (bool)installUIRuntimeBindings { - dispatch_block_t block = ^{ - RCTCxxBridge *cxxBridge = (RCTCxxBridge *)self.bridge; - auto runtime = (jsi::Runtime *)cxxBridge.runtime; - decorateRuntime(*runtime); - }; - if (_dispatchToJSThread) { - _dispatchToJSThread(block); - } else { - [self.bridge dispatchBlock:block queue:RCTJSThread]; - } + __weak RNGestureHandlerModule *weakSelf = self; - return @true; + return RNGHRuntimeDecorator::installUIRuntimeBindings(*_rnRuntime, [weakSelf](int handlerTag, int state) { + RNGestureHandlerModule *strongSelf = weakSelf; + if (strongSelf != nil) { + [strongSelf setGestureState:state forHandler:handlerTag]; + } + }); } -#endif // RCT_NEW_ARCH_ENABLED -RCT_EXPORT_METHOD(createGestureHandler - : (nonnull NSString *)handlerName handlerTag - : (double)handlerTag config - : (NSDictionary *)config) +- (NSNumber *)createGestureHandler:(NSString *)handlerName handlerTag:(double)handlerTag config:(NSDictionary *)config { + if (!_checkedIfReanimatedIsAvailable) { + _isReanimatedAvailable = [self.moduleRegistry moduleForName:"ReanimatedModule"] != nil; + } + + if (_isReanimatedAvailable && !_uiRuntimeDecorated) { + _uiRuntimeDecorated = [self installUIRuntimeBindings]; + } + [self addOperationBlock:^(RNGestureHandlerManager *manager) { [manager createGestureHandler:handlerName tag:[NSNumber numberWithDouble:handlerTag] config:config]; }]; + + return @1; } -RCT_EXPORT_METHOD(attachGestureHandler : (double)handlerTag newView : (double)viewTag actionType : (double)actionType) +- (void)attachGestureHandler:(double)handlerTag newView:(double)viewTag actionType:(double)actionType { [self addOperationBlock:^(RNGestureHandlerManager *manager) { [manager attachGestureHandler:[NSNumber numberWithDouble:handlerTag] @@ -176,35 +169,35 @@ - (void)setBridge:(RCTBridge *)bridge }]; } -RCT_EXPORT_METHOD(updateGestureHandler : (double)handlerTag newConfig : (NSDictionary *)config) +- (void)updateGestureHandler:(double)handlerTag newConfig:(NSDictionary *)config { [self addOperationBlock:^(RNGestureHandlerManager *manager) { [manager updateGestureHandler:[NSNumber numberWithDouble:handlerTag] config:config]; }]; } -RCT_EXPORT_METHOD(dropGestureHandler : (double)handlerTag) +- (void)dropGestureHandler:(double)handlerTag { [self addOperationBlock:^(RNGestureHandlerManager *manager) { [manager dropGestureHandler:[NSNumber numberWithDouble:handlerTag]]; }]; } -RCT_EXPORT_METHOD(handleSetJSResponder : (double)viewTag blockNativeResponder : (BOOL)blockNativeResponder) +- (void)handleSetJSResponder:(double)viewTag blockNativeResponder:(BOOL)blockNativeResponder { [self addOperationBlock:^(RNGestureHandlerManager *manager) { [manager handleSetJSResponder:[NSNumber numberWithDouble:viewTag] blockNativeResponder:blockNativeResponder]; }]; } -RCT_EXPORT_METHOD(handleClearJSResponder) +- (void)handleClearJSResponder { [self addOperationBlock:^(RNGestureHandlerManager *manager) { [manager handleClearJSResponder]; }]; } -RCT_EXPORT_METHOD(flushOperations) +- (void)flushOperations { // On the new arch we rely on `flushOperations` for scheduling the operations on the UI thread. // On the old arch we rely on `uiManagerWillPerformMounting` @@ -226,6 +219,18 @@ - (void)setBridge:(RCTBridge *)bridge - (void)setGestureState:(int)state forHandler:(int)handlerTag { + if (RCTIsMainQueue()) { + [self setGestureStateSync:state forHandler:handlerTag]; + } else { + RCTExecuteOnMainQueue(^{ + [self setGestureStateSync:state forHandler:handlerTag]; + }); + } +} + +- (void)setGestureStateSync:(int)state forHandler:(int)handlerTag +{ + RCTAssertMainQueue(); RNGestureHandler *handler = [_manager handlerWithTag:@(handlerTag)]; if (handler != nil) { diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerStateManager.h b/packages/react-native-gesture-handler/apple/RNGestureHandlerStateManager.h deleted file mode 100644 index e927e96a56..0000000000 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerStateManager.h +++ /dev/null @@ -1,5 +0,0 @@ -@protocol RNGestureHandlerStateManager - -- (void)setGestureState:(int)state forHandler:(int)handlerTag; - -@end diff --git a/packages/react-native-gesture-handler/package.json b/packages/react-native-gesture-handler/package.json index 9cb308a533..56ecd61260 100644 --- a/packages/react-native-gesture-handler/package.json +++ b/packages/react-native-gesture-handler/package.json @@ -9,7 +9,8 @@ "format-js": "prettier --write --list-different './src/**/*.{js,jsx,ts,tsx}'", "format:js": "yarn format-js", "format:android": "node ../../scripts/format-android.js", - "format:apple": "node ../../scripts/format-apple.js", + "format:apple": "FORMAT_GLOB_PATTERN=\"../packages/react-native-gesture-handler/apple/**/*.{h,m,mm,cpp}\" node ../../scripts/format-cpp.js", + "format:cpp": "FORMAT_GLOB_PATTERN=\"../packages/react-native-gesture-handler/{shared,android/src}/**/*.{h,cpp}\" node ../../scripts/format-cpp.js", "lint-js": "eslint --ext '.js,.ts,.tsx' src/ && yarn prettier --check './src/**/*.{js,jsx,ts,tsx}'", "lint:js": "yarn lint-js", "lint:android": "./android/gradlew -p android spotlessCheck -q", @@ -46,6 +47,7 @@ "android/svg", "android/nosvg", "apple/", + "shared/", "Swipeable/", "ReanimatedSwipeable/", "jest-utils/", diff --git a/packages/react-native-gesture-handler/shared/RNGHRuntimeDecorator.cpp b/packages/react-native-gesture-handler/shared/RNGHRuntimeDecorator.cpp new file mode 100644 index 0000000000..0406943186 --- /dev/null +++ b/packages/react-native-gesture-handler/shared/RNGHRuntimeDecorator.cpp @@ -0,0 +1,124 @@ +#ifndef ANDROID +#include +#include +#endif + +#include + +#include "RNGHRuntimeDecorator.h" + +namespace gesturehandler { + +using namespace facebook; +using namespace facebook::react; + +void RNGHRuntimeDecorator::installRNRuntimeBindings( + jsi::Runtime &rnRuntime, + std::function &&setGestureState) { + const auto isViewFlatteningDisabled = jsi::Function::createFromHostFunction( + rnRuntime, + jsi::PropNameID::forAscii(rnRuntime, "_isViewFlatteningDisabled"), + 1, + [](jsi::Runtime &runtime, + const jsi::Value &, + const jsi::Value *args, + size_t argumentCount) -> jsi::Value { + if (!args[0].isObject()) { + return jsi::Value::null(); + } + + const auto shadowNode = shadowNodeFromValue(runtime, args[0]); + +#ifndef ANDROID + if (dynamic_pointer_cast(shadowNode)) { + return jsi::Value(true); + } + + if (dynamic_pointer_cast(shadowNode)) { + return jsi::Value(true); + } +#endif + + const auto isFormsStackingContext = shadowNode->getTraits().check( + ShadowNodeTraits::FormsStackingContext); + + // This is done using component names instead of type checking because + // of duplicate symbols for RN types, which prevent RTTI from working. + const auto &componentName = shadowNode->getComponentName(); + const auto isTextOrParagraphComponent = + strcmp(componentName, "Paragraph") == 0 || + strcmp(componentName, "Text") == 0; + + return jsi::Value(isFormsStackingContext || isTextOrParagraphComponent); + }); + + rnRuntime.global().setProperty( + rnRuntime, + "_isViewFlatteningDisabled", + std::move(isViewFlatteningDisabled)); + + auto setGestureStateAsync = jsi::Function::createFromHostFunction( + rnRuntime, + jsi::PropNameID::forAscii(rnRuntime, "_setGestureStateAsync"), + 2, + [setGestureState]( + jsi::Runtime &rt, + const jsi::Value &, + const jsi::Value *args, + size_t argumentCount) -> jsi::Value { + if (argumentCount == 2) { + const auto handlerTag = static_cast(args[0].asNumber()); + const auto state = static_cast(args[1].asNumber()); + + setGestureState(handlerTag, state); + } + return jsi::Value::undefined(); + }); + + rnRuntime.global().setProperty( + rnRuntime, "_setGestureStateAsync", std::move(setGestureStateAsync)); +} + +bool RNGHRuntimeDecorator::installUIRuntimeBindings( + jsi::Runtime &rnRuntime, + std::function &&setGestureState) { + const auto runtimeHolder = + rnRuntime.global().getProperty(rnRuntime, "_WORKLET_RUNTIME"); + + if (runtimeHolder.isUndefined()) { + return false; + } + + const auto arrayBufferValue = + runtimeHolder.getObject(rnRuntime).getArrayBuffer(rnRuntime).data( + rnRuntime); + const auto uiRuntimeAddress = + reinterpret_cast(&arrayBufferValue[0]); + jsi::Runtime &uiRuntime = + *reinterpret_cast(*uiRuntimeAddress); + + auto setGestureStateSync = jsi::Function::createFromHostFunction( + uiRuntime, + jsi::PropNameID::forAscii(uiRuntime, "_setGestureStateSync"), + 2, + [setGestureState]( + jsi::Runtime &rt, + const jsi::Value &, + const jsi::Value *args, + size_t argumentCount) -> jsi::Value { + if (argumentCount == 2) { + const auto handlerTag = static_cast(args[0].asNumber()); + const auto state = static_cast(args[1].asNumber()); + + setGestureState(handlerTag, state); + } + return jsi::Value::undefined(); + }); + + uiRuntime.global().setProperty( + uiRuntime, "_setGestureStateSync", std::move(setGestureStateSync)); + + return true; +} + +} // namespace gesturehandler diff --git a/packages/react-native-gesture-handler/shared/RNGHRuntimeDecorator.h b/packages/react-native-gesture-handler/shared/RNGHRuntimeDecorator.h new file mode 100644 index 0000000000..17be4fd990 --- /dev/null +++ b/packages/react-native-gesture-handler/shared/RNGHRuntimeDecorator.h @@ -0,0 +1,17 @@ +#pragma once +#include + +namespace gesturehandler { +using namespace facebook; + +class RNGHRuntimeDecorator { + public: + static void installRNRuntimeBindings( + jsi::Runtime &rnRuntime, + std::function &&setGestureState); + static bool installUIRuntimeBindings( + jsi::Runtime &rnRuntime, + std::function &&setGestureState); +}; + +} // namespace gesturehandler diff --git a/packages/react-native-gesture-handler/src/components/GestureHandlerRootView.android.tsx b/packages/react-native-gesture-handler/src/components/GestureHandlerRootView.android.tsx index 6b5eb466cf..8e019b60dc 100644 --- a/packages/react-native-gesture-handler/src/components/GestureHandlerRootView.android.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureHandlerRootView.android.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { PropsWithChildren } from 'react'; import { ViewProps, StyleSheet } from 'react-native'; -import { maybeInitializeFabric } from '../init'; import GestureHandlerRootViewContext from '../GestureHandlerRootViewContext'; import GestureHandlerRootViewNativeComponent from '../specs/RNGestureHandlerRootViewNativeComponent'; @@ -12,11 +11,6 @@ export default function GestureHandlerRootView({ style, ...rest }: GestureHandlerRootViewProps) { - // Try initialize fabric on the first render, at this point we can - // reliably check if fabric is enabled (the function contains a flag - // to make sure it's called only once) - maybeInitializeFabric(); - return ( diff --git a/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/useViewRefHandler.ts b/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/useViewRefHandler.ts index 10679000c6..b62c1943f6 100644 --- a/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/useViewRefHandler.ts +++ b/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/useViewRefHandler.ts @@ -6,7 +6,7 @@ import React, { useCallback } from 'react'; import findNodeHandle from '../../../findNodeHandle'; declare const global: { - isViewFlatteningDisabled: (node: unknown) => boolean | null; // JSI function + _isViewFlatteningDisabled: (node: unknown) => boolean | null; // JSI function }; // Ref handler for the Wrap component attached under the GestureDetector. @@ -35,9 +35,9 @@ export function useViewRefHandler( updateAttachedGestures(true); } - if (__DEV__ && isFabric() && global.isViewFlatteningDisabled) { + if (__DEV__ && isFabric() && global._isViewFlatteningDisabled) { const node = getShadowNodeFromRef(ref); - if (global.isViewFlatteningDisabled(node) === false) { + if (global._isViewFlatteningDisabled(node) === false) { console.error( tagMessage( 'GestureDetector has received a child that may get view-flattened. ' + diff --git a/packages/react-native-gesture-handler/src/handlers/gestures/gestureStateManager.ts b/packages/react-native-gesture-handler/src/handlers/gestures/gestureStateManager.ts index 2f77095b56..2faa4250d7 100644 --- a/packages/react-native-gesture-handler/src/handlers/gestures/gestureStateManager.ts +++ b/packages/react-native-gesture-handler/src/handlers/gestures/gestureStateManager.ts @@ -1,4 +1,3 @@ -import { Reanimated } from './reanimatedWrapper'; import { State } from '../../State'; import { tagMessage } from '../../utils'; @@ -9,60 +8,45 @@ export interface GestureStateManagerType { end: () => void; } -const warningMessage = tagMessage( - 'react-native-reanimated is required in order to use synchronous state management' -); +// Declare methods to keep the TS happy +declare const globalThis: { + _setGestureStateSync?: (handlerTag: number, state: State) => void; + _setGestureStateAsync?: (handlerTag: number, state: State) => void; +}; + +const wrappedSetGestureState = (handlerTag: number, state: State) => { + 'worklet'; -// Check if reanimated module is available, but look for useSharedValue as conditional -// require of reanimated can sometimes return content of `utils.ts` file (?) -const REANIMATED_AVAILABLE = Reanimated?.useSharedValue !== undefined; -const setGestureState = Reanimated?.setGestureState; + if (globalThis._setGestureStateSync) { + globalThis._setGestureStateSync(handlerTag, state); + } else if (globalThis._setGestureStateAsync) { + globalThis._setGestureStateAsync(handlerTag, state); + } else { + throw new Error(tagMessage('Failed to set gesture state')); + } +}; function create(handlerTag: number): GestureStateManagerType { 'worklet'; return { begin: () => { 'worklet'; - if (REANIMATED_AVAILABLE) { - // When Reanimated is available, setGestureState should be defined - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - setGestureState!(handlerTag, State.BEGAN); - } else { - console.warn(warningMessage); - } + wrappedSetGestureState(handlerTag, State.BEGAN); }, activate: () => { 'worklet'; - if (REANIMATED_AVAILABLE) { - // When Reanimated is available, setGestureState should be defined - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - setGestureState!(handlerTag, State.ACTIVE); - } else { - console.warn(warningMessage); - } + wrappedSetGestureState(handlerTag, State.ACTIVE); }, fail: () => { 'worklet'; - if (REANIMATED_AVAILABLE) { - // When Reanimated is available, setGestureState should be defined - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - setGestureState!(handlerTag, State.FAILED); - } else { - console.warn(warningMessage); - } + wrappedSetGestureState(handlerTag, State.FAILED); }, end: () => { 'worklet'; - if (REANIMATED_AVAILABLE) { - // When Reanimated is available, setGestureState should be defined - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - setGestureState!(handlerTag, State.END); - } else { - console.warn(warningMessage); - } + wrappedSetGestureState(handlerTag, State.END); }, }; } diff --git a/packages/react-native-gesture-handler/src/init.ts b/packages/react-native-gesture-handler/src/init.ts index 078cd51657..11fb7ae64b 100644 --- a/packages/react-native-gesture-handler/src/init.ts +++ b/packages/react-native-gesture-handler/src/init.ts @@ -1,18 +1,5 @@ import { startListening } from './handlers/gestures/eventReceiver'; -import RNGestureHandlerModule from './RNGestureHandlerModule'; -import { isFabric } from './utils'; - -let fabricInitialized = false; export function initialize() { startListening(); } - -// Since isFabric() may give wrong results before the first render, we call this -// method during render of GestureHandlerRootView -export function maybeInitializeFabric() { - if (isFabric() && !fabricInitialized) { - RNGestureHandlerModule.install(); - fabricInitialized = true; - } -} diff --git a/packages/react-native-gesture-handler/src/specs/NativeRNGestureHandlerModule.ts b/packages/react-native-gesture-handler/src/specs/NativeRNGestureHandlerModule.ts index 21b1983d9d..da74171519 100644 --- a/packages/react-native-gesture-handler/src/specs/NativeRNGestureHandlerModule.ts +++ b/packages/react-native-gesture-handler/src/specs/NativeRNGestureHandlerModule.ts @@ -4,13 +4,15 @@ import { Double } from 'react-native/Libraries/Types/CodegenTypes'; export interface Spec extends TurboModule { handleSetJSResponder: (tag: Double, blockNativeResponder: boolean) => void; handleClearJSResponder: () => void; + // This method returns a boolean only to force the codegen to generate + // a synchronous method. The returned value doesn't have any meaning. createGestureHandler: ( handlerName: string, handlerTag: Double, // Record<> is not supported by codegen // eslint-disable-next-line @typescript-eslint/ban-types config: Object - ) => void; + ) => boolean; attachGestureHandler: ( handlerTag: Double, newView: Double, @@ -19,7 +21,6 @@ export interface Spec extends TurboModule { // eslint-disable-next-line @typescript-eslint/ban-types updateGestureHandler: (handlerTag: Double, newConfig: Object) => void; dropGestureHandler: (handlerTag: Double) => void; - install: () => boolean; flushOperations: () => void; } diff --git a/packages/react-native-gesture-handler/src/utils.ts b/packages/react-native-gesture-handler/src/utils.ts index cb035a3d85..8faf8bdf64 100644 --- a/packages/react-native-gesture-handler/src/utils.ts +++ b/packages/react-native-gesture-handler/src/utils.ts @@ -41,6 +41,7 @@ export function isTestEnv(): boolean { } export function tagMessage(msg: string) { + 'worklet'; return `[react-native-gesture-handler] ${msg}`; } diff --git a/scripts/format-apple.js b/scripts/format-cpp.js similarity index 75% rename from scripts/format-apple.js rename to scripts/format-cpp.js index 2f8d64f7ae..77482051d4 100755 --- a/scripts/format-apple.js +++ b/scripts/format-cpp.js @@ -21,10 +21,13 @@ if (argc > 2) { const files = process.argv.slice(2).join(' '); runFormatter(files); } else { - const pattern = path.join( - __dirname, - '../packages/react-native-gesture-handler/apple/**/*.{h,m,mm,cpp}' - ); + const globPattern = process.env.FORMAT_GLOB_PATTERN + if (!globPattern) { + console.error('FORMAT_GLOB_PATTERN environment variable is not set.'); + return exit(1); + } + + const pattern = path.join(__dirname, globPattern); glob(pattern, (err, filesArray) => { if (err) { diff --git a/yarn.lock b/yarn.lock index e73f74fb90..9ecee56791 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5959,6 +5959,8 @@ __metadata: react: "npm:19.1.0" react-native: "npm:0.80.0" react-native-gesture-handler: "workspace:*" + react-native-reanimated: "npm:4.0.0-beta.5" + react-native-worklets: "npm:^0.3.0" react-test-renderer: "npm:19.1.0" typescript: "npm:~5.8.3" languageName: unknown @@ -14119,6 +14121,21 @@ __metadata: languageName: node linkType: hard +"react-native-reanimated@npm:4.0.0-beta.5": + version: 4.0.0-beta.5 + resolution: "react-native-reanimated@npm:4.0.0-beta.5" + dependencies: + react-native-is-edge-to-edge: "npm:1.1.7" + semver: "npm:^7.7.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + react: "*" + react-native: "*" + react-native-worklets: ">=0.3.0" + checksum: 10c0/446acddd80e7913fed951dd689a368b2052690f59b24d0aa6af2dd87cfa710e882d5ba2c6e241ba5f0f3978c1d25bbe94e14562ef5cf88ba442ea1b918547973 + languageName: node + linkType: hard + "react-native-reanimated@npm:^3.18.0": version: 3.18.0 resolution: "react-native-reanimated@npm:3.18.0" @@ -14210,54 +14227,77 @@ __metadata: languageName: node linkType: hard -"react-native@npm:*, react-native@npm:0.80.0": - version: 0.80.0 - resolution: "react-native@npm:0.80.0" +"react-native-worklets@npm:^0.3.0": + version: 0.3.0 + resolution: "react-native-worklets@npm:0.3.0" + dependencies: + "@babel/plugin-transform-arrow-functions": "npm:^7.0.0-0" + "@babel/plugin-transform-class-properties": "npm:^7.0.0-0" + "@babel/plugin-transform-classes": "npm:^7.0.0-0" + "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.0.0-0" + "@babel/plugin-transform-optional-chaining": "npm:^7.0.0-0" + "@babel/plugin-transform-shorthand-properties": "npm:^7.0.0-0" + "@babel/plugin-transform-template-literals": "npm:^7.0.0-0" + "@babel/plugin-transform-unicode-regex": "npm:^7.0.0-0" + "@babel/preset-typescript": "npm:^7.16.7" + convert-source-map: "npm:^2.0.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + react: "*" + react-native: "*" + checksum: 10c0/db57580495699969035f9e04d66d0254ac4e819fe0ebbe3705e7e9cbc6b486753531631ee7d9cf94bfb75f685a587867f46a9525d83bf683faef7831961cf840 + languageName: node + linkType: hard + +"react-native@npm:*, react-native@npm:0.79.2": + version: 0.79.2 + resolution: "react-native@npm:0.79.2" dependencies: "@jest/create-cache-key-function": "npm:^29.7.0" - "@react-native/assets-registry": "npm:0.80.0" - "@react-native/codegen": "npm:0.80.0" - "@react-native/community-cli-plugin": "npm:0.80.0" - "@react-native/gradle-plugin": "npm:0.80.0" - "@react-native/js-polyfills": "npm:0.80.0" - "@react-native/normalize-colors": "npm:0.80.0" - "@react-native/virtualized-lists": "npm:0.80.0" + "@react-native/assets-registry": "npm:0.79.2" + "@react-native/codegen": "npm:0.79.2" + "@react-native/community-cli-plugin": "npm:0.79.2" + "@react-native/gradle-plugin": "npm:0.79.2" + "@react-native/js-polyfills": "npm:0.79.2" + "@react-native/normalize-colors": "npm:0.79.2" + "@react-native/virtualized-lists": "npm:0.79.2" abort-controller: "npm:^3.0.0" anser: "npm:^1.4.9" ansi-regex: "npm:^5.0.0" babel-jest: "npm:^29.7.0" - babel-plugin-syntax-hermes-parser: "npm:0.28.1" + babel-plugin-syntax-hermes-parser: "npm:0.25.1" base64-js: "npm:^1.5.1" chalk: "npm:^4.0.0" commander: "npm:^12.0.0" + event-target-shim: "npm:^5.0.1" flow-enums-runtime: "npm:^0.0.6" glob: "npm:^7.1.1" invariant: "npm:^2.2.4" jest-environment-node: "npm:^29.7.0" memoize-one: "npm:^5.0.0" - metro-runtime: "npm:^0.82.2" - metro-source-map: "npm:^0.82.2" + metro-runtime: "npm:^0.82.0" + metro-source-map: "npm:^0.82.0" nullthrows: "npm:^1.1.1" pretty-format: "npm:^29.7.0" promise: "npm:^8.3.0" react-devtools-core: "npm:^6.1.1" react-refresh: "npm:^0.14.0" regenerator-runtime: "npm:^0.13.2" - scheduler: "npm:0.26.0" + scheduler: "npm:0.25.0" semver: "npm:^7.1.3" stacktrace-parser: "npm:^0.1.10" whatwg-fetch: "npm:^3.0.0" ws: "npm:^6.2.3" yargs: "npm:^17.6.2" peerDependencies: - "@types/react": ^19.1.0 - react: ^19.1.0 + "@types/react": ^19.0.0 + react: ^19.0.0 peerDependenciesMeta: "@types/react": optional: true bin: react-native: cli.js - checksum: 10c0/a39e90d6e7d082a3d31c04b1c9170cf8eb632f08b0c8537d953db46885b19dbc0b067a156ffd373ff3ce8b46778ec657a0793c26a9073877984ccd25b1100795 + checksum: 10c0/6c9b05a74abe128a70b0bfabb286ccb2e61273af3e4a0f041f758dcf8e9b7e00722669510fb9d4d7dac6eee0078944ec01e38fd3edbb57e0ffda347c0dc07d71 languageName: node linkType: hard @@ -14313,55 +14353,54 @@ __metadata: languageName: node linkType: hard -"react-native@npm:0.79.2": - version: 0.79.2 - resolution: "react-native@npm:0.79.2" +"react-native@npm:0.80.0": + version: 0.80.0 + resolution: "react-native@npm:0.80.0" dependencies: "@jest/create-cache-key-function": "npm:^29.7.0" - "@react-native/assets-registry": "npm:0.79.2" - "@react-native/codegen": "npm:0.79.2" - "@react-native/community-cli-plugin": "npm:0.79.2" - "@react-native/gradle-plugin": "npm:0.79.2" - "@react-native/js-polyfills": "npm:0.79.2" - "@react-native/normalize-colors": "npm:0.79.2" - "@react-native/virtualized-lists": "npm:0.79.2" + "@react-native/assets-registry": "npm:0.80.0" + "@react-native/codegen": "npm:0.80.0" + "@react-native/community-cli-plugin": "npm:0.80.0" + "@react-native/gradle-plugin": "npm:0.80.0" + "@react-native/js-polyfills": "npm:0.80.0" + "@react-native/normalize-colors": "npm:0.80.0" + "@react-native/virtualized-lists": "npm:0.80.0" abort-controller: "npm:^3.0.0" anser: "npm:^1.4.9" ansi-regex: "npm:^5.0.0" babel-jest: "npm:^29.7.0" - babel-plugin-syntax-hermes-parser: "npm:0.25.1" + babel-plugin-syntax-hermes-parser: "npm:0.28.1" base64-js: "npm:^1.5.1" chalk: "npm:^4.0.0" commander: "npm:^12.0.0" - event-target-shim: "npm:^5.0.1" flow-enums-runtime: "npm:^0.0.6" glob: "npm:^7.1.1" invariant: "npm:^2.2.4" jest-environment-node: "npm:^29.7.0" memoize-one: "npm:^5.0.0" - metro-runtime: "npm:^0.82.0" - metro-source-map: "npm:^0.82.0" + metro-runtime: "npm:^0.82.2" + metro-source-map: "npm:^0.82.2" nullthrows: "npm:^1.1.1" pretty-format: "npm:^29.7.0" promise: "npm:^8.3.0" react-devtools-core: "npm:^6.1.1" react-refresh: "npm:^0.14.0" regenerator-runtime: "npm:^0.13.2" - scheduler: "npm:0.25.0" + scheduler: "npm:0.26.0" semver: "npm:^7.1.3" stacktrace-parser: "npm:^0.1.10" whatwg-fetch: "npm:^3.0.0" ws: "npm:^6.2.3" yargs: "npm:^17.6.2" peerDependencies: - "@types/react": ^19.0.0 - react: ^19.0.0 + "@types/react": ^19.1.0 + react: ^19.1.0 peerDependenciesMeta: "@types/react": optional: true bin: react-native: cli.js - checksum: 10c0/6c9b05a74abe128a70b0bfabb286ccb2e61273af3e4a0f041f758dcf8e9b7e00722669510fb9d4d7dac6eee0078944ec01e38fd3edbb57e0ffda347c0dc07d71 + checksum: 10c0/a39e90d6e7d082a3d31c04b1c9170cf8eb632f08b0c8537d953db46885b19dbc0b067a156ffd373ff3ce8b46778ec657a0793c26a9073877984ccd25b1100795 languageName: node linkType: hard @@ -14930,7 +14969,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.1.3, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0": +"semver@npm:^7.1.3, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.7.1": version: 7.7.2 resolution: "semver@npm:7.7.2" bin: From 1610fc18ff1a127f8887902b7b8ff9b5eb51539d Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 7 Jul 2025 09:49:32 +0200 Subject: [PATCH 003/236] Simplify Android event builders (#3603) ## Description Merges declaration and initialization of fields in the event builder classes ## Test plan Build android --- .../FlingGestureHandlerEventDataBuilder.kt | 15 +++------- .../GestureHandlerEventDataBuilder.kt | 15 +++------- .../HoverGestureHandlerEventDataBuilder.kt | 18 ++++------- ...LongPressGestureHandlerEventDataBuilder.kt | 18 ++++------- .../NativeGestureHandlerEventDataBuilder.kt | 6 +--- .../PanGestureHandlerEventDataBuilder.kt | 30 ++++++------------- .../PinchGestureHandlerEventDataBuilder.kt | 15 +++------- .../RotationGestureHandlerEventDataBuilder.kt | 15 +++------- .../TapGestureHandlerEventDataBuilder.kt | 15 +++------- 9 files changed, 40 insertions(+), 107 deletions(-) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/FlingGestureHandlerEventDataBuilder.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/FlingGestureHandlerEventDataBuilder.kt index d692e3be1b..dfd6ec1fa5 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/FlingGestureHandlerEventDataBuilder.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/FlingGestureHandlerEventDataBuilder.kt @@ -6,17 +6,10 @@ import com.swmansion.gesturehandler.core.FlingGestureHandler class FlingGestureHandlerEventDataBuilder(handler: FlingGestureHandler) : GestureHandlerEventDataBuilder(handler) { - private val x: Float - private val y: Float - private val absoluteX: Float - private val absoluteY: Float - - init { - x = handler.lastRelativePositionX - y = handler.lastRelativePositionY - absoluteX = handler.lastPositionInWindowX - absoluteY = handler.lastPositionInWindowY - } + private val x: Float = handler.lastRelativePositionX + private val y: Float = handler.lastRelativePositionY + private val absoluteX: Float = handler.lastPositionInWindowX + private val absoluteY: Float = handler.lastPositionInWindowY override fun buildEventData(eventData: WritableMap) { super.buildEventData(eventData) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/GestureHandlerEventDataBuilder.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/GestureHandlerEventDataBuilder.kt index ba87c07acb..007cefc230 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/GestureHandlerEventDataBuilder.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/GestureHandlerEventDataBuilder.kt @@ -4,17 +4,10 @@ import com.facebook.react.bridge.WritableMap import com.swmansion.gesturehandler.core.GestureHandler abstract class GestureHandlerEventDataBuilder(handler: T) { - private val numberOfPointers: Int - private val handlerTag: Int - private val state: Int - private val pointerType: Int - - init { - numberOfPointers = handler.numberOfPointers - handlerTag = handler.tag - state = handler.state - pointerType = handler.pointerType - } + private val handlerTag: Int = handler.tag + private val state: Int = handler.state + private val pointerType: Int = handler.pointerType + private val numberOfPointers: Int = handler.numberOfPointers open fun buildEventData(eventData: WritableMap) { eventData.putInt("numberOfPointers", numberOfPointers) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/HoverGestureHandlerEventDataBuilder.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/HoverGestureHandlerEventDataBuilder.kt index 4fc93bcb3e..d746c5c64d 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/HoverGestureHandlerEventDataBuilder.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/HoverGestureHandlerEventDataBuilder.kt @@ -7,19 +7,11 @@ import com.swmansion.gesturehandler.core.StylusData class HoverGestureHandlerEventDataBuilder(handler: HoverGestureHandler) : GestureHandlerEventDataBuilder(handler) { - private val x: Float - private val y: Float - private val absoluteX: Float - private val absoluteY: Float - private val stylusData: StylusData - - init { - x = handler.lastRelativePositionX - y = handler.lastRelativePositionY - absoluteX = handler.lastPositionInWindowX - absoluteY = handler.lastPositionInWindowY - stylusData = handler.stylusData - } + private val x: Float = handler.lastRelativePositionX + private val y: Float = handler.lastRelativePositionY + private val absoluteX: Float = handler.lastPositionInWindowX + private val absoluteY: Float = handler.lastPositionInWindowY + private val stylusData: StylusData = handler.stylusData override fun buildEventData(eventData: WritableMap) { super.buildEventData(eventData) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/LongPressGestureHandlerEventDataBuilder.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/LongPressGestureHandlerEventDataBuilder.kt index 90fb9563c5..c5bb0deb29 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/LongPressGestureHandlerEventDataBuilder.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/LongPressGestureHandlerEventDataBuilder.kt @@ -6,19 +6,11 @@ import com.swmansion.gesturehandler.core.LongPressGestureHandler class LongPressGestureHandlerEventDataBuilder(handler: LongPressGestureHandler) : GestureHandlerEventDataBuilder(handler) { - private val x: Float - private val y: Float - private val absoluteX: Float - private val absoluteY: Float - private val duration: Int - - init { - x = handler.lastRelativePositionX - y = handler.lastRelativePositionY - absoluteX = handler.lastPositionInWindowX - absoluteY = handler.lastPositionInWindowY - duration = handler.duration - } + private val x: Float = handler.lastRelativePositionX + private val y: Float = handler.lastRelativePositionY + private val absoluteX: Float = handler.lastPositionInWindowX + private val absoluteY: Float = handler.lastPositionInWindowY + private val duration: Int = handler.duration override fun buildEventData(eventData: WritableMap) { super.buildEventData(eventData) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/NativeGestureHandlerEventDataBuilder.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/NativeGestureHandlerEventDataBuilder.kt index 5e5d364b6f..6ce41b007d 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/NativeGestureHandlerEventDataBuilder.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/NativeGestureHandlerEventDataBuilder.kt @@ -5,11 +5,7 @@ import com.swmansion.gesturehandler.core.NativeViewGestureHandler class NativeGestureHandlerEventDataBuilder(handler: NativeViewGestureHandler) : GestureHandlerEventDataBuilder(handler) { - private val pointerInside: Boolean - - init { - pointerInside = handler.isWithinBounds - } + private val pointerInside: Boolean = handler.isWithinBounds override fun buildEventData(eventData: WritableMap) { super.buildEventData(eventData) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/PanGestureHandlerEventDataBuilder.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/PanGestureHandlerEventDataBuilder.kt index 19aa530cd0..9121f7c9c8 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/PanGestureHandlerEventDataBuilder.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/PanGestureHandlerEventDataBuilder.kt @@ -7,27 +7,15 @@ import com.swmansion.gesturehandler.core.StylusData class PanGestureHandlerEventDataBuilder(handler: PanGestureHandler) : GestureHandlerEventDataBuilder(handler) { - private val x: Float - private val y: Float - private val absoluteX: Float - private val absoluteY: Float - private val translationX: Float - private val translationY: Float - private val velocityX: Float - private val velocityY: Float - private val stylusData: StylusData - - init { - x = handler.lastRelativePositionX - y = handler.lastRelativePositionY - absoluteX = handler.lastPositionInWindowX - absoluteY = handler.lastPositionInWindowY - translationX = handler.translationX - translationY = handler.translationY - velocityX = handler.velocityX - velocityY = handler.velocityY - stylusData = handler.stylusData - } + private val x: Float = handler.lastRelativePositionX + private val y: Float = handler.lastRelativePositionY + private val absoluteX: Float = handler.lastPositionInWindowX + private val absoluteY: Float = handler.lastPositionInWindowY + private val translationX: Float = handler.translationX + private val translationY: Float = handler.translationY + private val velocityX: Float = handler.velocityX + private val velocityY: Float = handler.velocityY + private val stylusData: StylusData = handler.stylusData override fun buildEventData(eventData: WritableMap) { super.buildEventData(eventData) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/PinchGestureHandlerEventDataBuilder.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/PinchGestureHandlerEventDataBuilder.kt index 66d1af8161..b9985145a1 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/PinchGestureHandlerEventDataBuilder.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/PinchGestureHandlerEventDataBuilder.kt @@ -6,17 +6,10 @@ import com.swmansion.gesturehandler.core.PinchGestureHandler class PinchGestureHandlerEventDataBuilder(handler: PinchGestureHandler) : GestureHandlerEventDataBuilder(handler) { - private val scale: Double - private val focalX: Float - private val focalY: Float - private val velocity: Double - - init { - scale = handler.scale - focalX = handler.focalPointX - focalY = handler.focalPointY - velocity = handler.velocity - } + private val scale: Double = handler.scale + private val focalX: Float = handler.focalPointX + private val focalY: Float = handler.focalPointY + private val velocity: Double = handler.velocity override fun buildEventData(eventData: WritableMap) { super.buildEventData(eventData) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/RotationGestureHandlerEventDataBuilder.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/RotationGestureHandlerEventDataBuilder.kt index 1fea1f17e2..503d40ce54 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/RotationGestureHandlerEventDataBuilder.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/RotationGestureHandlerEventDataBuilder.kt @@ -6,17 +6,10 @@ import com.swmansion.gesturehandler.core.RotationGestureHandler class RotationGestureHandlerEventDataBuilder(handler: RotationGestureHandler) : GestureHandlerEventDataBuilder(handler) { - private val rotation: Double - private val anchorX: Float - private val anchorY: Float - private val velocity: Double - - init { - rotation = handler.rotation - anchorX = handler.anchorX - anchorY = handler.anchorY - velocity = handler.velocity - } + private val rotation: Double = handler.rotation + private val anchorX: Float = handler.anchorX + private val anchorY: Float = handler.anchorY + private val velocity: Double = handler.velocity override fun buildEventData(eventData: WritableMap) { super.buildEventData(eventData) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/TapGestureHandlerEventDataBuilder.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/TapGestureHandlerEventDataBuilder.kt index 6e7f64cf20..eff9095715 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/TapGestureHandlerEventDataBuilder.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/eventbuilders/TapGestureHandlerEventDataBuilder.kt @@ -6,17 +6,10 @@ import com.swmansion.gesturehandler.core.TapGestureHandler class TapGestureHandlerEventDataBuilder(handler: TapGestureHandler) : GestureHandlerEventDataBuilder(handler) { - private val x: Float - private val y: Float - private val absoluteX: Float - private val absoluteY: Float - - init { - x = handler.lastRelativePositionX - y = handler.lastRelativePositionY - absoluteX = handler.lastPositionInWindowX - absoluteY = handler.lastPositionInWindowY - } + private val x: Float = handler.lastRelativePositionX + private val y: Float = handler.lastRelativePositionY + private val absoluteX: Float = handler.lastPositionInWindowX + private val absoluteY: Float = handler.lastPositionInWindowY override fun buildEventData(eventData: WritableMap) { super.buildEventData(eventData) From 207d1ba9d4750e430ef0b0091d42103710745edf Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 7 Jul 2025 10:38:01 +0200 Subject: [PATCH 004/236] Remove actions testing build for the old architecture (#3604) ## Description Remove actions testing build for the old architecture ## Test plan See CI --- .github/workflows/android-build-paper.yml | 46 ----------------------- .github/workflows/ios-build-paper.yml | 46 ----------------------- apps/expo-example/app.config.js | 4 +- apps/expo-example/package.json | 1 - 4 files changed, 1 insertion(+), 96 deletions(-) delete mode 100644 .github/workflows/android-build-paper.yml delete mode 100644 .github/workflows/ios-build-paper.yml diff --git a/.github/workflows/android-build-paper.yml b/.github/workflows/android-build-paper.yml deleted file mode 100644 index 69a8a672cc..0000000000 --- a/.github/workflows/android-build-paper.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Test Android build (Paper) - -on: - pull_request: - paths: - - .github/workflows/android-build-paper.yml - - packages/react-native-gesture-handler/android/** - push: - branches: - - main - workflow_dispatch: - -jobs: - build: - if: github.repository == 'software-mansion/react-native-gesture-handler' - - runs-on: ubuntu-latest - env: - WORKING_DIRECTORY: apps/expo-example - concurrency: - group: android-paper-${{ github.ref }} - cancel-in-progress: true - - steps: - - name: checkout - uses: actions/checkout@v4 - - - name: Use Java 17 - uses: actions/setup-java@v4 - with: - distribution: oracle - java-version: 17 - - - name: Use Node.js 18 - uses: actions/setup-node@v4 - with: - node-version: 18 - cache: yarn - - - name: Install node dependencies - working-directory: ${{ env.WORKING_DIRECTORY }} - run: PAPER_ENABLED=1 yarn install --immutable - - - name: Build app - working-directory: ${{ env.WORKING_DIRECTORY }}/android - run: ./gradlew assembleDebug --console=plain -PreactNativeArchitectures=arm64-v8a diff --git a/.github/workflows/ios-build-paper.yml b/.github/workflows/ios-build-paper.yml deleted file mode 100644 index 355cce536f..0000000000 --- a/.github/workflows/ios-build-paper.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Test iOS build (paper) - -on: - pull_request: - paths: - - .github/workflows/ios-build-paper.yml - - packages/react-native-gesture-handler/RNGestureHandler.podspec - - packages/react-native-gesture-handler/apple/** - push: - branches: - - main - workflow_dispatch: - -jobs: - build: - if: github.repository == 'software-mansion/react-native-gesture-handler' - - runs-on: macos-14 - env: - WORKING_DIRECTORY: apps/expo-example - concurrency: - group: ios-paper-${{ matrix.working-directory }}-${{ github.ref }} - cancel-in-progress: true - - steps: - - name: checkout - uses: actions/checkout@v4 - - - name: Use latest stable Xcode - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: '16.1' - - - name: Use Node.js 18 - uses: actions/setup-node@v4 - with: - node-version: 18 - cache: yarn - - - name: Install node dependencies - working-directory: ${{ env.WORKING_DIRECTORY }} - run: PAPER_ENABLED=1 yarn install --immutable - - - name: Build app - working-directory: ${{ env.WORKING_DIRECTORY }} - run: npx react-native run-ios diff --git a/apps/expo-example/app.config.js b/apps/expo-example/app.config.js index 42fb49ec64..ae274ac063 100644 --- a/apps/expo-example/app.config.js +++ b/apps/expo-example/app.config.js @@ -1,5 +1,3 @@ -const shouldEnablePaper = process.env.PAPER_ENABLED === '1'; - export default { expo: { name: 'ExpoExample', @@ -8,7 +6,7 @@ export default { orientation: 'portrait', icon: './assets/icon.png', userInterfaceStyle: 'light', - newArchEnabled: !shouldEnablePaper, + newArchEnabled: true, splash: { image: './assets/splash.png', resizeMode: 'cover', diff --git a/apps/expo-example/package.json b/apps/expo-example/package.json index caa60cd7e5..3ee04f48df 100644 --- a/apps/expo-example/package.json +++ b/apps/expo-example/package.json @@ -8,7 +8,6 @@ "android": "expo run:android", "ios": "expo run:ios", "web": "expo start --web", - "paper": "rm -rf android ios && PAPER_ENABLED=1 npx expo prebuild", "clean": "rm -rf node_modules android ios" }, "dependencies": { From 1df54194bd770c233cc741a89dc52f9b5b8400a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= <63123542+m-bert@users.noreply.github.com> Date: Thu, 3 Jul 2025 11:50:50 +0200 Subject: [PATCH 005/236] [iOS] Fix `GestureDetector` on `Text` (#3591) ## Description ### Problem Currently the following configuration: ```jsx ... ``` does not work on `iOS`. This is due to change `react-native` introduced in 0.79 - `hitTest` in `RCTParagraphTextView` now returns `nil` by default ([see here](https://github.com/facebook/react-native/blob/dcbbf275cbc4150820691a4fbc254b198cc92bdd/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm#L379)). This results in native `UIGestureRecognizer` not responding to touches. ### Solution We no longer attach native recognizer to `RCTParagraphTextView`, but to its parent - `RCTParagraphComponentView`. The problem with this approach is that `handleGesture` method uses `reactTag` property, which on `RCTParagraphComponentView` is `nil`. This is why we use `reactTag` from `RCTParagraphTextView` when sending event to `JS` side. Fixes #3581 ## Test plan
Tested on the following code: ```jsx import React from 'react'; import { StyleSheet, Text } from 'react-native'; import { Gesture, GestureDetector, GestureHandlerRootView, } from 'react-native-gesture-handler'; export default function EmptyExample() { const g = Gesture.Tap().onEnd(() => { console.log('Tapped!'); }); return ( Click me Me too! ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', }, }); ```
--- .../apple/RNGestureHandler.h | 4 ++ .../apple/RNGestureHandler.mm | 47 ++++++++++++++++--- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandler.h b/packages/react-native-gesture-handler/apple/RNGestureHandler.h index 15857abe0f..b8e021d4db 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandler.h +++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.h @@ -72,6 +72,10 @@ @property (nonatomic) BOOL needsPointerData; @property (nonatomic) BOOL manualActivation; +#if RCT_NEW_ARCH_ENABLED +- (BOOL)isViewParagraphComponent:(nullable RNGHUIView *)view; +#endif +- (nonnull RNGHUIView *)chooseViewForInteraction:(nonnull UIGestureRecognizer *)recognizer; - (void)bindToView:(nonnull RNGHUIView *)view; - (void)unbindFromView; - (void)resetConfig NS_REQUIRES_SUPER; diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm index 5bf0d32792..708172f47a 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm @@ -11,6 +11,7 @@ #import #ifdef RCT_NEW_ARCH_ENABLED +#import #import #else #import @@ -215,15 +216,32 @@ - (UITouchType)getPointerType return (UITouchType)_pointerType; } +#if RCT_NEW_ARCH_ENABLED +- (BOOL)isViewParagraphComponent:(RNGHUIView *)view +{ + return [view isKindOfClass:[RCTParagraphComponentView class]]; +} +#endif + - (void)bindToView:(RNGHUIView *)view { + self.recognizer.delegate = self; + +#if RCT_NEW_ARCH_ENABLED + // Starting from react-native 0.79 `RCTParagraphTextView` overrides `hitTest` method to return `nil`. This results in + // native `UIGestureRecognizer` not responding to gestures. To fix this issue, we attach recognizer to its parent, + // i.e. `RCTParagraphComponentView`. + RNGHUIView *recognizerView = [self isViewParagraphComponent:view.superview] ? view.superview : view; +#else + RNGHUIView *recognizerView = view; +#endif + #if !TARGET_OS_OSX - view.userInteractionEnabled = YES; + recognizerView.userInteractionEnabled = YES; #endif - self.recognizer.delegate = self; - [view addGestureRecognizer:self.recognizer]; - [self bindManualActivationToView:view]; + [recognizerView addGestureRecognizer:self.recognizer]; + [self bindManualActivationToView:recognizerView]; } - (void)unbindFromView @@ -249,12 +267,27 @@ - (RNGestureHandlerEventExtraData *)eventExtraData:(UIGestureRecognizer *)recogn #endif } +/** + This method is used in `handleGesture` to choose appropriate view. `reactTag` in `RCTParagraphComponentView` + is `nil`, therefore we want to use `reactTag` from `RCTParagraphTextView`. + */ +- (RNGHUIView *)chooseViewForInteraction:(UIGestureRecognizer *)recognizer +{ +#if RCT_NEW_ARCH_ENABLED + return [self isViewParagraphComponent:recognizer.view] ? recognizer.view.subviews[0] : recognizer.view; +#else + return recognizer.view; +#endif +} + - (void)handleGesture:(UIGestureRecognizer *)recognizer { + RNGHUIView *view = [self chooseViewForInteraction:recognizer]; + // it may happen that the gesture recognizer is reset after it's been unbound from the view, // it that recognizer tried to send event, the app would crash because the target of the event // would be nil. - if (recognizer.view.reactTag == nil) { + if (view.reactTag == nil) { return; } @@ -266,7 +299,9 @@ - (void)handleGesture:(UIGestureRecognizer *)recognizer inState:(RNGestureHandle { _state = state; RNGestureHandlerEventExtraData *eventData = [self eventExtraData:recognizer]; - [self sendEventsInState:self.state forViewWithTag:recognizer.view.reactTag withExtraData:eventData]; + RNGHUIView *view = [self chooseViewForInteraction:recognizer]; + + [self sendEventsInState:self.state forViewWithTag:view.reactTag withExtraData:eventData]; } - (void)sendEventsInState:(RNGestureHandlerState)state From 30401e7b33febbfc9900fd0129840335d6fa0ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= <63123542+m-bert@users.noreply.github.com> Date: Fri, 4 Jul 2025 12:16:18 +0200 Subject: [PATCH 006/236] [Android] Fix `onTouches*` callbacks being called for all gestures (#3596) ## Description Logic behind sending touch events on `android` dispatches events into all gesture handlers registered in the orchestrator. This means that interaction with one `GestureDetector` triggers callbacks on the others. This PR adds check for tracked pointer, so that handlers respond only to those that they are tracking. Fixes #3543 ## Test plan
Tested on the following example: ```jsx import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { Gesture, GestureDetector, GestureHandlerRootView, } from 'react-native-gesture-handler'; type BoxProps = { label: string; }; function Box({ label }: BoxProps) { const manual = Gesture.Manual() .onTouchesDown((e) => { console.log('down', label, e.handlerTag); }) .onTouchesUp((e) => { console.log('up', label, e.handlerTag); }) .onTouchesCancelled(() => { console.log('cancelled', label); }); return ( {label} ); } export default function EmptyExample() { return ( ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 10, }, box: { width: 100, height: 100, backgroundColor: 'red', alignItems: 'center', justifyContent: 'center', }, text: { color: 'white', fontSize: 20, fontWeight: 'bold', }, }); ```
--- .../gesturehandler/core/GestureHandler.kt | 22 ++++++++++++------- .../core/GestureHandlerOrchestrator.kt | 4 +++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt index 169c9ae8ef..819498986c 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt @@ -203,19 +203,25 @@ open class GestureHandler { } fun startTrackingPointer(pointerId: Int) { - if (trackedPointerIDs[pointerId] == -1) { - trackedPointerIDs[pointerId] = findNextLocalPointerId() - trackedPointersIDsCount++ + if (isTrackingPointer(pointerId)) { + return } + + trackedPointerIDs[pointerId] = findNextLocalPointerId() + trackedPointersIDsCount++ } fun stopTrackingPointer(pointerId: Int) { - if (trackedPointerIDs[pointerId] != -1) { - trackedPointerIDs[pointerId] = -1 - trackedPointersIDsCount-- + if (!isTrackingPointer(pointerId)) { + return } + + trackedPointerIDs[pointerId] = -1 + trackedPointersIDsCount-- } + private fun isTrackingPointer(pointerId: Int) = trackedPointerIDs[pointerId] != -1 + private fun needAdapt(event: MotionEvent): Boolean { if (event.pointerCount != trackedPointersIDsCount) { return true @@ -573,11 +579,11 @@ open class GestureHandler { onStateChange(newState, oldState) } - fun wantEvents(): Boolean = isEnabled && + fun wantsEvent(event: MotionEvent): Boolean = isEnabled && state != STATE_FAILED && state != STATE_CANCELLED && state != STATE_END && - trackedPointersIDsCount > 0 + isTrackingPointer(event.getPointerId(event.actionIndex)) open fun shouldRequireToWaitForFailure(handler: GestureHandler): Boolean { if (handler === this) { diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt index d6e4cdc225..e76f9af9fa 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt @@ -249,6 +249,7 @@ class GestureHandlerOrchestrator( // on Arrays.sort providing a stable sort (as children are registered in order in which they // should be tested) preparedHandlers.sortWith(handlersComparator) + for (handler in preparedHandlers) { deliverEventToGestureHandler(handler, event) } @@ -273,7 +274,8 @@ class GestureHandlerOrchestrator( handler.cancel() return } - if (!handler.wantEvents()) { + + if (!handler.wantsEvent(sourceEvent)) { return } From e940406f75060a9e2bdefd8e17db43162a928aa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacy=20=C5=81=C4=85tka?= Date: Fri, 4 Jul 2025 12:22:18 +0200 Subject: [PATCH 007/236] Cleanup `ReanimatedSwipeable` (#3579) ## Changes - Replace `forwardRef` with prop `ref` - Fix possible infinite re-render bug - Fix invalid dependency lists - Split `ReanimatedSwipeable` into 3 smaller files: - `index.ts` - `ReanimatedSwipeable.tsx` - `ReanimatedSwipeableProps.tsx` No changes to the core logic have been made. ## Test plan Use available Swipeable examples to test if it works. --- .../src/components/ReanimatedSwipeable.tsx | 811 ------------------ .../ReanimatedSwipeable.tsx | 603 +++++++++++++ .../ReanimatedSwipeableProps.ts | 199 +++++ .../components/ReanimatedSwipeable/index.ts | 6 + 4 files changed, 808 insertions(+), 811 deletions(-) delete mode 100644 packages/react-native-gesture-handler/src/components/ReanimatedSwipeable.tsx create mode 100644 packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx create mode 100644 packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts create mode 100644 packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/index.ts diff --git a/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable.tsx b/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable.tsx deleted file mode 100644 index aa6a655537..0000000000 --- a/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable.tsx +++ /dev/null @@ -1,811 +0,0 @@ -// Similarily to the DrawerLayout component this deserves to be put in a -// separate repo. Although, keeping it here for the time being will allow us to -// move faster and fix possible issues quicker - -import React, { - ForwardedRef, - forwardRef, - useCallback, - useImperativeHandle, - useMemo, -} from 'react'; -import { GestureObjects as Gesture } from '../handlers/gestures/gestureObjects'; -import { GestureDetector } from '../handlers/gestures/GestureDetector'; -import { - GestureStateChangeEvent, - GestureUpdateEvent, -} from '../handlers/gestureHandlerCommon'; -import type { PanGestureHandlerProps } from '../handlers/PanGestureHandler'; -import type { PanGestureHandlerEventPayload } from '../handlers/GestureHandlerEventPayload'; -import Animated, { - ReduceMotion, - SharedValue, - interpolate, - measure, - runOnJS, - runOnUI, - useAnimatedRef, - useAnimatedStyle, - useSharedValue, - withSpring, -} from 'react-native-reanimated'; -import { - I18nManager, - LayoutChangeEvent, - StyleProp, - StyleSheet, - View, - ViewStyle, -} from 'react-native'; -import { applyRelationProp, RelationPropName, RelationPropType } from './utils'; - -const DRAG_TOSS = 0.05; - -type SwipeableExcludes = Exclude< - keyof PanGestureHandlerProps, - 'onGestureEvent' | 'onHandlerStateChange' ->; - -enum SwipeDirection { - LEFT = 'left', - RIGHT = 'right', -} - -export interface SwipeableProps - extends Pick { - /** - * Enables two-finger gestures on supported devices, for example iPads with - * trackpads. If not enabled the gesture will require click + drag, with - * `enableTrackpadTwoFingerGesture` swiping with two fingers will also trigger - * the gesture. - */ - enableTrackpadTwoFingerGesture?: boolean; - - /** - * Specifies how much the visual interaction will be delayed compared to the - * gesture distance. e.g. value of 1 will indicate that the swipeable panel - * should exactly follow the gesture, 2 means it is going to be two times - * "slower". - */ - friction?: number; - - /** - * Distance from the left edge at which released panel will animate to the - * open state (or the open panel will animate into the closed state). By - * default it's a half of the panel's width. - */ - leftThreshold?: number; - - /** - * Distance from the right edge at which released panel will animate to the - * open state (or the open panel will animate into the closed state). By - * default it's a half of the panel's width. - */ - rightThreshold?: number; - - /** - * Distance that the panel must be dragged from the left edge to be considered - * a swipe. The default value is 10. - */ - dragOffsetFromLeftEdge?: number; - - /** - * Distance that the panel must be dragged from the right edge to be considered - * a swipe. The default value is 10. - */ - dragOffsetFromRightEdge?: number; - - /** - * Value indicating if the swipeable panel can be pulled further than the left - * actions panel's width. It is set to true by default as long as the left - * panel render method is present. - */ - overshootLeft?: boolean; - - /** - * Value indicating if the swipeable panel can be pulled further than the - * right actions panel's width. It is set to true by default as long as the - * right panel render method is present. - */ - overshootRight?: boolean; - - /** - * Specifies how much the visual interaction will be delayed compared to the - * gesture distance at overshoot. Default value is 1, it mean no friction, for - * a native feel, try 8 or above. - */ - overshootFriction?: number; - - /** - * Called when action panel gets open (either right or left). - */ - onSwipeableOpen?: ( - direction: SwipeDirection.LEFT | SwipeDirection.RIGHT - ) => void; - - /** - * Called when action panel is closed. - */ - onSwipeableClose?: ( - direction: SwipeDirection.LEFT | SwipeDirection.RIGHT - ) => void; - - /** - * Called when action panel starts animating on open (either right or left). - */ - onSwipeableWillOpen?: ( - direction: SwipeDirection.LEFT | SwipeDirection.RIGHT - ) => void; - - /** - * Called when action panel starts animating on close. - */ - onSwipeableWillClose?: ( - direction: SwipeDirection.LEFT | SwipeDirection.RIGHT - ) => void; - - /** - * Called when action panel starts being shown on dragging to open. - */ - onSwipeableOpenStartDrag?: ( - direction: SwipeDirection.LEFT | SwipeDirection.RIGHT - ) => void; - - /** - * Called when action panel starts being shown on dragging to close. - */ - onSwipeableCloseStartDrag?: ( - direction: SwipeDirection.LEFT | SwipeDirection.RIGHT - ) => void; - - /** - * `progress`: Equals `0` when `swipeable` is closed, `1` when `swipeable` is opened. - * - When the element overshoots it's opened position the value tends towards `Infinity`. - * - Goes back to `1` when `swipeable` is released. - * - * `translation`: a horizontal offset of the `swipeable` relative to its closed position.\ - * `swipeableMethods`: provides an object exposing methods for controlling the `swipeable`. - * - * To support `rtl` flexbox layouts use `flexDirection` styling. - * */ - renderLeftActions?: ( - progress: SharedValue, - translation: SharedValue, - swipeableMethods: SwipeableMethods - ) => React.ReactNode; - - /** - * `progress`: Equals `0` when `swipeable` is closed, `1` when `swipeable` is opened. - * - When the element overshoots it's opened position the value tends towards `Infinity`. - * - Goes back to `1` when `swipeable` is released. - * - * `translation`: a horizontal offset of the `swipeable` relative to its closed position.\ - * `swipeableMethods`: provides an object exposing methods for controlling the `swipeable`. - * - * To support `rtl` flexbox layouts use `flexDirection` styling. - * */ - renderRightActions?: ( - progress: SharedValue, - translation: SharedValue, - swipeableMethods: SwipeableMethods - ) => React.ReactNode; - - animationOptions?: Record; - - /** - * Style object for the container (`Animated.View`), for example to override - * `overflow: 'hidden'`. - */ - containerStyle?: StyleProp; - - /** - * Style object for the children container (`Animated.View`), for example to - * apply `flex: 1` - */ - childrenContainerStyle?: StyleProp; - - /** - * A gesture object or an array of gesture objects containing the configuration and callbacks to be - * used with the swipeable's gesture handler. - */ - simultaneousWithExternalGesture?: RelationPropType; - - /** - * A gesture object or an array of gesture objects containing the configuration and callbacks to be - * used with the swipeable's gesture handler. - */ - requireExternalGestureToFail?: RelationPropType; - - /** - * A gesture object or an array of gesture objects containing the configuration and callbacks to be - * used with the swipeable's gesture handler. - */ - blocksExternalGesture?: RelationPropType; -} - -export interface SwipeableMethods { - close: () => void; - openLeft: () => void; - openRight: () => void; - reset: () => void; -} - -const Swipeable = forwardRef( - function Swipeable( - props: SwipeableProps, - ref: ForwardedRef - ) { - const defaultProps = { - friction: 1, - overshootFriction: 1, - dragOffset: 10, - enableTrackpadTwoFingerGesture: false, - }; - - const { - leftThreshold, - rightThreshold, - enabled, - containerStyle, - childrenContainerStyle, - animationOptions, - overshootLeft, - overshootRight, - testID, - children, - enableTrackpadTwoFingerGesture = defaultProps.enableTrackpadTwoFingerGesture, - dragOffsetFromLeftEdge = defaultProps.dragOffset, - dragOffsetFromRightEdge = defaultProps.dragOffset, - friction = defaultProps.friction, - overshootFriction = defaultProps.overshootFriction, - onSwipeableOpenStartDrag, - onSwipeableCloseStartDrag, - onSwipeableWillOpen, - onSwipeableWillClose, - onSwipeableOpen, - onSwipeableClose, - renderLeftActions, - renderRightActions, - simultaneousWithExternalGesture, - requireExternalGestureToFail, - blocksExternalGesture, - hitSlop, - ...remainingProps - } = props; - - const relationProps = { - simultaneousWithExternalGesture, - requireExternalGestureToFail, - blocksExternalGesture, - }; - - const rowState = useSharedValue(0); - - const userDrag = useSharedValue(0); - - const appliedTranslation = useSharedValue(0); - - const rowWidth = useSharedValue(0); - const leftWidth = useSharedValue(0); - const rightWidth = useSharedValue(0); - - const showLeftProgress = useSharedValue(0); - const showRightProgress = useSharedValue(0); - - const updateAnimatedEvent = useCallback(() => { - 'worklet'; - - const shouldOvershootLeft = overshootLeft ?? leftWidth.value > 0; - const shouldOvershootRight = overshootRight ?? rightWidth.value > 0; - - const startOffset = - rowState.value === 1 - ? leftWidth.value - : rowState.value === -1 - ? -rightWidth.value - : 0; - - const offsetDrag = userDrag.value / friction + startOffset; - - appliedTranslation.value = interpolate( - offsetDrag, - [ - -rightWidth.value - 1, - -rightWidth.value, - leftWidth.value, - leftWidth.value + 1, - ], - [ - -rightWidth.value - - (shouldOvershootRight ? 1 / overshootFriction : 0), - -rightWidth.value, - leftWidth.value, - leftWidth.value + (shouldOvershootLeft ? 1 / overshootFriction : 0), - ] - ); - - showLeftProgress.value = - leftWidth.value > 0 - ? interpolate( - appliedTranslation.value, - [-1, 0, leftWidth.value], - [0, 0, 1] - ) - : 0; - - showRightProgress.value = - rightWidth.value > 0 - ? interpolate( - appliedTranslation.value, - [-rightWidth.value, 0, 1], - [1, 0, 0] - ) - : 0; - }, [ - appliedTranslation, - friction, - leftWidth, - overshootFriction, - rightWidth, - rowState, - showLeftProgress, - showRightProgress, - userDrag, - overshootLeft, - overshootRight, - ]); - - const dispatchImmediateEvents = useCallback( - (fromValue: number, toValue: number) => { - 'worklet'; - - if (onSwipeableWillOpen && toValue !== 0) { - runOnJS(onSwipeableWillOpen)( - toValue > 0 ? SwipeDirection.RIGHT : SwipeDirection.LEFT - ); - } - - if (onSwipeableWillClose && toValue === 0) { - runOnJS(onSwipeableWillClose)( - fromValue > 0 ? SwipeDirection.LEFT : SwipeDirection.RIGHT - ); - } - }, - [onSwipeableWillClose, onSwipeableWillOpen, rowState] - ); - - const dispatchEndEvents = useCallback( - (fromValue: number, toValue: number) => { - 'worklet'; - - if (onSwipeableOpen && toValue !== 0) { - runOnJS(onSwipeableOpen)( - toValue > 0 ? SwipeDirection.RIGHT : SwipeDirection.LEFT - ); - } - - if (onSwipeableClose && toValue === 0) { - runOnJS(onSwipeableClose)( - fromValue > 0 ? SwipeDirection.LEFT : SwipeDirection.RIGHT - ); - } - }, - [onSwipeableClose, onSwipeableOpen] - ); - - const animateRow: (toValue: number, velocityX?: number) => void = - useCallback( - (toValue: number, velocityX?: number) => { - 'worklet'; - - const translationSpringConfig = { - mass: 2, - damping: 1000, - stiffness: 700, - velocity: velocityX, - overshootClamping: true, - reduceMotion: ReduceMotion.System, - ...animationOptions, - }; - - const isClosing = toValue === 0; - const moveToRight = isClosing ? rowState.value < 0 : toValue > 0; - - const usedWidth = isClosing - ? moveToRight - ? rightWidth.value - : leftWidth.value - : moveToRight - ? leftWidth.value - : rightWidth.value; - - const progressSpringConfig = { - ...translationSpringConfig, - restDisplacementThreshold: 0.01, - restSpeedThreshold: 0.01, - velocity: - velocityX && - interpolate(velocityX, [-usedWidth, usedWidth], [-1, 1]), - }; - - const frozenRowState = rowState.value; - - appliedTranslation.value = withSpring( - toValue, - translationSpringConfig, - (isFinished) => { - if (isFinished) { - dispatchEndEvents(frozenRowState, toValue); - } - } - ); - - const progressTarget = toValue === 0 ? 0 : 1 * Math.sign(toValue); - - showLeftProgress.value = withSpring( - Math.max(progressTarget, 0), - progressSpringConfig - ); - - showRightProgress.value = withSpring( - Math.max(-progressTarget, 0), - progressSpringConfig - ); - - dispatchImmediateEvents(frozenRowState, toValue); - - rowState.value = Math.sign(toValue); - }, - [ - rowState, - animationOptions, - appliedTranslation, - showLeftProgress, - leftWidth, - showRightProgress, - rightWidth, - dispatchImmediateEvents, - dispatchEndEvents, - ] - ); - - const leftLayoutRef = useAnimatedRef(); - const leftWrapperLayoutRef = useAnimatedRef(); - const rightLayoutRef = useAnimatedRef(); - - const updateElementWidths = useCallback(() => { - 'worklet'; - const leftLayout = measure(leftLayoutRef); - const leftWrapperLayout = measure(leftWrapperLayoutRef); - const rightLayout = measure(rightLayoutRef); - leftWidth.value = - (leftLayout?.pageX ?? 0) - (leftWrapperLayout?.pageX ?? 0); - - rightWidth.value = - rowWidth.value - - (rightLayout?.pageX ?? rowWidth.value) + - (leftWrapperLayout?.pageX ?? 0); - }, [ - leftLayoutRef, - leftWrapperLayoutRef, - rightLayoutRef, - leftWidth, - rightWidth, - rowWidth, - ]); - - const swipeableMethods = useMemo( - () => ({ - close() { - 'worklet'; - if (_WORKLET) { - animateRow(0); - return; - } - runOnUI(() => { - animateRow(0); - })(); - }, - openLeft() { - 'worklet'; - if (_WORKLET) { - updateElementWidths(); - animateRow(leftWidth.value); - return; - } - runOnUI(() => { - updateElementWidths(); - animateRow(leftWidth.value); - })(); - }, - openRight() { - 'worklet'; - if (_WORKLET) { - updateElementWidths(); - animateRow(-rightWidth.value); - return; - } - runOnUI(() => { - updateElementWidths(); - animateRow(-rightWidth.value); - })(); - }, - reset() { - 'worklet'; - userDrag.value = 0; - showLeftProgress.value = 0; - appliedTranslation.value = 0; - rowState.value = 0; - }, - }), - [ - animateRow, - updateElementWidths, - leftWidth, - rightWidth, - userDrag, - showLeftProgress, - appliedTranslation, - rowState, - ] - ); - - const onRowLayout = useCallback( - ({ nativeEvent }: LayoutChangeEvent) => { - rowWidth.value = nativeEvent.layout.width; - }, - [rowWidth] - ); - - // As stated in `Dimensions.get` docstring, this function should be called on every render - // since dimensions may change (e.g. orientation change) - - const leftActionAnimation = useAnimatedStyle(() => { - return { - opacity: showLeftProgress.value === 0 ? 0 : 1, - }; - }); - - const leftElement = useCallback( - () => ( - - {renderLeftActions?.( - showLeftProgress, - appliedTranslation, - swipeableMethods - )} - - - ), - [ - appliedTranslation, - leftActionAnimation, - leftLayoutRef, - leftWrapperLayoutRef, - renderLeftActions, - showLeftProgress, - swipeableMethods, - ] - ); - - const rightActionAnimation = useAnimatedStyle(() => { - return { - opacity: showRightProgress.value === 0 ? 0 : 1, - }; - }); - - const rightElement = useCallback( - () => ( - - {renderRightActions?.( - showRightProgress, - appliedTranslation, - swipeableMethods - )} - - - ), - [ - appliedTranslation, - renderRightActions, - rightActionAnimation, - rightLayoutRef, - showRightProgress, - swipeableMethods, - ] - ); - - const handleRelease = useCallback( - (event: GestureStateChangeEvent) => { - 'worklet'; - const { velocityX } = event; - userDrag.value = event.translationX; - - const leftThresholdProp = leftThreshold ?? leftWidth.value / 2; - const rightThresholdProp = rightThreshold ?? rightWidth.value / 2; - - const translationX = - (userDrag.value + DRAG_TOSS * velocityX) / friction; - - let toValue = 0; - - if (rowState.value === 0) { - if (translationX > leftThresholdProp) { - toValue = leftWidth.value; - } else if (translationX < -rightThresholdProp) { - toValue = -rightWidth.value; - } - } else if (rowState.value === 1) { - // Swiped to left - if (translationX > -leftThresholdProp) { - toValue = leftWidth.value; - } - } else { - // Swiped to right - if (translationX < rightThresholdProp) { - toValue = -rightWidth.value; - } - } - - animateRow(toValue, velocityX / friction); - }, - [ - animateRow, - friction, - leftThreshold, - leftWidth, - rightThreshold, - rightWidth, - rowState, - userDrag, - ] - ); - - const close = useCallback(() => { - 'worklet'; - animateRow(0); - }, [animateRow]); - - const dragStarted = useSharedValue(false); - - const tapGesture = useMemo(() => { - const tap = Gesture.Tap() - .shouldCancelWhenOutside(true) - .onStart(() => { - if (rowState.value !== 0) { - close(); - } - }); - - Object.entries(relationProps).forEach(([relationName, relation]) => { - applyRelationProp( - tap, - relationName as RelationPropName, - relation as RelationPropType - ); - }); - - return tap; - }, [close, rowState, simultaneousWithExternalGesture]); - - const panGesture = useMemo(() => { - const pan = Gesture.Pan() - .enabled(enabled !== false) - .enableTrackpadTwoFingerGesture(enableTrackpadTwoFingerGesture) - .activeOffsetX([-dragOffsetFromRightEdge, dragOffsetFromLeftEdge]) - .onStart(updateElementWidths) - .onUpdate( - (event: GestureUpdateEvent) => { - userDrag.value = event.translationX; - - const direction = - rowState.value === -1 - ? SwipeDirection.RIGHT - : rowState.value === 1 - ? SwipeDirection.LEFT - : event.translationX > 0 - ? SwipeDirection.RIGHT - : SwipeDirection.LEFT; - - if (!dragStarted.value) { - dragStarted.value = true; - if (rowState.value === 0 && onSwipeableOpenStartDrag) { - runOnJS(onSwipeableOpenStartDrag)(direction); - } else if (onSwipeableCloseStartDrag) { - runOnJS(onSwipeableCloseStartDrag)(direction); - } - } - - updateAnimatedEvent(); - } - ) - .onEnd( - (event: GestureStateChangeEvent) => { - handleRelease(event); - } - ) - .onFinalize(() => { - dragStarted.value = false; - }); - - Object.entries(relationProps).forEach(([relationName, relation]) => { - applyRelationProp( - pan, - relationName as RelationPropName, - relation as RelationPropType - ); - }); - - return pan; - }, [ - dragOffsetFromLeftEdge, - dragOffsetFromRightEdge, - dragStarted, - enableTrackpadTwoFingerGesture, - enabled, - handleRelease, - onSwipeableCloseStartDrag, - onSwipeableOpenStartDrag, - rowState, - updateAnimatedEvent, - updateElementWidths, - userDrag, - simultaneousWithExternalGesture, - ]); - - useImperativeHandle(ref, () => swipeableMethods, [swipeableMethods]); - - const animatedStyle = useAnimatedStyle( - () => ({ - transform: [{ translateX: appliedTranslation.value }], - pointerEvents: rowState.value === 0 ? 'auto' : 'box-only', - }), - [appliedTranslation, rowState] - ); - - const swipeableComponent = ( - - - {leftElement()} - {rightElement()} - - - {children} - - - - - ); - - return testID ? ( - {swipeableComponent} - ) : ( - swipeableComponent - ); - } -); - -export default Swipeable; -export type SwipeableRef = ForwardedRef; - -const styles = StyleSheet.create({ - container: { - overflow: 'hidden', - }, - leftActions: { - ...StyleSheet.absoluteFillObject, - flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row', - overflow: 'hidden', - }, - rightActions: { - ...StyleSheet.absoluteFillObject, - flexDirection: I18nManager.isRTL ? 'row' : 'row-reverse', - overflow: 'hidden', - }, -}); diff --git a/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx b/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx new file mode 100644 index 0000000000..53cce68ece --- /dev/null +++ b/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx @@ -0,0 +1,603 @@ +import { useMemo, useCallback, useImperativeHandle, ForwardedRef } from 'react'; +import { LayoutChangeEvent, View, I18nManager, StyleSheet } from 'react-native'; +import Animated, { + useSharedValue, + interpolate, + runOnJS, + ReduceMotion, + withSpring, + useAnimatedRef, + measure, + runOnUI, + useAnimatedStyle, +} from 'react-native-reanimated'; +import { SwipeableProps, SwipeableMethods, SwipeDirection } from '.'; +import { Gesture } from '../..'; +import { + GestureStateChangeEvent, + GestureUpdateEvent, +} from '../../handlers/gestureHandlerCommon'; +import { PanGestureHandlerEventPayload } from '../../handlers/GestureHandlerEventPayload'; +import { GestureDetector } from '../../handlers/gestures/GestureDetector'; +import { + applyRelationProp, + RelationPropName, + RelationPropType, +} from '../utils'; + +const DRAG_TOSS = 0.05; + +const DEFAULT_FRICTION = 1; +const DEFAULT_OVERSHOOT_FRICTION = 1; +const DEFAULT_DRAG_OFFSET = 10; +const DEFAULT_ENABLE_TRACKING_TWO_FINGER_GESTURE = false; + +const Swipeable = (props: SwipeableProps) => { + const { + ref, + leftThreshold, + rightThreshold, + enabled, + containerStyle, + childrenContainerStyle, + animationOptions, + overshootLeft, + overshootRight, + testID, + children, + enableTrackpadTwoFingerGesture = DEFAULT_ENABLE_TRACKING_TWO_FINGER_GESTURE, + dragOffsetFromLeftEdge = DEFAULT_DRAG_OFFSET, + dragOffsetFromRightEdge = DEFAULT_DRAG_OFFSET, + friction = DEFAULT_FRICTION, + overshootFriction = DEFAULT_OVERSHOOT_FRICTION, + onSwipeableOpenStartDrag, + onSwipeableCloseStartDrag, + onSwipeableWillOpen, + onSwipeableWillClose, + onSwipeableOpen, + onSwipeableClose, + renderLeftActions, + renderRightActions, + simultaneousWithExternalGesture, + requireExternalGestureToFail, + blocksExternalGesture, + hitSlop, + ...remainingProps + } = props; + + const relationProps = useMemo( + () => ({ + simultaneousWithExternalGesture, + requireExternalGestureToFail, + blocksExternalGesture, + }), + [ + blocksExternalGesture, + requireExternalGestureToFail, + simultaneousWithExternalGesture, + ] + ); + + const rowState = useSharedValue(0); + + const userDrag = useSharedValue(0); + + const appliedTranslation = useSharedValue(0); + + const rowWidth = useSharedValue(0); + const leftWidth = useSharedValue(0); + const rightWidth = useSharedValue(0); + + const showLeftProgress = useSharedValue(0); + const showRightProgress = useSharedValue(0); + + const updateAnimatedEvent = useCallback(() => { + 'worklet'; + + const shouldOvershootLeft = overshootLeft ?? leftWidth.value > 0; + const shouldOvershootRight = overshootRight ?? rightWidth.value > 0; + + const startOffset = + rowState.value === 1 + ? leftWidth.value + : rowState.value === -1 + ? -rightWidth.value + : 0; + + const offsetDrag = userDrag.value / friction + startOffset; + + appliedTranslation.value = interpolate( + offsetDrag, + [ + -rightWidth.value - 1, + -rightWidth.value, + leftWidth.value, + leftWidth.value + 1, + ], + [ + -rightWidth.value - (shouldOvershootRight ? 1 / overshootFriction : 0), + -rightWidth.value, + leftWidth.value, + leftWidth.value + (shouldOvershootLeft ? 1 / overshootFriction : 0), + ] + ); + + showLeftProgress.value = + leftWidth.value > 0 + ? interpolate( + appliedTranslation.value, + [-1, 0, leftWidth.value], + [0, 0, 1] + ) + : 0; + + showRightProgress.value = + rightWidth.value > 0 + ? interpolate( + appliedTranslation.value, + [-rightWidth.value, 0, 1], + [1, 0, 0] + ) + : 0; + }, [ + appliedTranslation, + friction, + leftWidth, + overshootFriction, + rightWidth, + rowState, + showLeftProgress, + showRightProgress, + userDrag, + overshootLeft, + overshootRight, + ]); + + const dispatchImmediateEvents = useCallback( + (fromValue: number, toValue: number) => { + 'worklet'; + + if (onSwipeableWillOpen && toValue !== 0) { + runOnJS(onSwipeableWillOpen)( + toValue > 0 ? SwipeDirection.RIGHT : SwipeDirection.LEFT + ); + } + + if (onSwipeableWillClose && toValue === 0) { + runOnJS(onSwipeableWillClose)( + fromValue > 0 ? SwipeDirection.LEFT : SwipeDirection.RIGHT + ); + } + }, + [onSwipeableWillClose, onSwipeableWillOpen] + ); + + const dispatchEndEvents = useCallback( + (fromValue: number, toValue: number) => { + 'worklet'; + + if (onSwipeableOpen && toValue !== 0) { + runOnJS(onSwipeableOpen)( + toValue > 0 ? SwipeDirection.RIGHT : SwipeDirection.LEFT + ); + } + + if (onSwipeableClose && toValue === 0) { + runOnJS(onSwipeableClose)( + fromValue > 0 ? SwipeDirection.LEFT : SwipeDirection.RIGHT + ); + } + }, + [onSwipeableClose, onSwipeableOpen] + ); + + const animateRow: (toValue: number, velocityX?: number) => void = useCallback( + (toValue: number, velocityX?: number) => { + 'worklet'; + + const translationSpringConfig = { + mass: 2, + damping: 1000, + stiffness: 700, + velocity: velocityX, + overshootClamping: true, + reduceMotion: ReduceMotion.System, + ...animationOptions, + }; + + const isClosing = toValue === 0; + const moveToRight = isClosing ? rowState.value < 0 : toValue > 0; + + const usedWidth = isClosing + ? moveToRight + ? rightWidth.value + : leftWidth.value + : moveToRight + ? leftWidth.value + : rightWidth.value; + + const progressSpringConfig = { + ...translationSpringConfig, + restDisplacementThreshold: 0.01, + restSpeedThreshold: 0.01, + velocity: + velocityX && interpolate(velocityX, [-usedWidth, usedWidth], [-1, 1]), + }; + + const frozenRowState = rowState.value; + + appliedTranslation.value = withSpring( + toValue, + translationSpringConfig, + (isFinished) => { + if (isFinished) { + dispatchEndEvents(frozenRowState, toValue); + } + } + ); + + const progressTarget = toValue === 0 ? 0 : 1 * Math.sign(toValue); + + showLeftProgress.value = withSpring( + Math.max(progressTarget, 0), + progressSpringConfig + ); + + showRightProgress.value = withSpring( + Math.max(-progressTarget, 0), + progressSpringConfig + ); + + dispatchImmediateEvents(frozenRowState, toValue); + + rowState.value = Math.sign(toValue); + }, + [ + rowState, + animationOptions, + appliedTranslation, + showLeftProgress, + leftWidth, + showRightProgress, + rightWidth, + dispatchImmediateEvents, + dispatchEndEvents, + ] + ); + + const leftLayoutRef = useAnimatedRef(); + const leftWrapperLayoutRef = useAnimatedRef(); + const rightLayoutRef = useAnimatedRef(); + + const updateElementWidths = useCallback(() => { + 'worklet'; + const leftLayout = measure(leftLayoutRef); + const leftWrapperLayout = measure(leftWrapperLayoutRef); + const rightLayout = measure(rightLayoutRef); + leftWidth.value = + (leftLayout?.pageX ?? 0) - (leftWrapperLayout?.pageX ?? 0); + + rightWidth.value = + rowWidth.value - + (rightLayout?.pageX ?? rowWidth.value) + + (leftWrapperLayout?.pageX ?? 0); + }, [ + leftLayoutRef, + leftWrapperLayoutRef, + rightLayoutRef, + leftWidth, + rightWidth, + rowWidth, + ]); + + const swipeableMethods = useMemo( + () => ({ + close() { + 'worklet'; + if (_WORKLET) { + animateRow(0); + return; + } + runOnUI(() => { + animateRow(0); + })(); + }, + openLeft() { + 'worklet'; + if (_WORKLET) { + updateElementWidths(); + animateRow(leftWidth.value); + return; + } + runOnUI(() => { + updateElementWidths(); + animateRow(leftWidth.value); + })(); + }, + openRight() { + 'worklet'; + if (_WORKLET) { + updateElementWidths(); + animateRow(-rightWidth.value); + return; + } + runOnUI(() => { + updateElementWidths(); + animateRow(-rightWidth.value); + })(); + }, + reset() { + 'worklet'; + userDrag.value = 0; + showLeftProgress.value = 0; + appliedTranslation.value = 0; + rowState.value = 0; + }, + }), + [ + animateRow, + updateElementWidths, + leftWidth, + rightWidth, + userDrag, + showLeftProgress, + appliedTranslation, + rowState, + ] + ); + + const onRowLayout = useCallback( + ({ nativeEvent }: LayoutChangeEvent) => { + rowWidth.value = nativeEvent.layout.width; + }, + [rowWidth] + ); + + // As stated in `Dimensions.get` docstring, this function should be called on every render + // since dimensions may change (e.g. orientation change) + + const leftActionAnimation = useAnimatedStyle(() => { + return { + opacity: showLeftProgress.value === 0 ? 0 : 1, + }; + }); + + const leftElement = useCallback( + () => ( + + {renderLeftActions?.( + showLeftProgress, + appliedTranslation, + swipeableMethods + )} + + + ), + [ + appliedTranslation, + leftActionAnimation, + leftLayoutRef, + leftWrapperLayoutRef, + renderLeftActions, + showLeftProgress, + swipeableMethods, + ] + ); + + const rightActionAnimation = useAnimatedStyle(() => { + return { + opacity: showRightProgress.value === 0 ? 0 : 1, + }; + }); + + const rightElement = useCallback( + () => ( + + {renderRightActions?.( + showRightProgress, + appliedTranslation, + swipeableMethods + )} + + + ), + [ + appliedTranslation, + renderRightActions, + rightActionAnimation, + rightLayoutRef, + showRightProgress, + swipeableMethods, + ] + ); + + const handleRelease = useCallback( + (event: GestureStateChangeEvent) => { + 'worklet'; + const { velocityX } = event; + userDrag.value = event.translationX; + + const leftThresholdProp = leftThreshold ?? leftWidth.value / 2; + const rightThresholdProp = rightThreshold ?? rightWidth.value / 2; + + const translationX = (userDrag.value + DRAG_TOSS * velocityX) / friction; + + let toValue = 0; + + if (rowState.value === 0) { + if (translationX > leftThresholdProp) { + toValue = leftWidth.value; + } else if (translationX < -rightThresholdProp) { + toValue = -rightWidth.value; + } + } else if (rowState.value === 1) { + // Swiped to left + if (translationX > -leftThresholdProp) { + toValue = leftWidth.value; + } + } else { + // Swiped to right + if (translationX < rightThresholdProp) { + toValue = -rightWidth.value; + } + } + + animateRow(toValue, velocityX / friction); + }, + [ + animateRow, + friction, + leftThreshold, + leftWidth, + rightThreshold, + rightWidth, + rowState, + userDrag, + ] + ); + + const close = useCallback(() => { + 'worklet'; + animateRow(0); + }, [animateRow]); + + const dragStarted = useSharedValue(false); + + const tapGesture = useMemo(() => { + const tap = Gesture.Tap() + .shouldCancelWhenOutside(true) + .onStart(() => { + if (rowState.value !== 0) { + close(); + } + }); + + Object.entries(relationProps).forEach(([relationName, relation]) => { + applyRelationProp( + tap, + relationName as RelationPropName, + relation as RelationPropType + ); + }); + + return tap; + }, [close, relationProps, rowState]); + + const panGesture = useMemo(() => { + const pan = Gesture.Pan() + .enabled(enabled !== false) + .enableTrackpadTwoFingerGesture(enableTrackpadTwoFingerGesture) + .activeOffsetX([-dragOffsetFromRightEdge, dragOffsetFromLeftEdge]) + .onStart(updateElementWidths) + .onUpdate((event: GestureUpdateEvent) => { + userDrag.value = event.translationX; + + const direction = + rowState.value === -1 + ? SwipeDirection.RIGHT + : rowState.value === 1 + ? SwipeDirection.LEFT + : event.translationX > 0 + ? SwipeDirection.RIGHT + : SwipeDirection.LEFT; + + if (!dragStarted.value) { + dragStarted.value = true; + if (rowState.value === 0 && onSwipeableOpenStartDrag) { + runOnJS(onSwipeableOpenStartDrag)(direction); + } else if (onSwipeableCloseStartDrag) { + runOnJS(onSwipeableCloseStartDrag)(direction); + } + } + + updateAnimatedEvent(); + }) + .onEnd( + (event: GestureStateChangeEvent) => { + handleRelease(event); + } + ) + .onFinalize(() => { + dragStarted.value = false; + }); + + Object.entries(relationProps).forEach(([relationName, relation]) => { + applyRelationProp( + pan, + relationName as RelationPropName, + relation as RelationPropType + ); + }); + + return pan; + }, [ + enabled, + enableTrackpadTwoFingerGesture, + dragOffsetFromRightEdge, + dragOffsetFromLeftEdge, + updateElementWidths, + relationProps, + userDrag, + rowState, + dragStarted, + updateAnimatedEvent, + onSwipeableOpenStartDrag, + onSwipeableCloseStartDrag, + handleRelease, + ]); + + useImperativeHandle(ref, () => swipeableMethods, [swipeableMethods]); + + const animatedStyle = useAnimatedStyle( + () => ({ + transform: [{ translateX: appliedTranslation.value }], + pointerEvents: rowState.value === 0 ? 'auto' : 'box-only', + }), + [appliedTranslation, rowState] + ); + + const swipeableComponent = ( + + + {leftElement()} + {rightElement()} + + + {children} + + + + + ); + + return testID ? ( + {swipeableComponent} + ) : ( + swipeableComponent + ); +}; + +export default Swipeable; +export type SwipeableRef = ForwardedRef; + +const styles = StyleSheet.create({ + container: { + overflow: 'hidden', + }, + leftActions: { + ...StyleSheet.absoluteFillObject, + flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row', + overflow: 'hidden', + }, + rightActions: { + ...StyleSheet.absoluteFillObject, + flexDirection: I18nManager.isRTL ? 'row' : 'row-reverse', + overflow: 'hidden', + }, +}); diff --git a/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts b/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts new file mode 100644 index 0000000000..3aa66b9a2d --- /dev/null +++ b/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts @@ -0,0 +1,199 @@ +import React from 'react'; +import type { PanGestureHandlerProps } from '../../handlers/PanGestureHandler'; +import { SharedValue } from 'react-native-reanimated'; +import { StyleProp, ViewStyle } from 'react-native'; +import { RelationPropType } from '../utils'; + +type SwipeableExcludes = Exclude< + keyof PanGestureHandlerProps, + 'onGestureEvent' | 'onHandlerStateChange' +>; + +export enum SwipeDirection { + LEFT = 'left', + RIGHT = 'right', +} + +export interface SwipeableProps + extends Pick { + /** + * + */ + ref?: React.RefObject; + + /** + * Enables two-finger gestures on supported devices, for example iPads with + * trackpads. If not enabled the gesture will require click + drag, with + * `enableTrackpadTwoFingerGesture` swiping with two fingers will also trigger + * the gesture. + */ + enableTrackpadTwoFingerGesture?: boolean; + + /** + * Specifies how much the visual interaction will be delayed compared to the + * gesture distance. e.g. value of 1 will indicate that the swipeable panel + * should exactly follow the gesture, 2 means it is going to be two times + * "slower". + */ + friction?: number; + + /** + * Distance from the left edge at which released panel will animate to the + * open state (or the open panel will animate into the closed state). By + * default it's a half of the panel's width. + */ + leftThreshold?: number; + + /** + * Distance from the right edge at which released panel will animate to the + * open state (or the open panel will animate into the closed state). By + * default it's a half of the panel's width. + */ + rightThreshold?: number; + + /** + * Distance that the panel must be dragged from the left edge to be considered + * a swipe. The default value is 10. + */ + dragOffsetFromLeftEdge?: number; + + /** + * Distance that the panel must be dragged from the right edge to be considered + * a swipe. The default value is 10. + */ + dragOffsetFromRightEdge?: number; + + /** + * Value indicating if the swipeable panel can be pulled further than the left + * actions panel's width. It is set to true by default as long as the left + * panel render method is present. + */ + overshootLeft?: boolean; + + /** + * Value indicating if the swipeable panel can be pulled further than the + * right actions panel's width. It is set to true by default as long as the + * right panel render method is present. + */ + overshootRight?: boolean; + + /** + * Specifies how much the visual interaction will be delayed compared to the + * gesture distance at overshoot. Default value is 1, it mean no friction, for + * a native feel, try 8 or above. + */ + overshootFriction?: number; + + /** + * Called when action panel gets open (either right or left). + */ + onSwipeableOpen?: ( + direction: SwipeDirection.LEFT | SwipeDirection.RIGHT + ) => void; + + /** + * Called when action panel is closed. + */ + onSwipeableClose?: ( + direction: SwipeDirection.LEFT | SwipeDirection.RIGHT + ) => void; + + /** + * Called when action panel starts animating on open (either right or left). + */ + onSwipeableWillOpen?: ( + direction: SwipeDirection.LEFT | SwipeDirection.RIGHT + ) => void; + + /** + * Called when action panel starts animating on close. + */ + onSwipeableWillClose?: ( + direction: SwipeDirection.LEFT | SwipeDirection.RIGHT + ) => void; + + /** + * Called when action panel starts being shown on dragging to open. + */ + onSwipeableOpenStartDrag?: ( + direction: SwipeDirection.LEFT | SwipeDirection.RIGHT + ) => void; + + /** + * Called when action panel starts being shown on dragging to close. + */ + onSwipeableCloseStartDrag?: ( + direction: SwipeDirection.LEFT | SwipeDirection.RIGHT + ) => void; + + /** + * `progress`: Equals `0` when `swipeable` is closed, `1` when `swipeable` is opened. + * - When the element overshoots it's opened position the value tends towards `Infinity`. + * - Goes back to `1` when `swipeable` is released. + * + * `translation`: a horizontal offset of the `swipeable` relative to its closed position.\ + * `swipeableMethods`: provides an object exposing methods for controlling the `swipeable`. + * + * To support `rtl` flexbox layouts use `flexDirection` styling. + * */ + renderLeftActions?: ( + progress: SharedValue, + translation: SharedValue, + swipeableMethods: SwipeableMethods + ) => React.ReactNode; + + /** + * `progress`: Equals `0` when `swipeable` is closed, `1` when `swipeable` is opened. + * - When the element overshoots it's opened position the value tends towards `Infinity`. + * - Goes back to `1` when `swipeable` is released. + * + * `translation`: a horizontal offset of the `swipeable` relative to its closed position.\ + * `swipeableMethods`: provides an object exposing methods for controlling the `swipeable`. + * + * To support `rtl` flexbox layouts use `flexDirection` styling. + * */ + renderRightActions?: ( + progress: SharedValue, + translation: SharedValue, + swipeableMethods: SwipeableMethods + ) => React.ReactNode; + + animationOptions?: Record; + + /** + * Style object for the container (`Animated.View`), for example to override + * `overflow: 'hidden'`. + */ + containerStyle?: StyleProp; + + /** + * Style object for the children container (`Animated.View`), for example to + * apply `flex: 1` + */ + childrenContainerStyle?: StyleProp; + + /** + * A gesture object or an array of gesture objects containing the configuration and callbacks to be + * used with the swipeable's gesture handler. + */ + simultaneousWithExternalGesture?: RelationPropType; + + /** + * A gesture object or an array of gesture objects containing the configuration and callbacks to be + * used with the swipeable's gesture handler. + */ + requireExternalGestureToFail?: RelationPropType; + + /** + * A gesture object or an array of gesture objects containing the configuration and callbacks to be + * used with the swipeable's gesture handler. + */ + blocksExternalGesture?: RelationPropType; +} + +export interface SwipeableMethods { + close: () => void; + openLeft: () => void; + openRight: () => void; + reset: () => void; +} diff --git a/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/index.ts b/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/index.ts new file mode 100644 index 0000000000..e63e6014e3 --- /dev/null +++ b/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/index.ts @@ -0,0 +1,6 @@ +export { + type SwipeableProps, + type SwipeableMethods, + SwipeDirection, +} from './ReanimatedSwipeableProps'; +export { default } from './ReanimatedSwipeable'; From 7a988f7fe16a245505fa317082e7ce6e248d6459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Kapu=C5=9Bciak?= <39658211+kacperkapusciak@users.noreply.github.com> Date: Fri, 4 Jul 2025 12:27:49 +0200 Subject: [PATCH 008/236] docs: add Radon IDE banner to TOCItems (#3592) This PR adds a Radon IDE banner to the right-hand side part of the documentation. ![image](https://github.com/user-attachments/assets/e7ee0fe9-e6fa-4f23-882d-07d783297aa3) The banner rotates a couple of different labels and CTAs. It's only visible on desktop. Related to https://github.com/software-mansion/react-native-reanimated/pull/7631 and https://github.com/software-mansion/react-native-reanimated/pull/7634 --- .../src/components/RadonBanner/index.tsx | 71 ++++++++++++++++ .../components/RadonBanner/styles.module.css | 83 +++++++++++++++++++ .../src/theme/TOCItems/index.js | 6 +- 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 packages/docs-gesture-handler/src/components/RadonBanner/index.tsx create mode 100644 packages/docs-gesture-handler/src/components/RadonBanner/styles.module.css diff --git a/packages/docs-gesture-handler/src/components/RadonBanner/index.tsx b/packages/docs-gesture-handler/src/components/RadonBanner/index.tsx new file mode 100644 index 0000000000..9b71e8d0e1 --- /dev/null +++ b/packages/docs-gesture-handler/src/components/RadonBanner/index.tsx @@ -0,0 +1,71 @@ +import React, { useRef } from 'react'; +import BrowserOnly from '@docusaurus/BrowserOnly'; +import styles from './styles.module.css'; + +import ArrowRight from '@site/static/img/arrow-right.svg'; + +const items = [ + { + text: 'An all-in-one IDE for Expo & React Native.', + button: 'Try Radon IDE for free', + }, + { + text: 'Streamline your React Native & Expo development.', + button: 'Install Radon IDE', + }, + { + text: 'Set breakpoints and debug React Native apps with ease.', + button: 'Try Radon IDE for free', + }, + { + text: 'Catch runtime errors right in your editor.', + button: 'Install Radon IDE', + }, + { + text: 'Preview React Native components in isolation.', + button: 'Try Radon IDE', + }, + { + text: 'Run, preview, and debug your React Native & Expo apps without leaving VSCode.', + button: 'Try Radon IDE for free', + }, + { + text: 'Run, preview, and debug apps faster.', + button: 'Try our IDE for React Native', + }, + { + text: 'Reduce context switching & debug faster.', + button: 'Try Radon IDE for free', + }, + { + text: 'Reduce context switching & preview components instantly.', + button: 'Try Radon IDE for free', + }, + { + text: 'Reduce context switching & streamline React Native and Expo development.', + button: 'Install Radon IDE', + }, +]; + +function RadonBannerInner(): JSX.Element { + const item = useRef(items[Math.floor(Math.random() * items.length)]); + + return ( + +
+

{item.current.text}

+ + {item.current.button} + +
+ + +
+ ); +} + +export default function RadonBanner() { + return {() => }; +} diff --git a/packages/docs-gesture-handler/src/components/RadonBanner/styles.module.css b/packages/docs-gesture-handler/src/components/RadonBanner/styles.module.css new file mode 100644 index 0000000000..1c5421cbb4 --- /dev/null +++ b/packages/docs-gesture-handler/src/components/RadonBanner/styles.module.css @@ -0,0 +1,83 @@ +.container { + display: flex; + align-items: center; + justify-content: center; + padding: 14px; + background-color: var(--swm-green-light-60); + border-radius: 4px; + position: relative; + overflow: hidden; + text-decoration: none !important; +} + +.container:hover .button svg { + transform: translate(5px); +} + +.container:hover .button { + border: 1px solid var(--swm-green-light-100); +} + +.text { + font-size: 15px; + color: var(--swm-navy-light-100); + font-weight: 500; + z-index: 1; + position: relative; + margin-bottom: 8px; + text-wrap: balance; + text-align: center; +} + +.button { + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: 5px 10px; + color: var(--swm-navy-light-100); + background-color: var(--swm-white); + transition: border 0.3s; + border: 1px solid var(--swm-navy-light-100); + font-size: 14px; + font-weight: 500; + cursor: pointer; + z-index: 1; + gap: 4px; +} + +.button svg { + transition: transform 0.3s; +} + +.ellipseLeft { + position: absolute; + width: 100px; + height: 100px; + top: -20px; + left: -30px; + border-radius: 50%; + transform: rotate(-30deg); + background: linear-gradient( + 184.24deg, + var(--swm-green-light-80) 3.45%, + var(--swm-green-light-40) 66.97%, + rgba(255, 255, 255, 0) 124.45% + ); +} + +.ellipseRight { + position: absolute; + width: 150px; + height: 150px; + bottom: -45px; + right: -70px; + border-radius: 50%; + transform: rotate(-120deg); + background: linear-gradient( + 184.24deg, + var(--swm-green-light-80) 3.45%, + var(--swm-green-light-40) 66.97%, + rgba(255, 255, 255, 0) 124.45% + ); +} diff --git a/packages/docs-gesture-handler/src/theme/TOCItems/index.js b/packages/docs-gesture-handler/src/theme/TOCItems/index.js index 086bd62715..749c6016fa 100644 --- a/packages/docs-gesture-handler/src/theme/TOCItems/index.js +++ b/packages/docs-gesture-handler/src/theme/TOCItems/index.js @@ -1,3 +1,7 @@ import { TOCItems } from '@swmansion/t-rex-ui'; -export default TOCItems; +import RadonBanner from '../../components/RadonBanner'; + +export default function TOCItemsWrapper(props) { + return } {...props} />; +} From 2fac00416e6bd24aaf83215e025e7093b9df8b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= <63123542+m-bert@users.noreply.github.com> Date: Mon, 7 Jul 2025 08:50:24 +0200 Subject: [PATCH 009/236] [Android] Use `asReversed` instead of `reversed` (#3598) ## Description Some of our users experience crashes that `reversed` is not defined: ``` java.lang.NoSuchMethodError: No virtual method reversed()Ljava/util/List; ``` This PR changes `reversed` occurrences to either `asReversed` (if possible), or `asReversed().toList()`, if array can be modified. Closes #3594 ## Test plan Tested on expo-example (mostly _transformations_, _multitap_). --- .../gesturehandler/core/GestureHandlerOrchestrator.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt index e76f9af9fa..a532cf6ec4 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt @@ -207,7 +207,7 @@ class GestureHandlerOrchestrator( } // Clear all awaiting handlers waiting for the current handler to fail - for (otherHandler in awaitingHandlers.reversed()) { + for (otherHandler in awaitingHandlers.asReversed()) { if (shouldHandlerBeCancelledBy(otherHandler, handler)) { otherHandler.isAwaiting = false } @@ -256,15 +256,18 @@ class GestureHandlerOrchestrator( } private fun cancelAll() { - for (handler in awaitingHandlers.reversed()) { + // We need `toList` as `awaitingHandlers` can be modified by `cancel`: + // `onHandlerStateChange` -> `tryActivate` -> `addAwaitingHandler` + for (handler in awaitingHandlers.asReversed().toList()) { handler.cancel() } + // Copy handlers to "prepared handlers" array, because the list of active handlers can change // as a result of state updates preparedHandlers.clear() preparedHandlers.addAll(gestureHandlers) - for (handler in gestureHandlers.reversed()) { + for (handler in gestureHandlers.asReversed()) { handler.cancel() } } @@ -283,7 +286,7 @@ class GestureHandlerOrchestrator( val event = transformEventToViewCoords(handler.view, MotionEvent.obtain(sourceEvent)) // Touch events are sent before the handler itself has a chance to process them, - // mainly because `onTouchesUp` shoul be send befor gesture finishes. This means that + // mainly because `onTouchesUp` should be send before gesture finishes. This means that // the first `onTouchesDown` event is sent before a gesture begins, activation in // callback for this event causes problems because the handler doesn't have a chance // to initialize itself with starting values of pointer (in pan this causes translation From c278c94e338223713e0a3bd9bef10cf427450f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= <63123542+m-bert@users.noreply.github.com> Date: Mon, 7 Jul 2025 10:55:06 +0200 Subject: [PATCH 010/236] Update `ReanimatedSwipeable` module path (#3605) ## Description #3579 introduced changes to `ReanimatedSwipeable`. However, it didn't update path to component in `tsconfig`, thus CI was failing. ## Test plan Check CI (+ `yarn ts-check`) --- apps/basic-example/tsconfig.json | 2 +- apps/common-app/tsconfig.json | 2 +- apps/expo-example/tsconfig.json | 2 +- apps/macos-example/tsconfig.json | 2 +- .../ReanimatedSwipeable/package.json | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/basic-example/tsconfig.json b/apps/basic-example/tsconfig.json index 975df0663d..7c907c3df4 100644 --- a/apps/basic-example/tsconfig.json +++ b/apps/basic-example/tsconfig.json @@ -8,7 +8,7 @@ "../../packages/react-native-gesture-handler/src" ], "react-native-gesture-handler/ReanimatedSwipeable": [ - "../../packages/react-native-gesture-handler/src/components/ReanimatedSwipeable.tsx" + "../../packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/" ], "react-native-gesture-handler/ReanimatedDrawerLayout": [ "../../packages/react-native-gesture-handler/src/components/ReanimatedDrawerLayout.tsx" diff --git a/apps/common-app/tsconfig.json b/apps/common-app/tsconfig.json index 46e02e211b..ad807aa0ad 100644 --- a/apps/common-app/tsconfig.json +++ b/apps/common-app/tsconfig.json @@ -9,7 +9,7 @@ "../../packages/react-native-gesture-handler/src" ], "react-native-gesture-handler/ReanimatedSwipeable": [ - "../../packages/react-native-gesture-handler/src/components/ReanimatedSwipeable.tsx" + "../../packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/" ], "react-native-gesture-handler/ReanimatedDrawerLayout": [ "../../packages/react-native-gesture-handler/src/components/ReanimatedDrawerLayout.tsx" diff --git a/apps/expo-example/tsconfig.json b/apps/expo-example/tsconfig.json index 3ea30d34b8..a2ccc08345 100644 --- a/apps/expo-example/tsconfig.json +++ b/apps/expo-example/tsconfig.json @@ -9,7 +9,7 @@ "../../packages/react-native-gesture-handler/src" ], "react-native-gesture-handler/ReanimatedSwipeable": [ - "../../packages/react-native-gesture-handler/src/components/ReanimatedSwipeable.tsx" + "../../packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/" ], "react-native-gesture-handler/ReanimatedDrawerLayout": [ "../../packages/react-native-gesture-handler/src/components/ReanimatedDrawerLayout.tsx" diff --git a/apps/macos-example/tsconfig.json b/apps/macos-example/tsconfig.json index 9e28dc4bb7..3a7b4ed3b3 100644 --- a/apps/macos-example/tsconfig.json +++ b/apps/macos-example/tsconfig.json @@ -9,7 +9,7 @@ "../../packages/react-native-gesture-handler/src" ], "react-native-gesture-handler/ReanimatedSwipeable": [ - "../../packages/react-native-gesture-handler/src/components/ReanimatedSwipeable.tsx" + "../../packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/" ], "react-native-gesture-handler/ReanimatedDrawerLayout": [ "../../packages/react-native-gesture-handler/src/components/ReanimatedDrawerLayout.tsx" diff --git a/packages/react-native-gesture-handler/ReanimatedSwipeable/package.json b/packages/react-native-gesture-handler/ReanimatedSwipeable/package.json index 9b8283043b..5569615eb2 100644 --- a/packages/react-native-gesture-handler/ReanimatedSwipeable/package.json +++ b/packages/react-native-gesture-handler/ReanimatedSwipeable/package.json @@ -1,6 +1,6 @@ { - "main": "../lib/commonjs/components/ReanimatedSwipeable", - "module": "../lib/module/components/ReanimatedSwipeable", - "react-native": "../src/components/ReanimatedSwipeable", - "types": "../lib/typescript/components/ReanimatedSwipeable.d.ts" + "main": "../lib/commonjs/components/ReanimatedSwipeable/", + "module": "../lib/module/components/ReanimatedSwipeable/", + "react-native": "../src/components/ReanimatedSwipeable/", + "types": "../lib/typescript/components/ReanimatedSwipeable/" } From fa6a818703bef92819fa8a11b5724e13ff783550 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Fri, 11 Jul 2025 12:37:31 +0200 Subject: [PATCH 011/236] Implement base for a native detector component (#3599) ## Description Implements a native `RNGestureHandlerDetector` component based on `display: contents`: - The view will match the layout of its child - The lifecycle of gestures is moved to hooks instead of being managed by the detector - Detector should attach and detach native gesture handlers to itself as it's mounted or unmounted - Implements sending native events to the detector component - It's able to work with RN's Animated events TODO: - Validate that gestures are correctly attached/detached in all cases (suspense, display: none, navigation) - Validate that the gesture lifecycle is correctly managed in StrictMode - Correctly handle `NativeViewGestureHandler`, which should be attached to the child (I think) - Correctly handle the Text component - Integration with Reanimated ## Test plan Updated basic example --- apps/basic-example/Gemfile.lock | 18 +- apps/basic-example/ios/Podfile.lock | 2 +- apps/basic-example/src/App.tsx | 72 +++---- apps/basic-example/src/ComponentsScreen.tsx | 193 ------------------ apps/basic-example/src/FinalScreen.tsx | 56 ----- .../src/GestureCompositionScreen.tsx | 120 ----------- apps/basic-example/src/NativeDetector.tsx | 57 ++++++ apps/basic-example/src/Navigator.tsx | 8 +- .../{HomeScreen.tsx => RuntimeDecoration.tsx} | 37 +++- .../src/ViewFlatteningScreen.tsx | 158 -------------- apps/basic-example/src/utils.ts | 9 - apps/macos-example/macos/Podfile.lock | 25 +++ .../android/CMakeLists.txt | 25 +++ ...GestureHandlerRootViewManagerDelegate.java | 8 +- ...estureHandlerRootViewManagerInterface.java | 2 +- .../gesturehandler/RNGestureHandlerPackage.kt | 11 +- .../gesturehandler/core/GestureHandler.kt | 2 + .../react/RNGestureHandlerDetectorView.kt | 87 ++++++++ .../RNGestureHandlerDetectorViewManager.kt | 43 ++++ .../react/RNGestureHandlerEvent.kt | 33 ++- .../react/RNGestureHandlerEventDispatcher.kt | 61 +++++- .../react/RNGestureHandlerModule.kt | 12 +- .../react/RNGestureHandlerRegistry.kt | 13 +- .../react/RNGestureHandlerRootHelper.kt | 9 +- .../react/RNGestureHandlerRootView.kt | 7 +- .../react/RNGestureHandlerRootViewManager.kt | 4 + .../react/RNGestureHandlerStateChangeEvent.kt | 31 ++- .../react/RNGestureHandlerTouchEvent.kt | 19 +- .../GestureHandlerEventDataBuilder.kt | 6 +- .../android/src/main/jni/CMakeLists.txt | 6 +- .../src/main/jni/RNGestureHandlerModule.cpp | 14 +- .../src/main/jni/RNGestureHandlerModule.h | 2 +- .../apple/Handlers/RNFlingHandler.m | 9 +- .../apple/RNGHVector.h | 10 +- .../apple/RNGHVector.m | 30 +-- .../apple/RNGestureHandler.h | 6 +- .../apple/RNGestureHandler.mm | 46 +++-- .../apple/RNGestureHandlerActionType.h | 2 + .../apple/RNGestureHandlerDetector.h | 25 +++ .../apple/RNGestureHandlerDetector.mm | 147 +++++++++++++ .../apple/RNGestureHandlerEvents.h | 15 +- ...dlerEvents.m => RNGestureHandlerEvents.mm} | 39 ++-- .../apple/RNGestureHandlerManager.h | 4 + .../apple/RNGestureHandlerManager.mm | 65 +++++- .../apple/RNGestureHandlerModule.h | 4 + .../apple/RNGestureHandlerModule.mm | 34 +-- .../apple/RNGestureHandlerNativeEventUtils.h | 15 ++ .../apple/RNGestureHandlerNativeEventUtils.mm | 88 ++++++++ ...er.m => RNGestureHandlerPointerTracker.mm} | 5 +- .../apple/RNGestureHandlerRegistry.h | 1 + .../apple/RNGestureHandlerRegistry.m | 6 + .../react-native-gesture-handler/package.json | 6 +- .../react-native.config.js | 10 + .../{ => runtime}/RNGHRuntimeDecorator.cpp | 10 + .../{ => runtime}/RNGHRuntimeDecorator.h | 2 + .../ComponentDescriptors.h | 30 +++ ...estureHandlerDetectorComponentDescriptor.h | 31 +++ .../RNGestureHandlerDetectorShadowNode.cpp | 58 ++++++ .../RNGestureHandlerDetectorShadowNode.h | 57 ++++++ .../RNGestureHandlerDetectorState.h | 32 +++ .../src/NativeDetector.tsx | 43 ++++ .../GestureHandlerRootView.android.tsx | 5 +- .../src/globals.ts | 8 + .../react-native-gesture-handler/src/index.ts | 8 +- ...RNGestureHandlerDetectorNativeComponent.ts | 58 ++++++ ...RNGestureHandlerRootViewNativeComponent.ts | 5 +- .../src/useGesture.ts | 67 ++++++ 67 files changed, 1427 insertions(+), 704 deletions(-) delete mode 100644 apps/basic-example/src/ComponentsScreen.tsx delete mode 100644 apps/basic-example/src/FinalScreen.tsx delete mode 100644 apps/basic-example/src/GestureCompositionScreen.tsx create mode 100644 apps/basic-example/src/NativeDetector.tsx rename apps/basic-example/src/{HomeScreen.tsx => RuntimeDecoration.tsx} (72%) delete mode 100644 apps/basic-example/src/ViewFlatteningScreen.tsx delete mode 100644 apps/basic-example/src/utils.ts create mode 100644 packages/react-native-gesture-handler/android/CMakeLists.txt create mode 100644 packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorView.kt create mode 100644 packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorViewManager.kt create mode 100644 packages/react-native-gesture-handler/apple/RNGestureHandlerDetector.h create mode 100644 packages/react-native-gesture-handler/apple/RNGestureHandlerDetector.mm rename packages/react-native-gesture-handler/apple/{RNGestureHandlerEvents.m => RNGestureHandlerEvents.mm} (87%) create mode 100644 packages/react-native-gesture-handler/apple/RNGestureHandlerNativeEventUtils.h create mode 100644 packages/react-native-gesture-handler/apple/RNGestureHandlerNativeEventUtils.mm rename packages/react-native-gesture-handler/apple/{RNGestureHandlerPointerTracker.m => RNGestureHandlerPointerTracker.mm} (96%) create mode 100644 packages/react-native-gesture-handler/react-native.config.js rename packages/react-native-gesture-handler/shared/{ => runtime}/RNGHRuntimeDecorator.cpp (92%) rename packages/react-native-gesture-handler/shared/{ => runtime}/RNGHRuntimeDecorator.h (91%) create mode 100644 packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/ComponentDescriptors.h create mode 100644 packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerDetectorComponentDescriptor.h create mode 100644 packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerDetectorShadowNode.cpp create mode 100644 packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerDetectorShadowNode.h create mode 100644 packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerDetectorState.h create mode 100644 packages/react-native-gesture-handler/src/NativeDetector.tsx create mode 100644 packages/react-native-gesture-handler/src/globals.ts create mode 100644 packages/react-native-gesture-handler/src/specs/RNGestureHandlerDetectorNativeComponent.ts create mode 100644 packages/react-native-gesture-handler/src/useGesture.ts diff --git a/apps/basic-example/Gemfile.lock b/apps/basic-example/Gemfile.lock index 3e46c77c15..9f9cbc5984 100644 --- a/apps/basic-example/Gemfile.lock +++ b/apps/basic-example/Gemfile.lock @@ -5,15 +5,18 @@ GEM base64 nkf rexml - activesupport (7.1.4.2) + activesupport (7.1.5.1) base64 + benchmark (>= 0.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) mutex_m + securerandom (>= 0.3) tzinfo (~> 2.0) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) @@ -21,9 +24,9 @@ GEM httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) atomos (0.1.3) - base64 (0.2.0) - benchmark (0.4.0) - bigdecimal (3.1.9) + base64 (0.3.0) + benchmark (0.4.1) + bigdecimal (3.2.2) claide (1.1.0) cocoapods (1.15.2) addressable (~> 2.8) @@ -64,8 +67,8 @@ GEM cocoapods-try (1.2.0) colored2 (3.1.2) concurrent-ruby (1.3.3) - connection_pool (2.5.1) - drb (2.2.1) + connection_pool (2.5.3) + drb (2.2.3) escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) @@ -77,7 +80,7 @@ GEM mutex_m i18n (1.14.7) concurrent-ruby (~> 1.0) - json (2.10.2) + json (2.12.2) logger (1.7.0) minitest (5.25.5) molinillo (0.8.0) @@ -89,6 +92,7 @@ GEM public_suffix (4.0.7) rexml (3.4.1) ruby-macho (2.5.1) + securerandom (0.3.2) typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.6) diff --git a/apps/basic-example/ios/Podfile.lock b/apps/basic-example/ios/Podfile.lock index 27ba11a5e7..d784023016 100644 --- a/apps/basic-example/ios/Podfile.lock +++ b/apps/basic-example/ios/Podfile.lock @@ -2678,7 +2678,7 @@ SPEC CHECKSUMS: RNReanimated: 25060745a200605462ff56cf488411db066631ce RNWorklets: 9bb08cb0ef718ce063f61ca18f95f57aec9b9673 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - Yoga: 0c4b7d2aacc910a1f702694fa86be830386f4ceb + Yoga: 395b5d614cd7cbbfd76b05d01bd67230a6ad004e PODFILE CHECKSUM: d05778d3a61b8d49242579ea0aa864580fbb1f64 diff --git a/apps/basic-example/src/App.tsx b/apps/basic-example/src/App.tsx index 8f5193ee0c..fceb9a2ba2 100644 --- a/apps/basic-example/src/App.tsx +++ b/apps/basic-example/src/App.tsx @@ -4,55 +4,47 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler'; import Navigator from './Navigator'; -import ComponentsScreen from './ComponentsScreen'; -import FinalScreen from './FinalScreen'; -import GestureCompositionScreen from './GestureCompositionScreen'; -import HomeScreen from './HomeScreen'; -import ViewFlatteningScreen from './ViewFlatteningScreen'; +import NativeDetector from './NativeDetector'; +import RuntimeDecoration from './RuntimeDecoration'; -const Stack = Navigator.create(); - -Stack.setRoutes({ - home: { - component: HomeScreen, - title: 'RNGH FabricExample', - rightButtonAction: () => { - Stack.navigateTo('gestureComposition'); - }, - }, - gestureComposition: { - component: GestureCompositionScreen, - title: 'Gesture Composition', - rightButtonAction: () => { - Stack.navigateTo('components'); - }, - }, - components: { - component: ComponentsScreen, - title: 'Components', - rightButtonAction: () => { - Stack.navigateTo('viewFlattening'); - }, +const EXAMPLES = [ + { + name: 'Runtime Decoration', + component: RuntimeDecoration, }, - viewFlattening: { - component: ViewFlatteningScreen, - title: 'View Flattening', - rightButtonAction: () => { - Stack.navigateTo('final'); - }, + { + name: 'Native Detector', + component: NativeDetector, }, - final: { - component: FinalScreen, - title: 'Final Screen', - }, -}); +]; + +const Stack = Navigator.create(); +Stack.setRoutes( + Object.fromEntries( + EXAMPLES.map((example, index) => [ + example.name.toLowerCase().replace(/\s+/g, ''), + { + component: example.component, + title: example.name, + rightButtonAction: + index === EXAMPLES.length - 1 + ? undefined + : () => { + Stack.navigateTo( + EXAMPLES[index + 1].name.toLowerCase().replace(/\s+/g, '') + ); + }, + }, + ]) + ) +); export default function App() { return ( - + ); diff --git a/apps/basic-example/src/ComponentsScreen.tsx b/apps/basic-example/src/ComponentsScreen.tsx deleted file mode 100644 index 8a3eb59fd9..0000000000 --- a/apps/basic-example/src/ComponentsScreen.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import React from 'react'; -import { - FlatList, - Gesture, - GestureDetector, - ScrollView, - Switch, - TextInput, - TouchableNativeFeedback, - TouchableOpacity, -} from 'react-native-gesture-handler'; -import { StyleSheet, Text, View } from 'react-native'; - -import { COLORS } from './colors'; - -function SwitchDemo() { - const [value, setValue] = React.useState(false); - - const pan = Gesture.Pan() - .onBegin(() => console.log('onBegin')) - .onUpdate(() => console.log('onUpdate')) // doesn't work on iOS - .onFinalize(() => console.log('onFinalize')); - - return ( - - RNGH Switch - - setValue(!value)} /> - - - ); -} - -function TextInputDemo() { - const [value, setValue] = React.useState('Hello!'); - - const pan = Gesture.Pan() - .onBegin(() => console.log('onBegin')) - .onUpdate(() => console.log('onUpdate')) - .onFinalize(() => console.log('onFinalize')); - - return ( - - RNGH TextInput - - - - - ); -} - -function TouchableNativeFeedbackDemo() { - return ( - - RNGH TouchableNativeFeedback - console.log('onPressIn')} - onPressOut={() => console.log('onPressOut')} - onLongPress={() => console.log('onLongPress')}> - - - - ); -} - -function TouchableOpacityDemo() { - return ( - - RNGH TouchableOpacity - console.log('onPressIn')} - onPressOut={() => console.log('onPressOut')} - onLongPress={() => console.log('onLongPress')}> - - - - ); -} - -function ScrollViewDemo() { - const pan = Gesture.Pan().onUpdate((e) => console.log('onUpdate', e.x, e.y)); - - return ( - - RNGH ScrollView - - - {Object.values(COLORS).map((color) => ( - - ))} - - - - Dragging here should not scroll - - - - - - - ); -} - -function FlatListDemo() { - return ( - - RNGH FlatList - - ( - - - - )} - keyExtractor={(e) => e} - /> - - - ); -} - -export default function ComponentsScreen() { - return ( - - - Gesture Handler also provides a set of components that support gestures. - - - - - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - }, - bold: { - fontWeight: 'bold', - textAlign: 'center', - marginHorizontal: 20, - }, - textInput: { - borderWidth: 1, - borderStyle: 'solid', - borderColor: 'darkgray', - width: 135, - padding: 10, - }, - text: { - marginVertical: 3, - }, - demo: { - marginVertical: 3, - alignItems: 'center', - }, - smallBox: { - width: 50, - height: 50, - }, - largeBox: { - width: 100, - height: 100, - }, - panBox: { - width: 100, - height: 75, - backgroundColor: 'lightgray', - alignItems: 'center', - justifyContent: 'center', - }, - panText: { - textAlign: 'center', - }, -}); diff --git a/apps/basic-example/src/FinalScreen.tsx b/apps/basic-example/src/FinalScreen.tsx deleted file mode 100644 index 9e65cf8135..0000000000 --- a/apps/basic-example/src/FinalScreen.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import { Image, StyleSheet, Text, View } from 'react-native'; -// @ts-ignore TODO: remove once there is a .d.ts file with definitions -import openURLInBrowser from 'react-native/Libraries/Core/Devtools/openURLInBrowser'; - -import { COLORS } from './colors'; - -const SOFTWARE_MANSION_LOGO_URL = - 'https://pbs.twimg.com/profile_images/1243176655172009986/Jgdl2m15_400x400.jpg'; -const SOFTWARE_MANSION_TWITTER_URL = 'https://twitter.com/swmansion/'; - -export default function FinalScreen() { - return ( - - - React Native Gesture Handler is developed by Software Mansion. - - - We are actively porting React Native libraries to Fabric, including - React Native Screens and Reanimated. - - - Follow us on Twitter and stay tuned! @swmansion - - openURLInBrowser(SOFTWARE_MANSION_TWITTER_URL)}> - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: COLORS.NAVY, - }, - text: { - textAlign: 'center', - color: 'white', - fontSize: 21, - marginHorizontal: 20, - marginVertical: 10, - }, - bold: { - fontWeight: 'bold', - }, - logo: { - width: 250, - height: 250, - }, -}); diff --git a/apps/basic-example/src/GestureCompositionScreen.tsx b/apps/basic-example/src/GestureCompositionScreen.tsx deleted file mode 100644 index 2eb920c62f..0000000000 --- a/apps/basic-example/src/GestureCompositionScreen.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import React from 'react'; -import { StyleSheet, Text, View } from 'react-native'; -import { Gesture, GestureDetector } from 'react-native-gesture-handler'; - -import { COLORS } from './colors'; - -function RaceDemo() { - const pan = Gesture.Pan() - .onStart(() => console.log('pan onStart')) - .onUpdate(() => console.log('pan onUpdate')) - .onEnd(() => console.log('pan onEnd')); - - const longPress = Gesture.LongPress() - .onStart(() => console.log('longPress onStart')) - .onEnd(() => console.log('longPress onEnd')); - - return ( - - - Gesture.Race(pan, longPress) - the first gesture that meets its - activation criteria will activate - - - - - - ); -} - -function ExclusiveDemo() { - const singleTap = Gesture.Tap().onStart(() => console.log('single tap!')); - const doubleTap = Gesture.Tap() - .onStart(() => console.log('double tap!')) - .numberOfTaps(2); - - return ( - - - Gesture.Exclusive(doubleTap, singleTap) - the second gesture will wait - for the failure of the first one - - - - - - ); -} - -function SimultaneousDemo() { - const pinch = Gesture.Pinch() - .onStart(() => console.log('pinch onStart')) - .onUpdate(() => console.log('pinch onUpdate')) - .onEnd(() => console.log('pinch onEnd')); - const rotation = Gesture.Rotation() - .onStart(() => console.log('rotation onStart')) - .onUpdate(() => console.log('rotation onUpdate')) - .onEnd(() => console.log('rotation onEnd')); - - return ( - - - Gesture.Simultaneous(pinch, rotation) - both gestures can activate and - process touches at the same time - - - - - - ); -} - -export default function ComponentsScreen() { - return ( - - - Gesture Handler provides a simple API for using multiple gestures at - once in different configurations. - - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - }, - bold: { - fontWeight: 'bold', - textAlign: 'center', - marginHorizontal: 20, - }, - text: { - marginVertical: 3, - marginHorizontal: 10, - textAlign: 'center', - }, - demo: { - marginVertical: 3, - alignItems: 'center', - }, - largeBox: { - width: 100, - height: 100, - }, - largerBox: { - width: 150, - height: 150, - }, -}); diff --git a/apps/basic-example/src/NativeDetector.tsx b/apps/basic-example/src/NativeDetector.tsx new file mode 100644 index 0000000000..b5cfc7b3b3 --- /dev/null +++ b/apps/basic-example/src/NativeDetector.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import { Animated, Button, useAnimatedValue } from 'react-native'; +import { + GestureHandlerRootView, + NativeDetector, + useGesture, +} from 'react-native-gesture-handler'; + +export default function App() { + const [visible, setVisible] = React.useState(true); + + const value = useAnimatedValue(0); + const event = Animated.event( + [{ nativeEvent: { handlerData: { translationX: value } } }], + { + useNativeDriver: true, + } + ); + + const gesture = useGesture('PanGestureHandler', { + onGestureHandlerAnimatedEvent: event, + onGestureHandlerEvent: (e: any) => + console.log('onGestureHandlerEvent', e.nativeEvent), + }); + + return ( + + + ); }; diff --git a/packages/docs-gesture-handler/src/components/CollapseButton/styles.module.css b/packages/docs-gesture-handler/src/components/CollapseButton/styles.module.css index 908df5fa09..43444e4535 100644 --- a/packages/docs-gesture-handler/src/components/CollapseButton/styles.module.css +++ b/packages/docs-gesture-handler/src/components/CollapseButton/styles.module.css @@ -13,6 +13,7 @@ font-size: 16px; color: var(--ifm-font-color-base); cursor: pointer; + user-select: none; } .arrow { diff --git a/packages/docs-gesture-handler/src/components/CollapsibleCode/index.tsx b/packages/docs-gesture-handler/src/components/CollapsibleCode/index.tsx index ae0ce28a48..e7ed873f99 100644 --- a/packages/docs-gesture-handler/src/components/CollapsibleCode/index.tsx +++ b/packages/docs-gesture-handler/src/components/CollapsibleCode/index.tsx @@ -1,36 +1,61 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import CodeBlock from '@theme/CodeBlock'; import styles from './styles.module.css'; import CollapseButton from '@site/src/components/CollapseButton'; +import * as prettier from 'prettier/standalone'; +import tsParser from 'prettier/plugins/typescript'; +import estreeParser from 'prettier/plugins/estree'; + +const prettierOptions = { + parser: 'typescript', + plugins: [tsParser, estreeParser], +}; + interface Props { src: string; + label: string; + expandedLabel: string; lineBounds: number[]; } -export default function CollapsibleCode({ src, lineBounds }: Props) { +export default function CollapsibleCode({ + src, + label, + expandedLabel, + lineBounds, +}: Props) { const [collapsed, setCollapsed] = useState(true); + const [code, setCode] = useState(src); + + useEffect(() => { + async function formatCode() { + const formattedCode = await prettier.format(src, prettierOptions); + setCode(formattedCode); + } + void formatCode(); + }, [src]); if (!lineBounds) { - return {src}; + return {code}; } const [start, end] = lineBounds; - const codeLines = src.split('\n'); + const codeLines = code.split('\n'); const linesToShow = codeLines.slice(start, end + 1).join('\n'); return (
setCollapsed(!collapsed)} className={styles.collapseButton} /> - {collapsed ? linesToShow : src} + {collapsed ? linesToShow : code}
); } From 7a4f77462bfa3f2aa62fec8f8b4dca5c9132dc1e Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 27 Jan 2026 15:27:24 +0100 Subject: [PATCH 208/236] [Native] `display: contents` based button styling (#3634) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Changes the button implementation of the native `GestureHandlerButton` component, not to re-export the button component. Instead, it provides a "sandwich" implementation based on `display: contents`. The issues we had with the styling of the buttons in the past stemmed from it extending `ViewGroup` instead of `ReactViewGroup`. This cannot be changed due to the difference in handling of `onTouchEvent`. We experimented with wrapping the button with a `View` in the past, but properly handling layout turned out to be quite a challenge. Hopefully, with the addition of `display: contents`, this will be easier now. This PR changes the structure of the button to be: - `RNGestureHandlerButtonWrapperNativeComponent` - `View` - `ButtonComponent` The styles passed from the user are split into two categories: - visual only - affecting layout Those affecting the layout need to be set on the button itself, while visual changes need to be set on the View wrapping it. Exceptions are: `zIndex`, `transform`, and `transformOrigin`, which need to be on the View. `zIndex` because the View is being rendered in the hierarchy at the level the button would be expected, and transforms to properly calculate touch coordinate transforms. ### Why not just `ButtonComponent`? We need a `View` component as its parent to handle all styles that React Native supports properly. ### Why not just `ButtonComponent` wrapped with `View`? In order for the view to always match the layout of the button, we need to set it at the shadow node level. Shadow nodes only have references to their children, not their parents. To have access to the layout of the button and the ability to copy it to the view wrapping it, there needs to be one more node as the parent of that subtree. That node is responsible for making sure that the size and positioning of the View match the Button. The native components for the wrapper serve no purpose, since it's being rendered with `display: contents`, but RN doesn't know that - there needs to be something it can link to. ## Test plan See the `ContentsButton` file --------- Co-authored-by: Michał Bert <63123542+m-bert@users.noreply.github.com> --- apps/basic-example/ios/Podfile.lock | 4 +- apps/basic-example/src/App.tsx | 5 + apps/basic-example/src/ContentsButton.tsx | 522 ++++++++++++++++++ .../gesturehandler/RNGestureHandlerPackage.kt | 5 + ...NGestureHandlerButtonWrapperViewManager.kt | 30 + .../react/RNGestureHandlerDetectorView.kt | 16 + .../apple/RNGestureHandlerButtonWrapper.h | 19 + .../apple/RNGestureHandlerButtonWrapper.mm | 46 ++ .../apple/RNGestureHandlerDetector.mm | 27 + .../react-native-gesture-handler/package.json | 3 +- .../react-native.config.js | 5 +- .../ComponentDescriptors.h | 2 + ...eHandlerButtonWrapperComponentDescriptor.h | 32 ++ ...NGestureHandlerButtonWrapperShadowNode.cpp | 98 ++++ .../RNGestureHandlerButtonWrapperShadowNode.h | 56 ++ .../RNGestureHandlerButtonWrapperState.h | 32 ++ .../src/components/GestureHandlerButton.tsx | 148 ++++- ...tureHandlerButtonWrapperNativeComponent.ts | 11 + 18 files changed, 1054 insertions(+), 7 deletions(-) create mode 100644 apps/basic-example/src/ContentsButton.tsx create mode 100644 packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonWrapperViewManager.kt create mode 100644 packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.h create mode 100644 packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.mm create mode 100644 packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperComponentDescriptor.h create mode 100644 packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.cpp create mode 100644 packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.h create mode 100644 packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperState.h create mode 100644 packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonWrapperNativeComponent.ts diff --git a/apps/basic-example/ios/Podfile.lock b/apps/basic-example/ios/Podfile.lock index 20ad8ccb84..bbaa392846 100644 --- a/apps/basic-example/ios/Podfile.lock +++ b/apps/basic-example/ios/Podfile.lock @@ -2919,7 +2919,7 @@ SPEC CHECKSUMS: FBLazyVector: a293a88992c4c33f0aee184acab0b64a08ff9458 fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 - hermes-engine: db854ab6e74b584dc130d3ed0be3425726bac226 + hermes-engine: eec912f8a125ae0d3ad67b2e7b81a227319aa13b RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCTDeprecation: 2b70c6e3abe00396cefd8913efbf6a2db01a2b36 RCTRequired: f3540eee8094231581d40c5c6d41b0f170237a81 @@ -2993,7 +2993,7 @@ SPEC CHECKSUMS: RNReanimated: 987d0b9af435441cc2ebc2a32ad06cafe8777d4e RNWorklets: 12b2d7cdcb48acbdd0b324d1fa810f849089bd7b SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - Yoga: 6ca93c8c13f56baeec55eb608577619b17a4d64e + Yoga: 7a9f26c70daf0b08d82ec2f862e9a8872442129e PODFILE CHECKSUM: ecce038d8e4749ee17b7dea28be0590cdc8b4836 diff --git a/apps/basic-example/src/App.tsx b/apps/basic-example/src/App.tsx index 483fdc1b7c..2a5fc77b2e 100644 --- a/apps/basic-example/src/App.tsx +++ b/apps/basic-example/src/App.tsx @@ -7,6 +7,7 @@ import Navigator from './Navigator'; import Text from './Text'; import NativeDetector from './NativeDetector'; import RuntimeDecoration from './RuntimeDecoration'; +import ContentsButton from './ContentsButton'; const EXAMPLES = [ { @@ -21,6 +22,10 @@ const EXAMPLES = [ name: 'Native Detector', component: NativeDetector, }, + { + name: 'Contents Button', + component: ContentsButton, + }, ]; const Stack = Navigator.create(); diff --git a/apps/basic-example/src/ContentsButton.tsx b/apps/basic-example/src/ContentsButton.tsx new file mode 100644 index 0000000000..23906db5ad --- /dev/null +++ b/apps/basic-example/src/ContentsButton.tsx @@ -0,0 +1,522 @@ +import React from 'react'; +import { View, StyleSheet, Text, SafeAreaView } from 'react-native'; +import { + GestureHandlerRootView, + ScrollView, + RectButton, +} from 'react-native-gesture-handler'; + +export default function ComplexUI() { + return ( + + + + + + + + + + + + + + + + + + ); +} + +const colors = ['#782AEB', '#38ACDD', '#57B495', '#FF6259', '#FFD61E']; + +function Avatars() { + return ( + + {colors.map((color) => ( + + {color.slice(1, 3)} + + ))} + + ); +} + +function Gallery() { + return ( + + Basic Gallery + + + + + + + + + + ); +} + +function SizeConstraints() { + return ( + + Size Constraints + + + Min/Max + + + 1:1 + + + Flex + + + + ); +} + +function FlexboxTests() { + return ( + + Flexbox Layouts + + + Start + + + Center + + + End + + + + + Wrap 1 + + + Wrap 2 + + + Wrap 3 + + + Wrap 4 + + + + ); +} + +function PositioningTests() { + return ( + + Positioning + + + Z-Index + + + Absolute + + + Relative + + + + ); +} + +function SpacingTests() { + return ( + + Spacing & Overflow + + + Padding + + + Margin + + + Overflow Hidden Test + + + + ); +} + +function VisualEffects() { + return ( + + Visual Effects + + + Shadow + + + Opacity + + + + ); +} + +function ComplexCombinations() { + return ( + + Complex Combinations + + + Complex 1 + + + Complex 2 + + + Complex 3 + + + Complex 4 + + + + ); +} + +function Transforms() { + return ( + + Transform + + + Transform + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f5f5f5', + }, + scrollContent: { + paddingBottom: 50, + }, + paddedContainer: { + padding: 16, + }, + section: { + marginBottom: 30, + }, + sectionTitle: { + fontSize: 18, + fontWeight: 'bold', + marginBottom: 12, + color: '#333', + }, + gap: { + gap: 10, + }, + row: { + flexDirection: 'row', + }, + + // Avatar styles + avatars: { + width: 90, + height: 90, + borderWidth: 2, + borderColor: '#001A72', + borderTopLeftRadius: 30, + borderTopRightRadius: 5, + borderBottomLeftRadius: 5, + borderBottomRightRadius: 30, + marginHorizontal: 4, + alignItems: 'center', + justifyContent: 'center', + }, + avatarLabel: { + color: '#F8F9FF', + fontSize: 24, + fontWeight: 'bold', + }, + + // Gallery styles + fullWidthButton: { + width: '100%', + height: 160, + backgroundColor: '#FF6259', + borderTopRightRadius: 30, + borderTopLeftRadius: 30, + borderWidth: 1, + borderColor: '#000', + }, + leftButton: { + flex: 1, + height: 160, + backgroundColor: '#FFD61E', + borderBottomLeftRadius: 30, + borderWidth: 5, + borderColor: '#000', + }, + rightButton: { + flex: 1, + backgroundColor: '#782AEB', + height: 160, + borderBottomRightRadius: 30, + borderWidth: 8, + borderColor: '#000', + }, + + // Size constraint styles + minMaxButton: { + minWidth: 80, + maxWidth: 120, + minHeight: 40, + maxHeight: 80, + backgroundColor: '#38ACDD', + borderRadius: 10, + justifyContent: 'center', + alignItems: 'center', + }, + aspectRatioButton: { + width: 80, + aspectRatio: 1, + backgroundColor: '#57B495', + borderRadius: 40, + justifyContent: 'center', + alignItems: 'center', + }, + flexGrowButton: { + flexGrow: 1, + height: 60, + backgroundColor: '#FF6259', + borderRadius: 15, + justifyContent: 'center', + alignItems: 'center', + }, + + // Flexbox styles + flexContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + height: 80, + backgroundColor: '#e0e0e0', + borderRadius: 10, + padding: 10, + marginBottom: 10, + }, + flexStart: { + alignSelf: 'flex-start', + backgroundColor: '#782AEB', + padding: 10, + borderRadius: 5, + }, + flexCenter: { + alignSelf: 'center', + backgroundColor: '#38ACDD', + padding: 10, + borderRadius: 5, + }, + flexEnd: { + alignSelf: 'flex-end', + backgroundColor: '#57B495', + padding: 10, + borderRadius: 5, + }, + flexWrapContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 5, + }, + wrapItem: { + width: '48%', + height: 50, + backgroundColor: '#FFD61E', + borderRadius: 8, + justifyContent: 'center', + alignItems: 'center', + }, + + // Positioning styles + positionContainer: { + height: 80, + backgroundColor: '#e0e0e0', + borderRadius: 10, + position: 'relative', + }, + absoluteButton: { + position: 'absolute', + top: 10, + right: 10, + backgroundColor: '#FF6259', + padding: 8, + borderRadius: 5, + }, + relativeButton: { + position: 'relative', + top: 20, + left: 20, + backgroundColor: '#782AEB', + padding: 8, + borderRadius: 5, + }, + zIndexButton: { + position: 'absolute', + top: 10, + left: 10, + zIndex: 10, + backgroundColor: '#57B495', + padding: 8, + borderRadius: 5, + }, + + // Spacing styles + paddingButton: { + flex: 1, + backgroundColor: '#38ACDD', + paddingVertical: 20, + paddingHorizontal: 15, + borderRadius: 10, + justifyContent: 'center', + alignItems: 'center', + }, + marginButton: { + flex: 1, + backgroundColor: '#FFD61E', + margin: 10, + padding: 10, + borderRadius: 10, + justifyContent: 'center', + alignItems: 'center', + }, + overflowButton: { + flex: 1, + height: 60, + backgroundColor: '#782AEB', + borderRadius: 10, + overflow: 'hidden', + justifyContent: 'center', + alignItems: 'center', + }, + + // Visual effect styles + shadowButton: { + flex: 1, + height: 60, + backgroundColor: '#FF6259', + borderRadius: 10, + justifyContent: 'center', + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 8, + }, + opacityButton: { + flex: 1, + height: 60, + backgroundColor: '#782AEB', + borderRadius: 10, + justifyContent: 'center', + alignItems: 'center', + opacity: 0.7, + }, + + // Complex combination styles + complexGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 10, + }, + complexButton1: { + width: '48%', + height: 100, + backgroundColor: '#FF6259', + borderRadius: 20, + borderWidth: 2, + borderColor: '#782AEB', + justifyContent: 'center', + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 2, height: 2 }, + shadowOpacity: 0.2, + shadowRadius: 4, + elevation: 4, + marginBottom: 5, + }, + complexButton2: { + width: '48%', + minHeight: 80, + maxHeight: 120, + backgroundColor: '#38ACDD', + borderTopLeftRadius: 30, + borderBottomRightRadius: 30, + paddingVertical: 15, + paddingHorizontal: 10, + justifyContent: 'center', + alignItems: 'center', + overflow: 'hidden', + }, + complexButton3: { + width: '48%', + aspectRatio: 1.5, + backgroundColor: '#57B495', + borderRadius: 15, + borderWidth: 4, + borderColor: '#FFD61E', + justifyContent: 'center', + alignItems: 'center', + opacity: 0.9, + marginTop: 10, + }, + complexButton4: { + width: '48%', + height: 80, + backgroundColor: '#FFD61E', + borderRadius: 10, + borderWidth: 1, + borderColor: '#FF6259', + justifyContent: 'center', + alignItems: 'center', + shadowColor: '#782AEB', + shadowOffset: { width: 0, height: 6 }, + shadowOpacity: 0.4, + shadowRadius: 10, + elevation: 10, + marginTop: 10, + }, + + // Transform styles + transformButton: { + width: '48%', + height: 100, + backgroundColor: '#38ACDD', + borderRadius: 15, + justifyContent: 'center', + alignItems: 'center', + transform: [{ translateX: 100 }, { rotate: '15deg' }, { scale: 1.1 }], + }, + + // Text styles + buttonText: { + color: 'white', + fontWeight: 'bold', + textAlign: 'center', + }, + longText: { + color: 'white', + fontWeight: 'bold', + textAlign: 'center', + fontSize: 16, + }, +}); diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/RNGestureHandlerPackage.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/RNGestureHandlerPackage.kt index 95f071671f..d5c7001e20 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/RNGestureHandlerPackage.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/RNGestureHandlerPackage.kt @@ -11,6 +11,7 @@ import com.facebook.react.module.model.ReactModuleInfo import com.facebook.react.module.model.ReactModuleInfoProvider import com.facebook.react.uimanager.ViewManager import com.swmansion.gesturehandler.react.RNGestureHandlerButtonViewManager +import com.swmansion.gesturehandler.react.RNGestureHandlerButtonWrapperViewManager import com.swmansion.gesturehandler.react.RNGestureHandlerDetectorViewManager import com.swmansion.gesturehandler.react.RNGestureHandlerModule import com.swmansion.gesturehandler.react.RNGestureHandlerRootViewManager @@ -34,6 +35,9 @@ class RNGestureHandlerPackage : RNGestureHandlerDetectorViewManager.REACT_CLASS to ModuleSpec.viewManagerSpec { RNGestureHandlerDetectorViewManager() }, + RNGestureHandlerButtonWrapperViewManager.REACT_CLASS to ModuleSpec.viewManagerSpec { + RNGestureHandlerButtonWrapperViewManager() + }, ) } @@ -41,6 +45,7 @@ class RNGestureHandlerPackage : RNGestureHandlerRootViewManager(), RNGestureHandlerButtonViewManager(), RNGestureHandlerDetectorViewManager(), + RNGestureHandlerButtonWrapperViewManager(), ) override fun getViewManagerNames(reactContext: ReactApplicationContext) = viewManagers.keys.toList() diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonWrapperViewManager.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonWrapperViewManager.kt new file mode 100644 index 0000000000..7137243bd7 --- /dev/null +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonWrapperViewManager.kt @@ -0,0 +1,30 @@ +package com.swmansion.gesturehandler.react + +import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.ViewGroupManager +import com.facebook.react.uimanager.ViewManagerDelegate +import com.facebook.react.viewmanagers.RNGestureHandlerButtonWrapperManagerDelegate +import com.facebook.react.viewmanagers.RNGestureHandlerButtonWrapperManagerInterface +import com.facebook.react.views.view.ReactViewGroup + +@ReactModule(name = RNGestureHandlerButtonWrapperViewManager.REACT_CLASS) +class RNGestureHandlerButtonWrapperViewManager : + ViewGroupManager(), + RNGestureHandlerButtonWrapperManagerInterface { + private val mDelegate: ViewManagerDelegate = + RNGestureHandlerButtonWrapperManagerDelegate< + ReactViewGroup, + RNGestureHandlerButtonWrapperViewManager, + >(this) + + override fun getDelegate(): ViewManagerDelegate = mDelegate + + override fun getName() = REACT_CLASS + + override fun createViewInstance(reactContext: ThemedReactContext) = ReactViewGroup(reactContext) + + companion object { + const val REACT_CLASS = "RNGestureHandlerButtonWrapper" + } +} diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorView.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorView.kt index c57dd8ad37..a91811c227 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorView.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorView.kt @@ -2,6 +2,8 @@ package com.swmansion.gesturehandler.react import android.content.Context import android.view.View +import android.view.ViewGroup +import androidx.core.view.isNotEmpty import com.facebook.react.bridge.ReadableArray import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.UIManagerHelper @@ -160,6 +162,9 @@ class RNGestureHandlerDetectorView(context: Context) : ReactViewGroup(context) { // Note: RefreshControl is wrapped with a VirtualDetector, and native gestures for it are attached in `attachVirtualChildren`. val id = if (child is ReactSwipeRefreshLayout) { child.getChildAt(0).id + // TODO: figure out how to do it correctly + } else if (child is ViewGroup && child.isNotEmpty()) { + child.tryFindGestureHandlerButton()?.id ?: child.id } else { child.id } @@ -212,4 +217,15 @@ class RNGestureHandlerDetectorView(context: Context) : ReactViewGroup(context) { }.filterNotNull() private fun ReadableArray.toIntList(): List = List(size()) { getInt(it) } + + private fun ViewGroup.tryFindGestureHandlerButton(): RNGestureHandlerButtonViewManager.ButtonViewGroup? { + if (isNotEmpty()) { + val child = getChildAt(0) + if (child is RNGestureHandlerButtonViewManager.ButtonViewGroup) { + return child + } + } + + return null + } } diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.h b/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.h new file mode 100644 index 0000000000..c94ecf946e --- /dev/null +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.h @@ -0,0 +1,19 @@ +#if !TARGET_OS_OSX +#import +#else +#import +#endif + +#import + +#import + +using namespace facebook::react; + +NS_ASSUME_NONNULL_BEGIN + +@interface RNGestureHandlerButtonWrapper : RCTViewComponentView + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.mm new file mode 100644 index 0000000000..526c7743d6 --- /dev/null +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.mm @@ -0,0 +1,46 @@ +#import "RNGestureHandlerButtonWrapper.h" +#import "RNGestureHandlerButtonWrapperComponentDescriptor.h" +#import "RNGestureHandlerModule.h" + +#import +#import + +#import +#import +#import + +@interface RNGestureHandlerButtonWrapper () +@end + +@implementation RNGestureHandlerButtonWrapper + +#if TARGET_OS_OSX ++ (BOOL)shouldBeRecycled +{ + return NO; +} +#endif + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + } + + return self; +} + +#pragma mark - RCTComponentViewProtocol + ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} + +@end + +Class RNGestureHandlerButtonWrapperCls(void) +{ + return RNGestureHandlerButtonWrapper.class; +} diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerDetector.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerDetector.mm index de67ea1694..c6f61e2954 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerDetector.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerDetector.mm @@ -1,4 +1,5 @@ #import "RNGestureHandlerDetector.h" +#import "RNGestureHandlerButtonComponentView.h" #import "RNGestureHandlerDetectorComponentDescriptor.h" #import "RNGestureHandlerModule.h" @@ -9,6 +10,7 @@ #import #import +#import #include @interface RNGestureHandlerDetector () @@ -295,10 +297,16 @@ - (void)tryAttachNativeHandlersToChildView RNGHUIView *view = self.subviews[0]; + // TODO: figure out how to do it correctly if ([view isKindOfClass:[RCTViewComponentView class]]) { RCTViewComponentView *componentView = (RCTViewComponentView *)view; if (componentView.contentView != nil) { view = componentView.contentView; + } else { + auto buttonView = [self tryFindGestureHandlerButton:view]; + if (buttonView != nil) { + view = buttonView; + } } } @@ -322,6 +330,25 @@ - (void)detachNativeGestureHandlers } } +- (RNGHUIView *)tryFindGestureHandlerButton:(RNGHUIView *)inView +{ + if (inView.subviews.count == 0) { + return nil; + } + + auto view = inView.subviews[0]; + + if ([view isKindOfClass:[RNGestureHandlerButtonComponentView class]]) { + RCTViewComponentView *componentView = (RCTViewComponentView *)view; + + if (componentView.contentView != nil) { + return componentView.contentView; + } + } + + return nil; +} + @end Class RNGestureHandlerDetectorCls(void) diff --git a/packages/react-native-gesture-handler/package.json b/packages/react-native-gesture-handler/package.json index 61b4a6fd24..91b04e4d88 100644 --- a/packages/react-native-gesture-handler/package.json +++ b/packages/react-native-gesture-handler/package.json @@ -128,7 +128,8 @@ "ios": { "componentProvider": { "RNGestureHandlerButton": "RNGestureHandlerButtonComponentView", - "RNGestureHandlerDetector": "RNGestureHandlerDetector" + "RNGestureHandlerDetector": "RNGestureHandlerDetector", + "RNGestureHandlerButtonWrapper": "RNGestureHandlerButtonWrapper" } } }, diff --git a/packages/react-native-gesture-handler/react-native.config.js b/packages/react-native-gesture-handler/react-native.config.js index 4ec41154cd..46e3de4682 100644 --- a/packages/react-native-gesture-handler/react-native.config.js +++ b/packages/react-native-gesture-handler/react-native.config.js @@ -2,7 +2,10 @@ module.exports = { dependency: { platforms: { android: { - componentDescriptors: ['RNGestureHandlerDetectorComponentDescriptor'], + componentDescriptors: [ + 'RNGestureHandlerDetectorComponentDescriptor', + 'RNGestureHandlerButtonWrapperComponentDescriptor', + ], cmakeListsPath: './CMakeLists.txt', }, }, diff --git a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/ComponentDescriptors.h b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/ComponentDescriptors.h index d9f3374c48..5621db20e7 100644 --- a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/ComponentDescriptors.h +++ b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/ComponentDescriptors.h @@ -15,12 +15,14 @@ #include #include +#include #include namespace facebook::react { using RNGestureHandlerButtonComponentDescriptor = ConcreteComponentDescriptor; + using RNGestureHandlerRootViewComponentDescriptor = ConcreteComponentDescriptor; diff --git a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperComponentDescriptor.h b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperComponentDescriptor.h new file mode 100644 index 0000000000..c3323ae823 --- /dev/null +++ b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperComponentDescriptor.h @@ -0,0 +1,32 @@ + +/** + * This code was generated by + * [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be + * lost once the code is regenerated. + * + * @generated by codegen project: GenerateComponentDescriptorH.js + */ + +#pragma once + +#include + +#include "RNGestureHandlerButtonWrapperShadowNode.h" + +namespace facebook::react { + +class RNGestureHandlerButtonWrapperComponentDescriptor final + : public ConcreteComponentDescriptor< + RNGestureHandlerButtonWrapperShadowNode> { + using ConcreteComponentDescriptor::ConcreteComponentDescriptor; + void adopt(ShadowNode &shadowNode) const override { + react_native_assert( + dynamic_cast(&shadowNode)); + + ConcreteComponentDescriptor::adopt(shadowNode); + } +}; + +} // namespace facebook::react diff --git a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.cpp b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.cpp new file mode 100644 index 0000000000..613498576e --- /dev/null +++ b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.cpp @@ -0,0 +1,98 @@ + +#include + +#include "RNGestureHandlerButtonWrapperShadowNode.h" + +namespace facebook::react { + +extern const char RNGestureHandlerButtonWrapperComponentName[] = + "RNGestureHandlerButtonWrapper"; + +void RNGestureHandlerButtonWrapperShadowNode::initialize() { + // When the button wrapper is cloned and has a child node, the child node + // should be cloned as well to ensure it is mutable. + if (!getChildren().empty()) { + prepareChildren(); + } +} + +void RNGestureHandlerButtonWrapperShadowNode::prepareChildren() { + const auto &children = getChildren(); + react_native_assert( + children.size() == 1 && + "RNGestureHandlerButtonWrapper received more than one child"); + + const auto directChild = children[0]; + react_native_assert( + directChild->getChildren().size() == 1 && + "RNGestureHandlerButtonWrapper received more than one grandchild"); + + const auto clonedChild = directChild->clone({}); + + const auto childWithProtectedAccess = + std::static_pointer_cast( + clonedChild); + childWithProtectedAccess->traits_.unset(ShadowNodeTraits::ForceFlattenView); + + replaceChild(*directChild, clonedChild); + + const auto grandChild = clonedChild->getChildren()[0]; + const auto clonedGrandChild = grandChild->clone({}); + clonedChild->replaceChild(*grandChild, clonedGrandChild); +} + +void RNGestureHandlerButtonWrapperShadowNode::appendChild( + const std::shared_ptr &child) { + YogaLayoutableShadowNode::appendChild(child); + prepareChildren(); +} + +void RNGestureHandlerButtonWrapperShadowNode::layout( + LayoutContext layoutContext) { + react_native_assert(getChildren().size() == 1); + react_native_assert(getChildren()[0]->getChildren().size() == 1); + + auto child = std::static_pointer_cast( + getChildren()[0]); + auto grandChild = std::static_pointer_cast( + child->getChildren()[0]); + + auto gradChildWithProtectedAccess = + std::static_pointer_cast( + grandChild); + + auto shouldSkipCustomLayout = + !gradChildWithProtectedAccess->yogaNode_.getHasNewLayout(); + + YogaLayoutableShadowNode::layout(layoutContext); + + child->ensureUnsealed(); + grandChild->ensureUnsealed(); + + auto mutableChild = std::const_pointer_cast(child); + auto mutableGrandChild = + std::const_pointer_cast(grandChild); + + // The grand child node did not have its layout changed, we can reuse previous + // values + if (shouldSkipCustomLayout) { + react_native_assert(previousGrandChildLayoutMetrics_.has_value()); + mutableChild->setLayoutMetrics(previousGrandChildLayoutMetrics_.value()); + + auto metricsNoOrigin = previousGrandChildLayoutMetrics_.value(); + metricsNoOrigin.frame.origin = Point{}; + mutableGrandChild->setLayoutMetrics(metricsNoOrigin); + return; + } + + auto metrics = grandChild->getLayoutMetrics(); + previousGrandChildLayoutMetrics_ = metrics; + + mutableChild->setLayoutMetrics(metrics); + + auto metricsNoOrigin = grandChild->getLayoutMetrics(); + metricsNoOrigin.frame.origin = Point{}; + mutableGrandChild->setLayoutMetrics(metricsNoOrigin); +} + +} // namespace facebook::react diff --git a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.h b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.h new file mode 100644 index 0000000000..f04021c054 --- /dev/null +++ b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "RNGestureHandlerButtonWrapperState.h" + +namespace facebook::react { + +JSI_EXPORT extern const char RNGestureHandlerButtonWrapperComponentName[]; + +/* + * `ShadowNode` for component. + */ +class RNGestureHandlerButtonWrapperShadowNode final + : public ConcreteViewShadowNode< + RNGestureHandlerButtonWrapperComponentName, + RNGestureHandlerButtonWrapperProps, + RNGestureHandlerButtonWrapperEventEmitter, + RNGestureHandlerButtonWrapperState> { + public: + RNGestureHandlerButtonWrapperShadowNode( + const ShadowNodeFragment &fragment, + const ShadowNodeFamily::Shared &family, + ShadowNodeTraits traits) + : ConcreteViewShadowNode(fragment, family, traits) { + initialize(); + } + + RNGestureHandlerButtonWrapperShadowNode( + const ShadowNode &sourceShadowNode, + const ShadowNodeFragment &fragment) + : ConcreteViewShadowNode(sourceShadowNode, fragment) { + const auto &sourceWrapperNode = + static_cast( + sourceShadowNode); + previousGrandChildLayoutMetrics_ = + sourceWrapperNode.previousGrandChildLayoutMetrics_; + + initialize(); + } + + void layout(LayoutContext layoutContext) override; + void appendChild(const std::shared_ptr &child) override; + + private: + void initialize(); + void prepareChildren(); + + std::optional previousGrandChildLayoutMetrics_; +}; + +} // namespace facebook::react diff --git a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperState.h b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperState.h new file mode 100644 index 0000000000..7d9fa242e0 --- /dev/null +++ b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperState.h @@ -0,0 +1,32 @@ +/** + * This code was generated by + * [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be + * lost once the code is regenerated. + * + * @generated by codegen project: GenerateStateH.js + */ +#pragma once + +#ifdef ANDROID +#include +#endif + +namespace facebook::react { + +class RNGestureHandlerButtonWrapperState { + public: + RNGestureHandlerButtonWrapperState() = default; + +#ifdef ANDROID + RNGestureHandlerButtonWrapperState( + RNGestureHandlerButtonWrapperState const &previousState, + folly::dynamic data){}; + folly::dynamic getDynamic() const { + return {}; + }; +#endif +}; + +} // namespace facebook::react diff --git a/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx b/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx index 8f7ddcd1fb..6e6e268903 100644 --- a/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx @@ -1,5 +1,147 @@ -import { HostComponent } from 'react-native'; -import type { LegacyRawButtonProps } from './GestureButtonsProps'; +import { HostComponent, StyleSheet, View } from 'react-native'; +import type { RawButtonProps } from '../v3/components/GestureButtonsProps'; import RNGestureHandlerButtonNativeComponent from '../specs/RNGestureHandlerButtonNativeComponent'; +import RNGestureHandlerButtonWrapperNativeComponent from '../specs/RNGestureHandlerButtonWrapperNativeComponent'; +import { useMemo } from 'react'; -export default RNGestureHandlerButtonNativeComponent as HostComponent; +const ButtonComponent = + RNGestureHandlerButtonNativeComponent as HostComponent; + +export default function GestureHandlerButton({ + style, + ...rest +}: RawButtonProps) { + const flattenedStyle = useMemo(() => StyleSheet.flatten(style), [style]); + + const { + // Layout properties + display, + width, + height, + minWidth, + maxWidth, + minHeight, + maxHeight, + flex, + flexGrow, + flexShrink, + flexBasis, + flexDirection, + flexWrap, + justifyContent, + alignItems, + alignContent, + alignSelf, + aspectRatio, + gap, + rowGap, + columnGap, + margin, + marginTop, + marginBottom, + marginLeft, + marginRight, + marginVertical, + marginHorizontal, + marginStart, + marginEnd, + padding, + paddingTop, + paddingBottom, + paddingLeft, + paddingRight, + paddingVertical, + paddingHorizontal, + paddingStart, + paddingEnd, + position, + top, + right, + bottom, + left, + start, + end, + overflow, + + // Visual properties + ...restStyle + } = flattenedStyle; + + // Layout styles for ButtonComponent + const layoutStyle = useMemo( + () => ({ + display, + width, + height, + minWidth, + maxWidth, + minHeight, + maxHeight, + flex, + flexGrow, + flexShrink, + flexBasis, + flexDirection, + flexWrap, + justifyContent, + alignItems, + alignContent, + alignSelf, + aspectRatio, + gap, + rowGap, + columnGap, + margin, + marginTop, + marginBottom, + marginLeft, + marginRight, + marginVertical, + marginHorizontal, + marginStart, + marginEnd, + padding, + paddingTop, + paddingBottom, + paddingLeft, + paddingRight, + paddingVertical, + paddingHorizontal, + paddingStart, + paddingEnd, + position, + top, + right, + bottom, + left, + start, + end, + overflow, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [flattenedStyle] + ); + + return ( + + + + + + ); +} + +const styles = StyleSheet.create({ + contents: { + display: 'contents', + }, + overflowHidden: { + overflow: 'hidden', + }, +}); diff --git a/packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonWrapperNativeComponent.ts b/packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonWrapperNativeComponent.ts new file mode 100644 index 0000000000..d84e610aaa --- /dev/null +++ b/packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonWrapperNativeComponent.ts @@ -0,0 +1,11 @@ +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; +import type { ViewProps } from 'react-native'; + +interface NativeProps extends ViewProps {} + +export default codegenNativeComponent( + 'RNGestureHandlerButtonWrapper', + { + interfaceOnly: true, + } +); From 6825dbbd07bb73730a7c23ab140f96c00b93bb8d Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 27 Jan 2026 15:47:56 +0100 Subject: [PATCH 209/236] [iOS] Restore scroll view behavior on gesture unbind (#3931) ## Description The new architecture comes with a new fancy tool - view recycling. After a view is no longer used, instead of deallocating it, it's restored to a "pristine" state and moved to a recycling pool. The restoration covers only the properties that may have been modified by React in the default implementation, and `delaysContentTouches` is not one of them. By setting it to `YES` on a ScrollView and never resetting it back, we may cause an unrelated scroll view to appear with that field set. This PR resets the field when the gesture unbinds from view. ## Test plan Remove native gesture from the scroll view, verify that the touches are no longer delayed. --- .../apple/Handlers/RNNativeViewHandler.mm | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index 5f0852349e..5532e06020 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -159,6 +159,15 @@ - (void)bindToView:(UIView *)view scrollView.delaysContentTouches = YES; } +- (void)unbindFromView +{ + // Restore the React Native's overriden behavor for not delaying content touches + UIScrollView *scrollView = [self retrieveScrollView:self.recognizer.view]; + scrollView.delaysContentTouches = NO; + + [super unbindFromView]; +} + - (void)handleTouchDown:(UIView *)sender forEvent:(UIEvent *)event { [self setCurrentPointerType:event]; From e1185cd20178521beaa8dfebea63b3585f5c387b Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 27 Jan 2026 16:34:52 +0100 Subject: [PATCH 210/236] [General] Fix `onBegin` not being called when the native recognizer skips the `BEGAN` state (#3932) ## Description The current state machine implementation allows for `onBegin` callback to be skipped if the native recognizer goes from `UNDETERMINED` directly to `ACTIVE` state (which is a valid transition in some cases). This PR updates the logic to "backfill" the `onBegin` callback when receiving the `UNDETERMINED -> ACTIVE` transition, before calling `onActivate`. Note: this PR only changes the behavior for the V3 API. I'm not sure whether it should be backported to V2 as it would likely be a breaking change. ## Test plan This scenario: https://github.com/software-mansion/react-native-gesture-handler/blob/59a5311e3cf517e9147017f20aa41bc644790a05/packages/react-native-gesture-handler/src/components/GestureButtons.tsx#L67-L68 --- .../src/v3/hooks/callbacks/stateChangeHandler.ts | 5 +++++ 1 file changed, 5 insertions(+) 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 d5d93ed785..0b671dfd53 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 @@ -38,6 +38,11 @@ export function getStateChangeHandler( (oldState === State.BEGAN || oldState === State.UNDETERMINED) && state === State.ACTIVE ) { + // If the native recognizer skipped the BEGAN state, we still need to call the callback + if (oldState === State.UNDETERMINED) { + runCallback(CALLBACK_TYPE.BEGAN, callbacks, event); + } + runCallback(CALLBACK_TYPE.START, callbacks, event); } else if (oldState !== state && state === State.END) { if (oldState === State.ACTIVE) { From c48d6534c27b3e14721668abfc9ea094cd8bba7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= <63123542+m-bert@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:59:14 +0100 Subject: [PATCH 211/236] [docs] Update quick start (#3895) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This PR updates quick start guide in our docs. ## Test plan Read docs (and follow the implementation 🤭) 🤓 --- .../docs/guides/quickstart/_steps/step1.md | 10 ++ .../docs/guides/quickstart/_steps/step2.md | 29 ++++-- .../docs/guides/quickstart/_steps/step3.md | 32 ++----- .../docs/guides/quickstart/_steps/step4.md | 29 ++++-- .../docs/guides/quickstart/_steps/step5.md | 39 ++------ .../docs/guides/quickstart/index.md | 91 +++++++++++++++---- packages/docs-gesture-handler/package.json | 3 +- 7 files changed, 142 insertions(+), 91 deletions(-) diff --git a/packages/docs-gesture-handler/docs/guides/quickstart/_steps/step1.md b/packages/docs-gesture-handler/docs/guides/quickstart/_steps/step1.md index 6e5c8de76b..18996cbca9 100644 --- a/packages/docs-gesture-handler/docs/guides/quickstart/_steps/step1.md +++ b/packages/docs-gesture-handler/docs/guides/quickstart/_steps/step1.md @@ -1,5 +1,15 @@ ```jsx import { StyleSheet } from 'react-native'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import Animated from 'react-native-reanimated'; + +export default function Ball() { + return ( + + + + ); +} const styles = StyleSheet.create({ ball: { diff --git a/packages/docs-gesture-handler/docs/guides/quickstart/_steps/step2.md b/packages/docs-gesture-handler/docs/guides/quickstart/_steps/step2.md index 7292504d92..08f4e2f9db 100644 --- a/packages/docs-gesture-handler/docs/guides/quickstart/_steps/step2.md +++ b/packages/docs-gesture-handler/docs/guides/quickstart/_steps/step2.md @@ -1,12 +1,25 @@ ```jsx -import { GestureDetector } from 'react-native-gesture-handler'; -import Animated from 'react-native-reanimated'; +import { + useAnimatedStyle, + useSharedValue, + withSpring, +} from 'react-native-reanimated'; -function Ball() { - return ( - - - - ); +export default function Ball() { + const isPressed = useSharedValue(false); + const offset = useSharedValue({ x: 0, y: 0 }); + + const animatedStyles = useAnimatedStyle(() => { + return { + transform: [ + { translateX: offset.value.x }, + { translateY: offset.value.y }, + { scale: withSpring(isPressed.value ? 1.2 : 1) }, + ], + backgroundColor: isPressed.value ? 'yellow' : 'blue', + }; + }); + + // ... } ``` diff --git a/packages/docs-gesture-handler/docs/guides/quickstart/_steps/step3.md b/packages/docs-gesture-handler/docs/guides/quickstart/_steps/step3.md index ac49f59bd9..f75641e27f 100644 --- a/packages/docs-gesture-handler/docs/guides/quickstart/_steps/step3.md +++ b/packages/docs-gesture-handler/docs/guides/quickstart/_steps/step3.md @@ -1,25 +1,9 @@ -```jsx -import { - useSharedValue, - useAnimatedStyle, - withSpring, -} from 'react-native-reanimated'; - -function Ball() { - const isPressed = useSharedValue(false); - const offset = useSharedValue({ x: 0, y: 0 }); - - const animatedStyles = useAnimatedStyle(() => { - return { - transform: [ - { translateX: offset.value.x }, - { translateY: offset.value.y }, - { scale: withSpring(isPressed.value ? 1.2 : 1) }, - ], - backgroundColor: isPressed.value ? 'yellow' : 'blue', - }; - }); - - // ... -} +```jsx {4} +// ... +return ( + + + +); +// ... ``` diff --git a/packages/docs-gesture-handler/docs/guides/quickstart/_steps/step4.md b/packages/docs-gesture-handler/docs/guides/quickstart/_steps/step4.md index 7750e97e3b..a7260cb8bb 100644 --- a/packages/docs-gesture-handler/docs/guides/quickstart/_steps/step4.md +++ b/packages/docs-gesture-handler/docs/guides/quickstart/_steps/step4.md @@ -1,9 +1,22 @@ -```jsx {4} -// ... -return ( - - - -); -// ... +```jsx +import { usePanGesture } from 'react-native-gesture-handler'; + +function Ball() { + // ... + const gesture = usePanGesture({ + onBegin: () => { + isPressed.value = true; + }, + onUpdate: (e) => { + offset.value = { + x: offset.value.x + e.changeX, + y: offset.value.y + e.changeY, + }; + }, + onFinalize: () => { + isPressed.value = false; + }, + }); + // ... +} ``` diff --git a/packages/docs-gesture-handler/docs/guides/quickstart/_steps/step5.md b/packages/docs-gesture-handler/docs/guides/quickstart/_steps/step5.md index a2ab8eb776..75107330d2 100644 --- a/packages/docs-gesture-handler/docs/guides/quickstart/_steps/step5.md +++ b/packages/docs-gesture-handler/docs/guides/quickstart/_steps/step5.md @@ -1,38 +1,11 @@ -```jsx -import { Gesture } from 'react-native-gesture-handler'; - -function Ball() { - // ... - const start = useSharedValue({ x: 0, y: 0 }); - const gesture = Gesture.Pan() - .onBegin(() => { - isPressed.value = true; - }) - .onUpdate((e) => { - offset.value = { - x: e.translationX + start.value.x, - y: e.translationY + start.value.y, - }; - }) - .onEnd(() => { - start.value = { - x: offset.value.x, - y: offset.value.y, - }; - }) - .onFinalize(() => { - isPressed.value = false; - }); - // ... -} -``` - -```jsx {3} +```jsx {4} // ... return ( - - - + + + + + ); // ... ``` diff --git a/packages/docs-gesture-handler/docs/guides/quickstart/index.md b/packages/docs-gesture-handler/docs/guides/quickstart/index.md index 1724181bda..cbfa9bbbd2 100644 --- a/packages/docs-gesture-handler/docs/guides/quickstart/index.md +++ b/packages/docs-gesture-handler/docs/guides/quickstart/index.md @@ -12,45 +12,102 @@ import Step3 from './\_steps/step3.md'; import Step4 from './\_steps/step4.md'; import Step5 from './\_steps/step5.md'; -RNGH2 provides much simpler way to add gestures to your app. All you need to do is wrap the view that you want your gesture to work on with [`GestureDetector`](/docs/fundamentals/gesture-detectors#gesture-detector), define the gesture and pass it to detector. That's all! +RNGH3 offers a straightforward way to add gestures to your app. Simply wrap your target view with the [GestureDetector](/docs/fundamentals/gesture-detectors#gesture-detector) component, define your gesture, and pass it in. That’s it! -To demonstrate how you would use the new API, let's make a simple app where you can drag a ball around. You will need to add `react-native-gesture-handler` (for gestures) and `react-native-reanimated` (for animations) modules. +To see the new API in action, let's build a simple app where you can drag a ball around the screen. To follow along, you'll need both `react-native-gesture-handler` (to handle gestures) and [`react-native-reanimated`](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/) (to handle the animations). -
First let's define styles we will need to make the app:
+
Start by defining the basic structure of the application:
-
Then we can start writing our Ball component:
+
+ Next, define the `SharedValues` to track the ball's position and create the animated styles required to position the ball on the screen: +
-
- We also need to define{' '} - - shared values - {' '} - to keep track of the ball position and create animated styles in order to be - able to position the ball on the screen: -
+
Apply the animated styles to the ball component:
-
And add it to the ball's styles:
+
+ Now, define the Pan gesture logic. +
- The only thing left is to define the pan gesture and assign it to the - detector: + Finally, wrap the component responsible for rendering the ball with a GestureDetector, and attach the Pan gesture to it:
-Note the `start` shared value. We need it to store the position of the ball at the moment we grab it to be able to correctly position it later, because we only have access to translation relative to the starting point of the gesture. +The complete implementation is shown below: + +```jsx +import { StyleSheet } from 'react-native'; +import { + GestureDetector, + GestureHandlerRootView, + usePanGesture, +} from 'react-native-gesture-handler'; +import Animated, { + useAnimatedStyle, + useSharedValue, + withSpring, +} from 'react-native-reanimated'; + +export default function Ball() { + const isPressed = useSharedValue(false); + const offset = useSharedValue({ x: 0, y: 0 }); + + const gesture = usePanGesture({ + onBegin: () => { + isPressed.value = true; + }, + onUpdate: (e) => { + offset.value = { + x: offset.value.x + e.changeX, + y: offset.value.y + e.changeY, + }; + }, + onFinalize: () => { + isPressed.value = false; + }, + }); + + const animatedStyles = useAnimatedStyle(() => { + return { + transform: [ + { translateX: offset.value.x }, + { translateY: offset.value.y }, + { scale: withSpring(isPressed.value ? 1.2 : 1) }, + ], + backgroundColor: isPressed.value ? 'yellow' : 'blue', + }; + }); + + return ( + + + + + + ); +} -Now you can just add `Ball` component to some view in the app and see the results! (Or you can just check the code [here](https://github.com/software-mansion/react-native-gesture-handler/blob/main/example/src/new_api/velocityTest/index.tsx) and see it in action in the Example app.) +const styles = StyleSheet.create({ + ball: { + width: 100, + height: 100, + borderRadius: 100, + backgroundColor: 'blue', + alignSelf: 'center', + }, +}); +``` diff --git a/packages/docs-gesture-handler/package.json b/packages/docs-gesture-handler/package.json index 3a16be6aca..3492dc5182 100644 --- a/packages/docs-gesture-handler/package.json +++ b/packages/docs-gesture-handler/package.json @@ -25,17 +25,18 @@ "@docusaurus/core": "3.7.0", "@docusaurus/plugin-debug": "3.7.0", "@docusaurus/preset-classic": "3.7.0", + "@docusaurus/theme-common": "3.7.0", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@mdx-js/react": "^3.0.0", "@mui/material": "^7.1.0", "@swmansion/t-rex-ui": "1.0.0", "@vercel/og": "^0.6.2", - "prism-react-renderer": "^2.1.0", "babel-polyfill": "^6.26.0", "babel-preset-expo": "^9.2.2", "babel-preset-react-native": "^4.0.1", "clsx": "^2.1.0", + "prism-react-renderer": "^2.1.0", "raf": "^3.4.1", "raw-loader": "^4.0.2", "react": "^18.2.0", From 0c130e33c2804e9a5b9695f48d46c53655d573df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= <63123542+m-bert@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:28:24 +0100 Subject: [PATCH 212/236] Move `Reanimated` availability check to `JS` side (#3935) ## Description Currently if `Reanimated` is not installed, running apps on `iOS` results in redbox that says that it `Reanimated` module doesn't exist. It happens because in [this line](https://github.com/software-mansion/react-native-gesture-handler/blob/6825dbbd07bb73730a7c23ab140f96c00b93bb8d/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm#L125) in module we call `[self.moduleRegistry moduleForName:"ReanimatedModule"]`. This goes through [RCTModuleRegistry](https://github.com/facebook/react-native/blob/ca520ee4c19dd921f9225c71a9ad872c4bbd7cb9/packages/react-native/React/Base/RCTModuleRegistry.m#L50) to [RCTTurboModuleManager](https://github.com/facebook/react-native/blob/ca520ee4c19dd921f9225c71a9ad872c4bbd7cb9/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm#L980), where this method is forced to throw redbox if module is not found. To fix this, we move check for `Reanimated` availability to JS side. ## Test plan Tested on basic-example app with and without `Reanimated` --- .../com/swmansion/gesturehandler/ReanimatedProxy.kt | 4 ---- .../com/swmansion/gesturehandler/ReanimatedProxy.kt | 4 ---- .../gesturehandler/react/RNGestureHandlerModule.kt | 9 +++++++-- .../apple/RNGestureHandlerModule.mm | 10 +++++----- .../src/RNGestureHandlerModule.web.ts | 3 +++ .../src/handlers/gestures/reanimatedWrapper.ts | 3 +++ .../react-native-gesture-handler/src/mocks/mocks.tsx | 2 ++ .../src/specs/NativeRNGestureHandlerModule.ts | 1 + .../react-native-gesture-handler/src/v3/NativeProxy.ts | 3 +++ .../src/v3/NativeProxy.web.ts | 3 +++ 10 files changed, 27 insertions(+), 15 deletions(-) diff --git a/packages/react-native-gesture-handler/android/noreanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedProxy.kt b/packages/react-native-gesture-handler/android/noreanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedProxy.kt index e98bbb390f..1168f68ab2 100644 --- a/packages/react-native-gesture-handler/android/noreanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedProxy.kt +++ b/packages/react-native-gesture-handler/android/noreanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedProxy.kt @@ -9,8 +9,4 @@ class ReanimatedProxy { fun > sendEvent(event: T, reactApplicationContext: ReactContext) { // no-op } - - companion object { - const val REANIMATED_INSTALLED = false - } } diff --git a/packages/react-native-gesture-handler/android/reanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedProxy.kt b/packages/react-native-gesture-handler/android/reanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedProxy.kt index c11fea2a9b..f567ff64c5 100644 --- a/packages/react-native-gesture-handler/android/reanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedProxy.kt +++ b/packages/react-native-gesture-handler/android/reanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedProxy.kt @@ -14,8 +14,4 @@ class ReanimatedProxy { reanimatedModule?.nodesManager?.onEventDispatch(event) } - - companion object { - const val REANIMATED_INSTALLED = true - } } 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 a5819f3c84..09b02ad1f1 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 @@ -12,7 +12,6 @@ import com.facebook.react.turbomodule.core.interfaces.BindingsInstallerHolder import com.facebook.react.turbomodule.core.interfaces.TurboModuleWithJSIBindings import com.facebook.soloader.SoLoader import com.swmansion.gesturehandler.NativeRNGestureHandlerModuleSpec -import com.swmansion.gesturehandler.ReanimatedProxy import com.swmansion.gesturehandler.core.GestureHandler import com.swmansion.gesturehandler.react.events.RNGestureHandlerEventDispatcher @@ -29,6 +28,7 @@ class RNGestureHandlerModule(reactContext: ReactApplicationContext?) : @DoNotStrip @Suppress("unused") private var mHybridData: HybridData = initHybrid() + private var isReanimatedAvailable = false private var uiRuntimeDecorated = false private val registry: RNGestureHandlerRegistry get() = registries[moduleId]!! @@ -62,7 +62,7 @@ class RNGestureHandlerModule(reactContext: ReactApplicationContext?) : @ReactMethod override fun createGestureHandler(handlerName: String, handlerTagDouble: Double, config: ReadableMap): Boolean { - if (ReanimatedProxy.REANIMATED_INSTALLED && !uiRuntimeDecorated) { + if (isReanimatedAvailable && !uiRuntimeDecorated) { uiRuntimeDecorated = decorateUIRuntime() } @@ -124,6 +124,11 @@ class RNGestureHandlerModule(reactContext: ReactApplicationContext?) : @ReactMethod override fun flushOperations() = Unit + @ReactMethod + override fun setReanimatedAvailable(isAvailable: Boolean) { + isReanimatedAvailable = isAvailable + } + @DoNotStrip @Suppress("unused") fun setGestureHandlerState(handlerTag: Int, newState: Int) { diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm index 604551274a..fda2921666 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm @@ -42,7 +42,6 @@ @implementation RNGestureHandlerModule { jsi::Runtime *_rnRuntime; int _moduleId; - bool _checkedIfReanimatedIsAvailable; bool _isReanimatedAvailable; bool _uiRuntimeDecorated; } @@ -121,10 +120,6 @@ - (bool)installUIRuntimeBindings - (NSNumber *)createGestureHandler:(NSString *)handlerName handlerTag:(double)handlerTag config:(NSDictionary *)config { - if (!_checkedIfReanimatedIsAvailable) { - _isReanimatedAvailable = [self.moduleRegistry moduleForName:"ReanimatedModule"] != nil; - } - if (_isReanimatedAvailable && !_uiRuntimeDecorated) { _uiRuntimeDecorated = [self installUIRuntimeBindings]; } @@ -191,6 +186,11 @@ - (void)flushOperations }]; } +- (void)setReanimatedAvailable:(BOOL)isAvailable +{ + _isReanimatedAvailable = isAvailable; +} + - (void)setGestureState:(int)state forHandler:(int)handlerTag { if (RCTIsMainQueue()) { diff --git a/packages/react-native-gesture-handler/src/RNGestureHandlerModule.web.ts b/packages/react-native-gesture-handler/src/RNGestureHandlerModule.web.ts index 1baf8dd9e7..c30bfd7729 100644 --- a/packages/react-native-gesture-handler/src/RNGestureHandlerModule.web.ts +++ b/packages/react-native-gesture-handler/src/RNGestureHandlerModule.web.ts @@ -89,4 +89,7 @@ export default { }, // eslint-disable-next-line @typescript-eslint/no-empty-function flushOperations() {}, + setReanimatedAvailable(_isAvailable: boolean) { + // No-op on web + }, }; 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 9044a8a3df..afef43867e 100644 --- a/packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper.ts +++ b/packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper.ts @@ -5,6 +5,7 @@ import { GestureUpdateEventWithHandlerData, SharedValue, } from '../../v3/types'; +import { NativeProxy } from '../../v3/NativeProxy'; export type ReanimatedContext = { lastUpdateEvent: GestureUpdateEventWithHandlerData | undefined; @@ -80,6 +81,7 @@ let Reanimated: try { Reanimated = require('react-native-reanimated'); + NativeProxy.setReanimatedAvailable(true); } catch (e) { // When 'react-native-reanimated' is not available we want to quietly continue // @ts-ignore TS demands the variable to be initialized @@ -90,6 +92,7 @@ if (!Reanimated?.useSharedValue) { // @ts-ignore Make sure the loaded module is actually Reanimated, if it's not // reset the module to undefined so we can fallback to the default implementation Reanimated = undefined; + NativeProxy.setReanimatedAvailable(false); } if (Reanimated !== undefined && !Reanimated.setGestureState) { diff --git a/packages/react-native-gesture-handler/src/mocks/mocks.tsx b/packages/react-native-gesture-handler/src/mocks/mocks.tsx index 03d42f34fc..e04a070782 100644 --- a/packages/react-native-gesture-handler/src/mocks/mocks.tsx +++ b/packages/react-native-gesture-handler/src/mocks/mocks.tsx @@ -25,6 +25,7 @@ const setGestureHandlerConfig = NOOP; const updateGestureHandlerConfig = NOOP; const flushOperations = NOOP; const configureRelations = NOOP; +const setReanimatedAvailable = NOOP; const install = NOOP; const NativeViewGestureHandler = View; const TapGestureHandler = View; @@ -66,6 +67,7 @@ export default { setGestureHandlerConfig, updateGestureHandlerConfig, configureRelations, + setReanimatedAvailable, flushOperations, install, // Probably can be removed diff --git a/packages/react-native-gesture-handler/src/specs/NativeRNGestureHandlerModule.ts b/packages/react-native-gesture-handler/src/specs/NativeRNGestureHandlerModule.ts index c1dcd6a4e7..c40798482b 100644 --- a/packages/react-native-gesture-handler/src/specs/NativeRNGestureHandlerModule.ts +++ b/packages/react-native-gesture-handler/src/specs/NativeRNGestureHandlerModule.ts @@ -24,6 +24,7 @@ export interface Spec extends TurboModule { configureRelations: (handlerTag: Double, relations: Object) => void; dropGestureHandler: (handlerTag: Double) => void; flushOperations: () => void; + setReanimatedAvailable: (isAvailable: boolean) => void; } export default TurboModuleRegistry.getEnforcing('RNGestureHandlerModule'); diff --git a/packages/react-native-gesture-handler/src/v3/NativeProxy.ts b/packages/react-native-gesture-handler/src/v3/NativeProxy.ts index 14431b433b..c115060b28 100644 --- a/packages/react-native-gesture-handler/src/v3/NativeProxy.ts +++ b/packages/react-native-gesture-handler/src/v3/NativeProxy.ts @@ -51,4 +51,7 @@ export const NativeProxy = { RNGestureHandlerModule.configureRelations(handlerTag, relations); }); }, + setReanimatedAvailable: (isAvailable: boolean) => { + RNGestureHandlerModule.setReanimatedAvailable(isAvailable); + }, } as const; diff --git a/packages/react-native-gesture-handler/src/v3/NativeProxy.web.ts b/packages/react-native-gesture-handler/src/v3/NativeProxy.web.ts index abafb2ed5d..ef81140e62 100644 --- a/packages/react-native-gesture-handler/src/v3/NativeProxy.web.ts +++ b/packages/react-native-gesture-handler/src/v3/NativeProxy.web.ts @@ -35,4 +35,7 @@ export const NativeProxy = { configureRelations: (handlerTag: number, relations: GestureRelations) => { RNGestureHandlerModule.configureRelations(handlerTag, relations); }, + setReanimatedAvailable: (isAvailable: boolean) => { + RNGestureHandlerModule.setReanimatedAvailable(isAvailable); + }, } as const; From 2bdd543fc3baa1617dd3f39d769327e50ece0a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= <81448793+akwasniewski@users.noreply.github.com> Date: Fri, 30 Jan 2026 08:17:22 +0100 Subject: [PATCH 213/236] [web] fix context menu (#3939) ## Description Context menu was broken on v3, this PR fixes it. WebDelegate never handled changing contextMenu. when context had been disabled the `areContextMenuListenersAdded` was set to true, later when it was enabled it would not change the listener from disabled to enabled as the `addContextMenuListeners` returned after seeing that the listeners had already been added. ## Test plan Tested on the following example:
```tsx import React from 'react'; import { StyleSheet, View } from 'react-native'; import { GestureDetector, MouseButton, usePanGesture, } from 'react-native-gesture-handler'; export default function ContextMenuExample() { const p1 = usePanGesture({ mouseButton: MouseButton.RIGHT }); const p2 = usePanGesture({}); const p3 = usePanGesture({}); return ( ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'space-around', alignItems: 'center', }, grandParent: { width: 300, height: 300, backgroundColor: "navy", }, parent: { width: 200, height: 200, backgroundColor: "purple", }, child: { width: 100, height: 100, backgroundColor: "blue", }, box: { display: 'flex', justifyContent: 'space-around', alignItems: 'center', borderRadius: 20, }, }); ```
--- .../web/tools/GestureHandlerWebDelegate.ts | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/react-native-gesture-handler/src/web/tools/GestureHandlerWebDelegate.ts b/packages/react-native-gesture-handler/src/web/tools/GestureHandlerWebDelegate.ts index 2d2a8dfa28..ca4b3b01f9 100644 --- a/packages/react-native-gesture-handler/src/web/tools/GestureHandlerWebDelegate.ts +++ b/packages/react-native-gesture-handler/src/web/tools/GestureHandlerWebDelegate.ts @@ -33,6 +33,7 @@ export class GestureHandlerWebDelegate }; private areContextMenuListenersAdded = false; + private wasContextMenuEnabled = false; init(viewRef: number, handler: IGestureHandler): void { if (!viewRef) { @@ -158,9 +159,11 @@ export class GestureHandlerWebDelegate } if (this.shouldDisableContextMenu()) { + this.wasContextMenuEnabled = false; this.view.addEventListener('contextmenu', this.disableContextMenu); this.areContextMenuListenersAdded = true; } else if (this.gestureHandler.enableContextMenu) { + this.wasContextMenuEnabled = true; this.view.addEventListener('contextmenu', this.enableContextMenu); this.areContextMenuListenersAdded = true; } @@ -173,10 +176,14 @@ export class GestureHandlerWebDelegate this.ensureView(this.view); - if (this.shouldDisableContextMenu()) { + if (!this.areContextMenuListenersAdded) { + return; + } + + if (!this.wasContextMenuEnabled) { this.view.removeEventListener('contextmenu', this.disableContextMenu); this.areContextMenuListenersAdded = false; - } else if (this.gestureHandler.enableContextMenu) { + } else { this.view.removeEventListener('contextmenu', this.enableContextMenu); this.areContextMenuListenersAdded = false; } @@ -220,11 +227,16 @@ export class GestureHandlerWebDelegate } private setContextMenu() { - if (this.gestureHandler.enabled) { - this.addContextMenuListeners(); - } else { + if (!this.gestureHandler.enabled) { + this.removeContextMenuListeners(); + return; + } + + if (!this.wasContextMenuEnabled) { this.removeContextMenuListeners(); } + + this.addContextMenuListeners(); } onEnabledChange(): void { From d519ea3d3bdb3d6921aaa0a0cd7fa859cc2f8306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= <81448793+akwasniewski@users.noreply.github.com> Date: Fri, 30 Jan 2026 08:31:38 +0100 Subject: [PATCH 214/236] Fix state manager on unregistered gestures (#3913) ## Description The new `StateManager` is global and given `handlerTag` it can manually set the states of an arbitrary gesture. This causes errors, when the gesture, which state is being set, has not been yet recorded in the orchestrator. Recording gestures in the orchestrator on android is done lazily, thus if it never received touches it is not recorded. It also adds explicit error when trying to manually handled a gesture not attached to any detector on all platforms. ## Test plan Tested on the following example
```ts import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { GestureHandlerRootView, GestureDetector, useLongPressGesture, GestureStateManager, usePanGesture, useSimultaneousGestures, useTapGesture } from 'react-native-gesture-handler'; export default function TwoPressables() { const longPress = useLongPressGesture({ onTouchesDown: (e) => { 'worklet'; console.log("touches down") }, onActivate: () => { 'worklet'; console.log("long pressed") }, minDuration: 100000000, disableReanimated: true }) const pan = useTapGesture({ onTouchesDown: () => { 'worklet'; console.log("tap") GestureStateManager.activate(longPress.tag) }, disableReanimated: true }); return ( Long Press Pan ); } const styles = StyleSheet.create({ root: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#f7f7f7', }, outer: { padding: 20, backgroundColor: '#ddd', borderRadius: 12, marginBottom: 50 }, label: { fontSize: 18, marginBottom: 10, }, }) ```
--- .../gesturehandler/core/GestureHandler.kt | 10 +++++++ .../core/GestureHandlerOrchestrator.kt | 2 +- .../RNGestureHandlerButtonViewManager.kt | 19 ++----------- .../react/RNGestureHandlerDetectorView.kt | 5 ++++ .../react/RNGestureHandlerRootHelper.kt | 4 +++ .../react/RNGestureHandlerRootView.kt | 20 +++++++++++++ .../apple/RNGestureHandlerModule.mm | 6 ++++ .../src/v3/gestureStateManager.web.ts | 28 +++++++++++++++++-- .../src/web/handlers/GestureHandler.ts | 10 +++++++ .../src/web/handlers/IGestureHandler.ts | 1 + 10 files changed, 84 insertions(+), 21 deletions(-) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt index cdfd8d1416..30ba4fcf7e 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt @@ -595,6 +595,16 @@ open class GestureHandler { // generated faster than they can be treated by JS thread eventCoalescingKey = nextEventCoalescingKey++ } + + check(hostDetectorView != null || orchestrator != null) { + "Manually handled gesture had not been assigned to any detector" + } + + if (orchestrator == null) { + // If the state is set manually, the handler may not have been fully recorded by the orchestrator. + hostDetectorView?.recordHandlerIfNotPresent(this) + } + orchestrator!!.onHandlerStateChange(this, newState, oldState) onStateChange(newState, oldState) } diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt index 3f13f15b82..5907cd6ac1 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt @@ -442,7 +442,7 @@ class GestureHandlerOrchestrator( } } - private fun recordHandlerIfNotPresent(handler: GestureHandler, view: View) { + fun recordHandlerIfNotPresent(handler: GestureHandler, view: View?) { if (gestureHandlers.contains(handler)) { return } diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt index 32cb973b80..c4ad8da6e1 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt @@ -21,7 +21,6 @@ import android.view.MotionEvent import android.view.View import android.view.View.OnClickListener import android.view.ViewGroup -import android.view.ViewParent import android.view.accessibility.AccessibilityNodeInfo import androidx.core.view.children import com.facebook.react.R @@ -497,9 +496,9 @@ class RNGestureHandlerButtonViewManager : // a parent button from playing) return if (!isChildTouched()) { if (context.isScreenReaderOn()) { - findGestureHandlerRootView()?.activateNativeHandlers(this) + RNGestureHandlerRootView.findGestureHandlerRootView(this)?.activateNativeHandlers(this) } else if (receivedKeyEvent) { - findGestureHandlerRootView()?.activateNativeHandlers(this) + RNGestureHandlerRootView.findGestureHandlerRootView(this)?.activateNativeHandlers(this) receivedKeyEvent = false } @@ -538,20 +537,6 @@ class RNGestureHandlerButtonViewManager : // by default Viewgroup would pass hotspot change events } - private fun findGestureHandlerRootView(): RNGestureHandlerRootView? { - var parent: ViewParent? = this.parent - var gestureHandlerRootView: RNGestureHandlerRootView? = null - - while (parent != null) { - if (parent is RNGestureHandlerRootView) { - gestureHandlerRootView = parent - } - parent = parent.parent - } - - return gestureHandlerRootView - } - companion object { var resolveOutValue = TypedValue() var touchResponder: ButtonViewGroup? = null diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorView.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorView.kt index a91811c227..6e0bb86b5b 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorView.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorView.kt @@ -11,6 +11,7 @@ import com.facebook.react.uimanager.events.Event import com.facebook.react.views.swiperefresh.ReactSwipeRefreshLayout import com.facebook.react.views.view.ReactViewGroup import com.swmansion.gesturehandler.core.GestureHandler +import com.swmansion.gesturehandler.react.RNGestureHandlerRootView class RNGestureHandlerDetectorView(context: Context) : ReactViewGroup(context) { private val reactContext: ThemedReactContext @@ -208,6 +209,10 @@ class RNGestureHandlerDetectorView(context: Context) : ReactViewGroup(context) { } } + fun recordHandlerIfNotPresent(handler: GestureHandler) { + RNGestureHandlerRootView.findGestureHandlerRootView(this)?.recordHandlerIfNotPresent(handler) + } + private fun ReadableArray.mapVirtualChildren(): List = List(size()) { i -> val child = getMap(i) ?: return@List null val handlerTags = child.getArray("handlerTags")?.toIntList().orEmpty() diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt index 131119ecb7..fb327172be 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt @@ -141,6 +141,10 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView: orchestrator?.activateNativeHandlersForView(view) } + fun recordHandlerIfNotPresent(handler: GestureHandler) { + orchestrator?.recordHandlerIfNotPresent(handler, null) + } + companion object { private const val MIN_ALPHA_FOR_TOUCH = 0.1f private fun findRootViewTag(viewGroup: ViewGroup): ViewGroup { diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt index f66bcd8387..79167e9c27 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt @@ -5,11 +5,13 @@ import android.util.Log import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import android.view.ViewParent import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.common.ReactConstants import com.facebook.react.uimanager.RootView import com.facebook.react.views.view.ReactViewGroup +import com.swmansion.gesturehandler.core.GestureHandler class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) { private var moduleId: Int = -1 @@ -39,6 +41,10 @@ class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) { rootHelper?.tearDown() } + fun recordHandlerIfNotPresent(handler: GestureHandler) { + rootHelper?.recordHandlerIfNotPresent(handler) + } + override fun dispatchTouchEvent(event: MotionEvent) = if (rootViewEnabled && rootHelper!!.dispatchTouchEvent(event)) { true } else { @@ -87,5 +93,19 @@ class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) { } return false } + + fun findGestureHandlerRootView(viewGroup: ViewGroup): RNGestureHandlerRootView? { + var parent: ViewParent? = viewGroup.parent + var gestureHandlerRootView: RNGestureHandlerRootView? = null + + while (parent != null) { + if (parent is RNGestureHandlerRootView) { + gestureHandlerRootView = parent + } + parent = parent.parent + } + + return gestureHandlerRootView + } } } diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm index fda2921666..0f224571f0 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm @@ -208,6 +208,12 @@ - (void)setGestureStateSync:(int)state forHandler:(int)handlerTag RNGestureHandlerManager *manager = [RNGestureHandlerModule handlerManagerForModuleId:_moduleId]; RNGestureHandler *handler = [manager handlerWithTag:@(handlerTag)]; + if (handler.hostDetectorView == nil && [handler usesNativeOrVirtualDetector]) { + @throw [NSException exceptionWithName:@"HandlerNotAttached" + reason:@"Manually handled gesture had not been assigned to any detector" + userInfo:nil]; + } + if (handler != nil) { if (state == 1) { // FAILED handler.recognizer.state = RNGHGestureRecognizerStateFailed; diff --git a/packages/react-native-gesture-handler/src/v3/gestureStateManager.web.ts b/packages/react-native-gesture-handler/src/v3/gestureStateManager.web.ts index b2f4ff6f4a..8705d72489 100644 --- a/packages/react-native-gesture-handler/src/v3/gestureStateManager.web.ts +++ b/packages/react-native-gesture-handler/src/v3/gestureStateManager.web.ts @@ -1,16 +1,32 @@ import { State } from '../State'; +import { tagMessage } from '../utils'; +import IGestureHandler from '../web/handlers/IGestureHandler'; import NodeManager from '../web/tools/NodeManager'; import { GestureStateManagerType } from './gestureStateManager'; +function ensureHandlerAttached(handler: IGestureHandler) { + if (!handler.attached) { + throw new Error( + tagMessage( + 'Manually handled gesture had not been assigned to any detector' + ) + ); + } +} + export const GestureStateManager: GestureStateManagerType = { begin(handlerTag: number): void { 'worklet'; - NodeManager.getHandler(handlerTag).begin(); + const handler = NodeManager.getHandler(handlerTag); + ensureHandlerAttached(handler); + + handler.begin(); }, activate(handlerTag: number): void { 'worklet'; const handler = NodeManager.getHandler(handlerTag); + ensureHandlerAttached(handler); // Force going from UNDETERMINED to ACTIVE through BEGAN to preserve // the correct state transition flow. if (handler.state === State.UNDETERMINED) { @@ -22,11 +38,17 @@ export const GestureStateManager: GestureStateManagerType = { fail(handlerTag: number): void { 'worklet'; - NodeManager.getHandler(handlerTag).fail(); + const handler = NodeManager.getHandler(handlerTag); + ensureHandlerAttached(handler); + + handler.fail(); }, deactivate(handlerTag: number): void { 'worklet'; - NodeManager.getHandler(handlerTag).end(); + const handler = NodeManager.getHandler(handlerTag); + ensureHandlerAttached(handler); + + handler.end(); }, }; diff --git a/packages/react-native-gesture-handler/src/web/handlers/GestureHandler.ts b/packages/react-native-gesture-handler/src/web/handlers/GestureHandler.ts index efd00bc9d2..99dab55c2f 100644 --- a/packages/react-native-gesture-handler/src/web/handlers/GestureHandler.ts +++ b/packages/react-native-gesture-handler/src/web/handlers/GestureHandler.ts @@ -66,6 +66,7 @@ export default abstract class GestureHandler implements IGestureHandler { private _awaiting = false; private _active = false; + private _attached = false; private _shouldResetProgress = false; private _pointerType: PointerType = PointerType.MOUSE; @@ -86,6 +87,7 @@ export default abstract class GestureHandler implements IGestureHandler { propsRef: React.RefObject, actionType: ActionType ) { + this.attached = true; this.propsRef = propsRef; this.viewRef = viewRef; this.actionType = actionType; @@ -106,6 +108,7 @@ export default abstract class GestureHandler implements IGestureHandler { this.state = State.UNDETERMINED; this.forAnimated = false; this.forReanimated = false; + this.attached = false; this.delegate.detach(); } @@ -1019,6 +1022,13 @@ export default abstract class GestureHandler implements IGestureHandler { this._awaiting = value; } + public get attached() { + return this._attached; + } + protected set attached(value) { + this._attached = value; + } + public get activationIndex() { return this._activationIndex; } diff --git a/packages/react-native-gesture-handler/src/web/handlers/IGestureHandler.ts b/packages/react-native-gesture-handler/src/web/handlers/IGestureHandler.ts index a7797352e4..02611ee35f 100644 --- a/packages/react-native-gesture-handler/src/web/handlers/IGestureHandler.ts +++ b/packages/react-native-gesture-handler/src/web/handlers/IGestureHandler.ts @@ -13,6 +13,7 @@ import type PointerTracker from '../tools/PointerTracker'; import { SingleGestureName } from '../../v3/types'; export default interface IGestureHandler { + attached: boolean; active: boolean; activationIndex: number; awaiting: boolean; From c6051cae923fff1467313dff8eee2454c74d654c Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Fri, 30 Jan 2026 08:54:19 +0100 Subject: [PATCH 215/236] [iOS] Prevent gesture recognizers from beginning, when they should not (#3936) ## Description Gesture detection on complex views (like Text) relies on `gestureRecognizerShouldBegin` checking for the virtual react tag. The logic itself is fine, but iOS runs it when it wants to see if the gesture should activate, at which point all gestures attached to the composed view have fired their `onBegin` callbacks. This PR explicitly calls this method inside `onTouches(Interactions)Began` to immediately fail handlers that don't pass the test. ## Test plan Test the `basic-example/Text` with `onBegin` instead of `onActivate`. --- .../apple/Handlers/RNFlingHandler.m | 6 ++++++ .../apple/Handlers/RNLongPressHandler.m | 6 ++++++ .../apple/Handlers/RNManualHandler.m | 5 +++++ .../apple/Handlers/RNPanHandler.m | 5 +++++ .../apple/Handlers/RNPinchHandler.m | 5 +++++ .../apple/Handlers/RNRotationHandler.m | 5 +++++ .../apple/Handlers/RNTapHandler.m | 5 +++++ .../react-native-gesture-handler/apple/RNGestureHandler.mm | 6 ++++++ 8 files changed, 43 insertions(+) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNFlingHandler.m b/packages/react-native-gesture-handler/apple/Handlers/RNFlingHandler.m index 14e050c09b..26e04adeeb 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNFlingHandler.m +++ b/packages/react-native-gesture-handler/apple/Handlers/RNFlingHandler.m @@ -30,6 +30,12 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event _lastPoint = [[[touches allObjects] objectAtIndex:0] locationInView:_gestureHandler.recognizer.view]; [_gestureHandler reset]; [super touchesBegan:touches withEvent:event]; + + if (self.state == UIGestureRecognizerStatePossible && ![self.delegate gestureRecognizerShouldBegin:self]) { + self.state = UIGestureRecognizerStateFailed; + return; + } + [_gestureHandler.pointerTracker touchesBegan:touches withEvent:event]; // self.numberOfTouches doesn't work for this because in case than one finger is required, diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNLongPressHandler.m b/packages/react-native-gesture-handler/apple/Handlers/RNLongPressHandler.m index 5998fd36b9..dfa690ea9f 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNLongPressHandler.m +++ b/packages/react-native-gesture-handler/apple/Handlers/RNLongPressHandler.m @@ -81,6 +81,12 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [_gestureHandler setCurrentPointerType:event]; [super touchesBegan:touches withEvent:event]; + + if (self.state == UIGestureRecognizerStatePossible && ![self.delegate gestureRecognizerShouldBegin:self]) { + self.state = UIGestureRecognizerStateFailed; + return; + } + [_gestureHandler.pointerTracker touchesBegan:touches withEvent:event]; self.state = UIGestureRecognizerStatePossible; diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNManualHandler.m b/packages/react-native-gesture-handler/apple/Handlers/RNManualHandler.m index d8d620e38d..d9b3214aab 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNManualHandler.m +++ b/packages/react-native-gesture-handler/apple/Handlers/RNManualHandler.m @@ -26,6 +26,11 @@ - (id)initWithGestureHandler:(RNGestureHandler *)gestureHandler - (void)interactionsBegan:(NSSet *)touches withEvent:(UIEvent *)event { + if (self.state == UIGestureRecognizerStatePossible && ![self.delegate gestureRecognizerShouldBegin:self]) { + self.state = UIGestureRecognizerStateFailed; + return; + } + [_gestureHandler.pointerTracker touchesBegan:touches withEvent:event]; if (_shouldSendBeginEvent) { diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNPanHandler.m b/packages/react-native-gesture-handler/apple/Handlers/RNPanHandler.m index f5c5ccdb81..a61f482e26 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNPanHandler.m +++ b/packages/react-native-gesture-handler/apple/Handlers/RNPanHandler.m @@ -121,6 +121,11 @@ - (void)activateAfterLongPress - (void)interactionsBegan:(NSSet *)touches withEvent:(UIEvent *)event { + if (self.state == UIGestureRecognizerStatePossible && ![self.delegate gestureRecognizerShouldBegin:self]) { + self.state = UIGestureRecognizerStateFailed; + return; + } + if (touches.count == 0) { [_gestureHandler reset]; } diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNPinchHandler.m b/packages/react-native-gesture-handler/apple/Handlers/RNPinchHandler.m index 6aecc80855..01b09c3d61 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNPinchHandler.m +++ b/packages/react-native-gesture-handler/apple/Handlers/RNPinchHandler.m @@ -57,6 +57,11 @@ - (void)handleGesture:(UIGestureRecognizer *)recognizer fromReset:(BOOL)fromRese - (void)interactionsBegan:(NSSet *)touches withEvent:(UIEvent *)event { + if (self.state == UIGestureRecognizerStatePossible && ![self.delegate gestureRecognizerShouldBegin:self]) { + self.state = UIGestureRecognizerStateFailed; + return; + } + [_gestureHandler.pointerTracker touchesBegan:touches withEvent:event]; } diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNRotationHandler.m b/packages/react-native-gesture-handler/apple/Handlers/RNRotationHandler.m index 6291d841e0..c584f04998 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNRotationHandler.m +++ b/packages/react-native-gesture-handler/apple/Handlers/RNRotationHandler.m @@ -51,6 +51,11 @@ - (void)handleGesture:(UIGestureRecognizer *)recognizer fromReset:(BOOL)fromRese - (void)interactionsBegan:(NSSet *)touches withEvent:(UIEvent *)event { + if (self.state == UIGestureRecognizerStatePossible && ![self.delegate gestureRecognizerShouldBegin:self]) { + self.state = UIGestureRecognizerStateFailed; + return; + } + [_gestureHandler.pointerTracker touchesBegan:touches withEvent:event]; } diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNTapHandler.m b/packages/react-native-gesture-handler/apple/Handlers/RNTapHandler.m index e022355de1..ad0b5498d4 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNTapHandler.m +++ b/packages/react-native-gesture-handler/apple/Handlers/RNTapHandler.m @@ -78,6 +78,11 @@ - (void)cancel - (void)interactionsBegan:(NSSet *)touches withEvent:(UIEvent *)event { + if (self.state == UIGestureRecognizerStatePossible && ![self.delegate gestureRecognizerShouldBegin:self]) { + self.state = UIGestureRecognizerStateFailed; + return; + } + [_gestureHandler.pointerTracker touchesBegan:touches withEvent:event]; if (_tapsSoFar == 0) { diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm index f24b923edc..7141ba2871 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm @@ -301,6 +301,12 @@ - (RNGHUIView *)chooseViewForInteraction:(UIGestureRecognizer *)recognizer - (void)handleGesture:(UIGestureRecognizer *)recognizer fromReset:(BOOL)fromReset { + // Don't dispatch state changes from undetermined when resetting handler. There will be no follow-up + // since the handler is being reset, so these events are wrong. + if (fromReset && _lastState == RNGestureHandlerStateUndetermined) { + return; + } + RNGHUIView *view = [self chooseViewForInteraction:recognizer]; // it may happen that the gesture recognizer is reset after it's been unbound from the view, From 4fc247ef57a6b8314447298f677322e19a12223c Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 17 Dec 2025 15:01:29 +0100 Subject: [PATCH 216/236] [General] Throw when no gesture is passed to the `GestureDetector` (#3884) Adds an explicit error message when `GestureDetector` is rendered without any gesture. The current behavior is a random error when trying to call a method on `undefined`. Verify that the error is thrown when no gesture is passed. --- .../src/handlers/gestures/GestureDetector/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/index.tsx b/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/index.tsx index 5cee35a61d..52a4d22499 100644 --- a/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/index.tsx +++ b/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/index.tsx @@ -84,6 +84,10 @@ export interface GestureDetectorProps { * @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/gesture-detector */ export const GestureDetector = (props: GestureDetectorProps) => { + if (!props.gesture) { + throw new Error('GestureDetector must have a gesture prop provided.'); + } + // Gesture config should be wrapped with useMemo to prevent unnecessary re-renders const gestureConfig = props.gesture; propagateDetectorConfig(props, gestureConfig); From 0971094b5e3c06137a16387234540545ed3a4a83 Mon Sep 17 00:00:00 2001 From: Dimuthu Wannipurage Date: Wed, 17 Dec 2025 10:40:52 -0500 Subject: [PATCH 217/236] Fixing number of touches becoming 0 issue for pan gestures in apple track pad (#3865) Currently the track pad pinch gesture returns the number of touch points to 0 from native method. This causes the focal X and Y to become NaN. This fixes the issue reported in https://github.com/software-mansion/react-native-gesture-handler/issues/3864 --- .../apple/Handlers/RNPinchHandler.m | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNPinchHandler.m b/packages/react-native-gesture-handler/apple/Handlers/RNPinchHandler.m index 01b09c3d61..12e16fbf6e 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNPinchHandler.m +++ b/packages/react-native-gesture-handler/apple/Handlers/RNPinchHandler.m @@ -168,21 +168,28 @@ - (RNGestureHandlerEventExtraData *)eventExtraData:(NSMagnificationGestureRecogn #else - (RNGestureHandlerEventExtraData *)eventExtraData:(UIPinchGestureRecognizer *)recognizer { - CGPoint accumulatedPoint = CGPointZero; - - for (int i = 0; i < recognizer.numberOfTouches; i++) { - CGPoint location = [recognizer locationOfTouch:i inView:recognizer.view]; - accumulatedPoint.x += location.x; - accumulatedPoint.y += location.y; + CGPoint focalPoint; + NSUInteger numberOfTouches = recognizer.numberOfTouches; + + if (numberOfTouches > 0) { + CGPoint accumulatedPoint = CGPointZero; + + for (int i = 0; i < numberOfTouches; i++) { + CGPoint location = [recognizer locationOfTouch:i inView:recognizer.view]; + accumulatedPoint.x += location.x; + accumulatedPoint.y += location.y; + } + + focalPoint = CGPointMake(accumulatedPoint.x / numberOfTouches, accumulatedPoint.y / numberOfTouches); + } else { + // Trackpad pinch gestures may report 0 touches - use the recognizer's location instead + focalPoint = [recognizer locationInView:recognizer.view]; } - CGPoint focalPoint = - CGPointMake(accumulatedPoint.x / recognizer.numberOfTouches, accumulatedPoint.y / recognizer.numberOfTouches); - return [RNGestureHandlerEventExtraData forPinch:recognizer.scale withFocalPoint:focalPoint withVelocity:recognizer.velocity - withNumberOfTouches:recognizer.numberOfTouches + withNumberOfTouches:numberOfTouches withPointerType:_pointerType]; } #endif From 956a6651dba68f258ec92da6e67320b06ec51092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= <63123542+m-bert@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:55:26 +0100 Subject: [PATCH 218/236] [iOS] Fix manual activation crash (#3890) ## Description In #3855 we've introduced `fromReset` argument for `handleGesture`. However, `ManualActivationRecognizer` does not have such signature. This PR removes it from selector as `fromReset` is not passed anyway. ## Test plan Check that on `Pressable` example app no longer crashes when clicking on **press retention** area. --- .../apple/RNManualActivationRecognizer.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-gesture-handler/apple/RNManualActivationRecognizer.m b/packages/react-native-gesture-handler/apple/RNManualActivationRecognizer.m index 75eac985b1..0b96541f2e 100644 --- a/packages/react-native-gesture-handler/apple/RNManualActivationRecognizer.m +++ b/packages/react-native-gesture-handler/apple/RNManualActivationRecognizer.m @@ -8,7 +8,7 @@ @implementation RNManualActivationRecognizer { - (id)initWithGestureHandler:(RNGestureHandler *)gestureHandler { - if ((self = [super initWithTarget:self action:@selector(handleGesture:fromReset:)])) { + if ((self = [super initWithTarget:self action:@selector(handleGesture:)])) { _handler = gestureHandler; _activePointers = 0; self.delegate = self; From 377bdf530483158b0ecf7545c9b57279b22f1592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Ma=C5=82ecki?= <92953623+p-malecki@users.noreply.github.com> Date: Tue, 23 Dec 2025 13:29:07 +0100 Subject: [PATCH 219/236] [docs] Add sorn25 banner to readme (#3893) ## Description Adds a banner linking to the State of React Native Survey in readme. --- README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b2ef2c9ace..4fe392554a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,17 @@ -React Native Gesture Handler by Software Mansion +

+ React Native Gesture Handler by Software Mansion + + State of React Native Survey + +

### Declarative API exposing platform native touch and gesture system to React Native. From 9aec429c777f70fe12d8bb2a4a910ab79cd92e1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Ma=C5=82ecki?= <92953623+p-malecki@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:49:22 +0100 Subject: [PATCH 220/236] [docs] Remove sorn25 banner from readme (#3922) ## Description Removes a banner linking to the State of React Native Survey from readme. --- README.md | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/README.md b/README.md index 4fe392554a..b2ef2c9ace 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,4 @@ -

- React Native Gesture Handler by Software Mansion - - State of React Native Survey - -

+React Native Gesture Handler by Software Mansion ### Declarative API exposing platform native touch and gesture system to React Native. From a31355746efdee9cd0c411812a9730c4814e04e0 Mon Sep 17 00:00:00 2001 From: Hugo Extrat Date: Wed, 21 Jan 2026 20:12:49 +0100 Subject: [PATCH 221/236] fix(iOS): handles `pointerEvents` for `Pressable` component (#3925) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description fixes: https://github.com/software-mansion/react-native-gesture-handler/issues/3891 fixes: https://github.com/software-mansion/react-native-gesture-handler/issues/3904 This PR implements `pointerEvents` support for `Pressable` component from `react-native-gesture-handler` on iOS. Previously, setting `pointerEvents="box-none"` (or other modes) had no effect on iOS, while it worked correctly on Android and with React Native's `Pressable` on iOS. Android PR: https://github.com/software-mansion/react-native-gesture-handler/pull/3927 ### Implementation Details The implementation follows React Native's `hitTest` logic for `pointerEvents`: - For `box-only`: Returns `self` if point is inside (respecting `hitSlop`), `nil` otherwise - For `box-none`: Checks subviews only, returns the hit subview or `nil` - For `none`: Always returns `nil` - For `auto`: Uses standard hit testing with `shouldHandleTouch` logic The implementation respects `hitTestEdgeInsets` (hitSlop) for all modes, ensuring consistent behavior with React Native's `Pressable`. ## Test plan Tested all `pointerEvents` modes on iOS: - ✅ `pointerEvents="none"` - View and subviews don't receive touches - ✅ `pointerEvents="box-none"` - View doesn't receive touches, subviews do - ✅ `pointerEvents="box-only"` - View receives touches, subviews don't - ✅ `pointerEvents="auto"` - Default behavior works as expected I've used https://github.com/huextrat/repro-pressable-gh to test scenarios Tested on both old architecture (Paper) and new architecture (Fabric). edit: `pointerEvents` with RNGH Pressable is not working on Android --------- Co-authored-by: Michał --- .../apple/RNGestureHandler.h | 1 + .../apple/RNGestureHandlerButton.h | 1 + .../apple/RNGestureHandlerButton.mm | 24 +++++++++++ .../RNGestureHandlerButtonComponentView.mm | 43 +++++++++++++++++++ .../apple/RNGestureHandlerButtonManager.mm | 24 +++++++++++ .../apple/RNGestureHandlerPointerEvents.h | 8 ++++ 6 files changed, 101 insertions(+) create mode 100644 packages/react-native-gesture-handler/apple/RNGestureHandlerPointerEvents.h diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandler.h b/packages/react-native-gesture-handler/apple/RNGestureHandler.h index 597d061a31..edf1ae9ed3 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandler.h +++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.h @@ -3,6 +3,7 @@ #import "RNGestureHandlerDirection.h" #import "RNGestureHandlerEventHandlerType.h" #import "RNGestureHandlerEvents.h" +#import "RNGestureHandlerPointerEvents.h" #import "RNGestureHandlerPointerTracker.h" #import "RNGestureHandlerPointerType.h" #import "RNGestureHandlerState.h" diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.h b/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.h index 59d9304eab..ce0015850f 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.h +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.h @@ -26,6 +26,7 @@ @property (nonatomic, assign) UIEdgeInsets hitTestEdgeInsets; @property (nonatomic, assign) CGFloat borderRadius; @property (nonatomic) BOOL userEnabled; +@property (nonatomic, assign) RNGestureHandlerPointerEvents pointerEvents; #if TARGET_OS_OSX - (void)mountChildComponentView:(RNGHUIView *)childComponentView index:(NSInteger)index; diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm index 9ecec68cc4..64c1ec4873 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm @@ -46,6 +46,7 @@ - (instancetype)init if (self) { _hitTestEdgeInsets = UIEdgeInsetsZero; _userEnabled = YES; + _pointerEvents = RNGestureHandlerPointerEventsAuto; #if !TARGET_OS_TV && !TARGET_OS_OSX [self setExclusiveTouch:YES]; #endif @@ -89,6 +90,29 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event - (RNGHUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + RNGestureHandlerPointerEvents pointerEvents = _pointerEvents; + + if (pointerEvents == RNGestureHandlerPointerEventsNone) { + return nil; + } + + if (pointerEvents == RNGestureHandlerPointerEventsBoxNone) { + for (UIView *subview in [self.subviews reverseObjectEnumerator]) { + if (!subview.isHidden && subview.alpha > 0) { + CGPoint convertedPoint = [subview convertPoint:point fromView:self]; + UIView *hitView = [subview hitTest:convertedPoint withEvent:event]; + if (hitView != nil && [self shouldHandleTouch:hitView]) { + return hitView; + } + } + } + return nil; + } + + if (pointerEvents == RNGestureHandlerPointerEventsBoxOnly) { + return [self pointInside:point withEvent:event] ? self : nil; + } + RNGHUIView *inner = [super hitTest:point withEvent:event]; while (inner && ![self shouldHandleTouch:inner]) { inner = inner.superview; diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm index e90fcc5e84..fa54b10504 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm @@ -7,11 +7,27 @@ #import #import #import +#import #import "RNGestureHandlerButton.h" using namespace facebook::react; +static RNGestureHandlerPointerEvents RCTPointerEventsToEnum(facebook::react::PointerEventsMode pointerEvents) +{ + switch (pointerEvents) { + case facebook::react::PointerEventsMode::None: + return RNGestureHandlerPointerEventsNone; + case facebook::react::PointerEventsMode::BoxNone: + return RNGestureHandlerPointerEventsBoxNone; + case facebook::react::PointerEventsMode::BoxOnly: + return RNGestureHandlerPointerEventsBoxOnly; + case facebook::react::PointerEventsMode::Auto: + default: + return RNGestureHandlerPointerEventsAuto; + } +} + @interface RNGestureHandlerButtonComponentView () @end @@ -205,8 +221,35 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & _buttonView.hitTestEdgeInsets = UIEdgeInsetsMake( -newProps.hitSlop.top, -newProps.hitSlop.left, -newProps.hitSlop.bottom, -newProps.hitSlop.right); + if (!oldProps) { + _buttonView.pointerEvents = RCTPointerEventsToEnum(newProps.pointerEvents); + } else { + const auto &oldButtonProps = *std::static_pointer_cast(oldProps); + if (oldButtonProps.pointerEvents != newProps.pointerEvents) { + _buttonView.pointerEvents = RCTPointerEventsToEnum(newProps.pointerEvents); + } + } + [super updateProps:props oldProps:oldProps]; } + +#if !TARGET_OS_OSX +// Override hitTest to forward touches to _buttonView +// This is necessary because RCTViewComponentView's hitTest might handle pointerEvents +// from ViewProps and prevent touches from reaching _buttonView (which is the contentView). +// Since _buttonView has its own pointerEvents handling, we always forward to it. +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + if (![self pointInside:point withEvent:event]) { + return nil; + } + + CGPoint buttonPoint = [self convertPoint:point toView:_buttonView]; + + return [_buttonView hitTest:buttonPoint withEvent:event]; +} +#endif + @end Class RNGestureHandlerButtonCls(void) diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonManager.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonManager.mm index 68345982cf..a7cc615816 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonManager.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonManager.mm @@ -1,6 +1,20 @@ #import "RNGestureHandlerButtonManager.h" #import "RNGestureHandlerButton.h" +static RNGestureHandlerPointerEvents RCTPointerEventsToEnum(RCTPointerEvents pointerEvents) +{ + switch (pointerEvents) { + case RCTPointerEventsNone: + return RNGestureHandlerPointerEventsNone; + case RCTPointerEventsBoxNone: + return RNGestureHandlerPointerEventsBoxNone; + case RCTPointerEventsBoxOnly: + return RNGestureHandlerPointerEventsBoxOnly; + default: + return RNGestureHandlerPointerEventsAuto; + } +} + @implementation RNGestureHandlerButtonManager RCT_EXPORT_MODULE(RNGestureHandlerButton) @@ -28,6 +42,16 @@ @implementation RNGestureHandlerButtonManager } } +RCT_CUSTOM_VIEW_PROPERTY(pointerEvents, RCTPointerEvents, RNGestureHandlerButton) +{ + if (json) { + RCTPointerEvents pointerEvents = [RCTConvert RCTPointerEvents:json]; + view.pointerEvents = RCTPointerEventsToEnum(pointerEvents); + } else { + view.pointerEvents = RNGestureHandlerPointerEventsAuto; + } +} + - (RNGHUIView *)view { return (RNGHUIView *)[RNGestureHandlerButton new]; diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerPointerEvents.h b/packages/react-native-gesture-handler/apple/RNGestureHandlerPointerEvents.h new file mode 100644 index 0000000000..b2fe715bdd --- /dev/null +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerPointerEvents.h @@ -0,0 +1,8 @@ +#import + +typedef NS_ENUM(NSInteger, RNGestureHandlerPointerEvents) { + RNGestureHandlerPointerEventsNone, + RNGestureHandlerPointerEventsBoxNone, + RNGestureHandlerPointerEventsBoxOnly, + RNGestureHandlerPointerEventsAuto +}; From c744948543677b380dd9aa8656550ac531db13d9 Mon Sep 17 00:00:00 2001 From: Hugo Extrat Date: Thu, 22 Jan 2026 16:51:22 +0100 Subject: [PATCH 222/236] fix(android): handles `pointerEvents` for `Pressable` component (#3927) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes: https://github.com/software-mansion/react-native-gesture-handler/issues/3891 fixes: https://github.com/software-mansion/react-native-gesture-handler/issues/3904 This PR fixes `pointerEvents` support for `Pressable` component from `react-native-gesture-handler` on Android. Waiting for iOS PR: https://github.com/software-mansion/react-native-gesture-handler/pull/3925 as codegen is involved and a small iOS changes is needed Tested all `pointerEvents` modes on Android: - ✅ `pointerEvents="none"` - View and subviews don't receive touches - ✅ `pointerEvents="box-none"` - View doesn't receive touches, subviews do - ✅ `pointerEvents="box-only"` - View receives touches, subviews don't - ✅ `pointerEvents="auto"` - Default behavior works as expected I've used https://github.com/huextrat/repro-pressable-gh to test scenarios Tested on both old architecture (Paper) and new architecture (Fabric). --- .../react/RNGestureHandlerButtonViewManager.kt | 18 +++++++++++++++++- .../RNGestureHandlerButtonComponentView.mm | 11 ++++++++--- .../RNGestureHandlerButtonNativeComponent.ts | 5 +++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt index c4ad8da6e1..dc87058867 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt @@ -26,6 +26,8 @@ import androidx.core.view.children import com.facebook.react.R import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.PixelUtil +import com.facebook.react.uimanager.PointerEvents +import com.facebook.react.uimanager.ReactPointerEventsView import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ViewGroupManager import com.facebook.react.uimanager.ViewManagerDelegate @@ -131,6 +133,17 @@ class RNGestureHandlerButtonViewManager : view.isSoundEffectsEnabled = !touchSoundDisabled } + @ReactProp(name = ViewProps.POINTER_EVENTS) + override fun setPointerEvents(view: ButtonViewGroup, pointerEvents: String?) { + view.pointerEvents = when (pointerEvents) { + "none" -> PointerEvents.NONE + "box-none" -> PointerEvents.BOX_NONE + "box-only" -> PointerEvents.BOX_ONLY + "auto", null -> PointerEvents.AUTO + else -> PointerEvents.AUTO + } + } + override fun onAfterUpdateTransaction(view: ButtonViewGroup) { super.onAfterUpdateTransaction(view) @@ -141,7 +154,8 @@ class RNGestureHandlerButtonViewManager : class ButtonViewGroup(context: Context?) : ViewGroup(context), - NativeViewGestureHandler.NativeViewGestureHandlerHook { + NativeViewGestureHandler.NativeViewGestureHandlerHook, + ReactPointerEventsView { // Using object because of handling null representing no value set. var rippleColor: Int? = null set(color) = withBackgroundUpdate { @@ -199,6 +213,8 @@ class RNGestureHandlerButtonViewManager : var exclusive = true + override var pointerEvents: PointerEvents = PointerEvents.AUTO + private var buttonBackgroundColor = Color.TRANSPARENT private var needBackgroundUpdate = false private var lastEventTime = -1L diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm index fa54b10504..c52051bfaa 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm @@ -221,12 +221,17 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & _buttonView.hitTestEdgeInsets = UIEdgeInsetsMake( -newProps.hitSlop.top, -newProps.hitSlop.left, -newProps.hitSlop.bottom, -newProps.hitSlop.right); + // We need to cast to ViewProps to access the pointerEvents property with the correct type. + // This is necessary because pointerEvents is redefined in the spec, + // which shadows the base property with a different, incompatible type. + const auto &newViewProps = static_cast(newProps); if (!oldProps) { - _buttonView.pointerEvents = RCTPointerEventsToEnum(newProps.pointerEvents); + _buttonView.pointerEvents = RCTPointerEventsToEnum(newViewProps.pointerEvents); } else { const auto &oldButtonProps = *std::static_pointer_cast(oldProps); - if (oldButtonProps.pointerEvents != newProps.pointerEvents) { - _buttonView.pointerEvents = RCTPointerEventsToEnum(newProps.pointerEvents); + const auto &oldViewProps = static_cast(oldButtonProps); + if (oldViewProps.pointerEvents != newViewProps.pointerEvents) { + _buttonView.pointerEvents = RCTPointerEventsToEnum(newViewProps.pointerEvents); } } diff --git a/packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonNativeComponent.ts b/packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonNativeComponent.ts index 8d4c3da4b9..4fedd3160e 100644 --- a/packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonNativeComponent.ts +++ b/packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonNativeComponent.ts @@ -6,6 +6,7 @@ import type { } from 'react-native/Libraries/Types/CodegenTypes'; import type { ViewProps, ColorValue } from 'react-native'; +// @ts-ignore - Redefining pointerEvents with WithDefault for codegen, conflicts with ViewProps type but codegen needs it interface NativeProps extends ViewProps { exclusive?: WithDefault; foreground?: boolean; @@ -17,6 +18,10 @@ interface NativeProps extends ViewProps { borderWidth?: Float; borderColor?: ColorValue; borderStyle?: WithDefault; + pointerEvents?: WithDefault< + 'box-none' | 'none' | 'box-only' | 'auto', + 'auto' + >; } export default codegenNativeComponent('RNGestureHandlerButton'); From 550be45593148b218be03e948501bed7e07654b7 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 28 Jan 2026 13:54:52 +0100 Subject: [PATCH 223/236] Update the script responsible for updating the package version (#3934) ## Description Updates the `set-package-version` script to also support beta and release candidate versions. Since those two may not be published from a stable branch, they require explicit version to be set. The version format for those releases would be: ``` {major}.{minor}.{patch}-rc.{rcVersion} {major}.{minor}.{patch}-beta.{betaVersion} ``` ## Test plan Run the script in different configurations --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/release/set-package-version.js | 84 ++++++++++++++++++++++++-- scripts/release/should-be-latest.js | 8 ++- scripts/release/version-utils.js | 10 ++- 3 files changed, 92 insertions(+), 10 deletions(-) diff --git a/scripts/release/set-package-version.js b/scripts/release/set-package-version.js index d3e6399837..86c56642b4 100644 --- a/scripts/release/set-package-version.js +++ b/scripts/release/set-package-version.js @@ -2,9 +2,17 @@ const fs = require('fs'); const { execSync } = require('child_process'); const { getPackageVersionByTag } = require('./npm-utils'); const { parseVersion, getStableBranchVersion } = require('./version-utils'); +const assert = require('assert'); const PACKAGE_PATH = './packages/react-native-gesture-handler/package.json'; +const ReleaseType = { + STABLE: 'stable', + BETA: 'beta', + RELEASE_CANDIDATE: 'rc', + COMMITLY: 'commitly', +}; + function getLatestVersion() { const latestVersion = getPackageVersionByTag('react-native-gesture-handler', 'latest'); @@ -37,9 +45,24 @@ function getNextStableVersion() { } } -function getVersion(isCommitly) { - if (isCommitly) { - const [major, minor] = getLatestVersion() +function getNextPreReleaseVersion(releaseType, version) { + let dotIndex = 1; + while (true) { + const targetVersion = `${version}-${releaseType}.${dotIndex}`; + + try { + // if the version is already published, increment the pre-release sequence (rc/beta number) and try again + getPackageVersionByTag('react-native-gesture-handler', targetVersion); + dotIndex++; + } catch (error) { + return targetVersion; + } + } +} + +function getVersion(releaseType, preReleaseVersion = null) { + if (releaseType === ReleaseType.COMMITLY) { + const [major, minor] = getLatestVersion(); const currentSHA = execSync('git rev-parse HEAD').toString().trim(); const now = new Date(); @@ -50,6 +73,12 @@ function getVersion(isCommitly) { const commitlyVersion = `${major}.${minor + 1}.${0}-nightly-${currentDate}-${currentSHA.slice(0, 9)}`; return commitlyVersion; + } else if (releaseType === ReleaseType.BETA || releaseType === ReleaseType.RELEASE_CANDIDATE) { + if (preReleaseVersion == null) { + throw new Error(`Version must be provided for ${releaseType} releases`); + } + + return getNextPreReleaseVersion(releaseType, preReleaseVersion); } const [major, minor, patch] = getNextStableVersion(); @@ -57,8 +86,49 @@ function getVersion(isCommitly) { } function setPackageVersion() { - const isCommitly = process.argv.includes('--commitly'); - const version = getVersion(isCommitly); + let version = null; + let isCommitly = false; + let isBeta = false; + let isReleaseCandidate = false; + + for (let i = 2; i < process.argv.length; i++) { + const arg = process.argv[i]; + if (arg === '--commitly') { + isCommitly = true; + } else if (arg === '--beta') { + isBeta = true; + } else if (arg === '--rc') { + isReleaseCandidate = true; + } else if (arg === '--version') { + if (i + 1 < process.argv.length) { + version = process.argv[i + 1]; + i++; + } else { + throw new Error('Expected a version after --version'); + } + } + } + + assert([isCommitly, isBeta, isReleaseCandidate].filter(Boolean).length <= 1, 'Release flags --commitly, --beta, and --rc are mutually exclusive; specify at most one'); + assert(version === null || isBeta || isReleaseCandidate, 'Version should not be provided for stable nor commitly releases'); + assert(version !== null || (!isBeta && !isReleaseCandidate), 'Version must be provided for beta and release candidate releases'); + + const releaseType = isCommitly + ? ReleaseType.COMMITLY + : isBeta + ? ReleaseType.BETA + : isReleaseCandidate + ? ReleaseType.RELEASE_CANDIDATE + : ReleaseType.STABLE; + + if (version != null) { + const versionRegex = /^(\d+)\.(\d+)\.(\d+)$/; + if (!versionRegex.test(version)) { + throw new Error(`Provided version "${version}" is not valid. Expected format: x.y.z`); + } + } + + version = getVersion(releaseType, version); const packageJson = JSON.parse(fs.readFileSync(PACKAGE_PATH, 'utf8')); packageJson.version = version; @@ -68,4 +138,6 @@ function setPackageVersion() { console.log(version); } -setPackageVersion(); +if (require.main === module) { + setPackageVersion(); +} diff --git a/scripts/release/should-be-latest.js b/scripts/release/should-be-latest.js index 9ebfa51071..a0fa94724a 100644 --- a/scripts/release/should-be-latest.js +++ b/scripts/release/should-be-latest.js @@ -2,9 +2,15 @@ const { getPackageVersionByTag } = require('./npm-utils'); const { parseVersion } = require('./version-utils'); function shouldBeLatest(version) { + const [newMajor, newMinor, newPatch, newPreRelease] = parseVersion(version); + + // Pre-releases should never be latest + if (newPreRelease !== null) { + return false; + } + const latestVersion = getPackageVersionByTag('react-native-gesture-handler', 'latest'); const [major, minor, patch] = parseVersion(latestVersion); - const [newMajor, newMinor, newPatch] = parseVersion(version); // TODO: We'll worry about 3.x.x later :) if (newMajor !== major) { diff --git a/scripts/release/version-utils.js b/scripts/release/version-utils.js index 6f0d949f33..dabbd6389f 100644 --- a/scripts/release/version-utils.js +++ b/scripts/release/version-utils.js @@ -1,11 +1,15 @@ const { execSync } = require('child_process'); -const VERSION_REGEX = /^(\d+)\.(\d+)\.(\d+)$/; +const VERSION_REGEX = /^(\d+)\.(\d+)\.(\d+)(-.*)?$/; const BRANCH_REGEX = /^(\d+)\.(\d+)-stable$/; function parseVersion(version) { - const [, major, minor, patch] = version.match(VERSION_REGEX); - return [Number(major), Number(minor), Number(patch)]; + const match = version.match(VERSION_REGEX); + if (!match) { + throw new Error(`Invalid version string: ${version}`); + } + const [, major, minor, patch, preRelease] = match; + return [Number(major), Number(minor), Number(patch), preRelease || null]; } function getStableBranchVersion() { From 5f4b7e85d0c0698e5c41a842359fe411d28ff3d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= <81448793+akwasniewski@users.noreply.github.com> Date: Fri, 30 Jan 2026 12:03:05 +0100 Subject: [PATCH 224/236] Update mocks (#3854) ## Description Updates mocks to mock `HostGestureDetector`, and have a separate file for mocking buttons. Moreover it adds tests for correct error throws in V3. ## Test plan `yarn test` --- .../react-native-gesture-handler/jestSetup.js | 30 +++-- .../src/__mocks__/RNGestureHandlerModule.ts | 2 +- .../src/__tests__/Errors.test.tsx | 125 ++++++++++++++++++ .../src/mocks/GestureButtons.tsx | 10 ++ .../src/mocks/gestureComponents.tsx | 23 ++++ .../src/mocks/hostDetector.tsx | 5 + .../src/mocks/mocks.tsx | 76 ----------- .../src/mocks/module.tsx | 25 ++++ .../useEnsureGestureHandlerRootView.ts | 3 +- 9 files changed, 211 insertions(+), 88 deletions(-) create mode 100644 packages/react-native-gesture-handler/src/__tests__/Errors.test.tsx create mode 100644 packages/react-native-gesture-handler/src/mocks/GestureButtons.tsx create mode 100644 packages/react-native-gesture-handler/src/mocks/gestureComponents.tsx create mode 100644 packages/react-native-gesture-handler/src/mocks/hostDetector.tsx delete mode 100644 packages/react-native-gesture-handler/src/mocks/mocks.tsx create mode 100644 packages/react-native-gesture-handler/src/mocks/module.tsx diff --git a/packages/react-native-gesture-handler/jestSetup.js b/packages/react-native-gesture-handler/jestSetup.js index ebc6cd1a56..0a2d18c788 100644 --- a/packages/react-native-gesture-handler/jestSetup.js +++ b/packages/react-native-gesture-handler/jestSetup.js @@ -1,26 +1,38 @@ -jest.mock('./src/RNGestureHandlerModule', () => require('./src/mocks/mocks')); -jest.mock('./src/components/GestureButtons', () => require('./src/mocks/mocks')); -jest.mock('./src/components/Pressable/Pressable', () => require('./src/mocks/Pressable')); +jest.mock('./src/RNGestureHandlerModule', () => require('./src/mocks/module')); +jest.mock('./src/components/GestureButtons', () => require('./src/mocks/gestureButtons')); +jest.mock('./src/components/Pressable/Pressable', () => require('./src/mocks/Pressable')); +jest.mock('./src/components/GestureComponents', () => require('./src/mocks/gestureComponents')); +jest.mock('./src/v3/detectors/HostGestureDetector', () => require('./src/mocks/hostDetector')); jest.mock('./lib/commonjs/RNGestureHandlerModule', () => - require('./lib/commonjs/mocks/mocks') + require('./lib/commonjs/mocks/module') ); jest.mock('./lib/commonjs/components/GestureButtons', () => - require('./lib/commonjs/mocks/mocks') + require('./lib/commonjs/mocks/GestureButtons') ); jest.mock('./lib/commonjs/components/Pressable', () => require('./lib/commonjs/mocks/Pressable') ); - +jest.mock('./lib/commonjs/components/GestureComponents', () => + require('./lib/commonjs/mocks/gestureComponents') +); +jest.mock('./lib/commonjs/v3/detectors/HostGestureDetector', () => + require('./lib/commonjs/mocks/hostDetector') +); jest.mock('./lib/module/RNGestureHandlerModule', () => - require('./lib/module/mocks/mocks') + require('./lib/module/mocks/module') ); jest.mock('./lib/module/components/GestureButtons', () => - require('./lib/module/mocks/mocks') + require('./lib/module/mocks/GestureButtons') ); jest.mock('./lib/module/components/Pressable', () => require('./lib/module/mocks/Pressable') ); - +jest.mock('./lib/module/components/GestureComponents', () => + require('./lib/module/mocks/gestureComponents') +); +jest.mock('./lib/module/v3/detectors/HostGestureDetector', () => + require('./lib/module/mocks/hostDetector') +); diff --git a/packages/react-native-gesture-handler/src/__mocks__/RNGestureHandlerModule.ts b/packages/react-native-gesture-handler/src/__mocks__/RNGestureHandlerModule.ts index 1d52ce8df9..6c8c202a3a 100644 --- a/packages/react-native-gesture-handler/src/__mocks__/RNGestureHandlerModule.ts +++ b/packages/react-native-gesture-handler/src/__mocks__/RNGestureHandlerModule.ts @@ -1,4 +1,4 @@ -import Mocks from '../mocks/mocks'; +import Mocks from '../mocks/module'; export default { ...Mocks, diff --git a/packages/react-native-gesture-handler/src/__tests__/Errors.test.tsx b/packages/react-native-gesture-handler/src/__tests__/Errors.test.tsx new file mode 100644 index 0000000000..e494f2cf7a --- /dev/null +++ b/packages/react-native-gesture-handler/src/__tests__/Errors.test.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { render, cleanup } from '@testing-library/react-native'; +import { + Gesture, + GestureDetector, + GestureHandlerRootView, + InterceptingGestureDetector, + useTapGesture, +} from '../index'; +import { findNodeHandle, View } from 'react-native'; +import { VirtualDetector } from '../v3/detectors/VirtualDetector/VirtualDetector'; + +beforeEach(() => cleanup()); +jest.mock('react-native/Libraries/ReactNative/RendererProxy', () => ({ + findNodeHandle: jest.fn(), +})); + +describe('VirtualDetector', () => { + test('virtual detector must be under InterceptingGestureDetector', () => { + function VirtualDetectorWithNoBoundary() { + const tap = useTapGesture({}); + return ( + + + + + + ); + } + + expect(() => render()).toThrow( + 'VirtualGestureDetector must be a descendant of an InterceptingGestureDetector' + ); + }); + test('virtual detector does not handle animated events', () => { + (findNodeHandle as jest.Mock).mockReturnValue(123); + + function VirtualDetectorAnimated() { + const tap = useTapGesture({ useAnimated: true }); + return ( + + + + + + + + ); + } + + expect(() => render()).toThrow( + 'VirtualGestureDetector cannot handle Animated events with native driver when used inside InterceptingGestureDetector. Use Reanimated or Animated events without native driver instead.' + ); + }); + + test('intercepting detector cant handle multiple types of events', () => { + (findNodeHandle as jest.Mock).mockReturnValue(123); + const mockWorklet = () => undefined; + mockWorklet.__workletHash = 123; + + function InterceptingDetectorMultipleTypes() { + const tap = useTapGesture({ useAnimated: true }); + const tap2 = useTapGesture({ onActivate: mockWorklet }); + return ( + + + + + + + + ); + } + + expect(() => render()).toThrow( + 'InterceptingGestureDetector can only handle either Reanimated or Animated events.' + ); + }); +}); + +describe('Check if descendant of root view', () => { + test('gesture detector', () => { + function GestureDetectorNoRootView() { + const tap = useTapGesture({}); + return ( + + + + ); + } + expect(() => render()).toThrow( + 'GestureDetector must be used as a descendant of GestureHandlerRootView. Otherwise the gestures will not be recognized. See https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/installation for more details.' + ); + }); + + test('intercepting detector', () => { + function GestureDetectorNoRootView() { + const tap = useTapGesture({}); + return ( + + + + ); + } + + expect(() => render()).toThrow( + 'GestureDetector must be used as a descendant of GestureHandlerRootView. Otherwise the gestures will not be recognized. See https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/installation for more details.' + ); + }); + + test('legacy detector', () => { + function GestureDetectorNoRootView() { + const tap = Gesture.Tap(); + return ( + + + + ); + } + + expect(() => render()).toThrow( + 'GestureDetector must be used as a descendant of GestureHandlerRootView. Otherwise the gestures will not be recognized. See https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/installation for more details.' + ); + }); +}); diff --git a/packages/react-native-gesture-handler/src/mocks/GestureButtons.tsx b/packages/react-native-gesture-handler/src/mocks/GestureButtons.tsx new file mode 100644 index 0000000000..a4ca02ae26 --- /dev/null +++ b/packages/react-native-gesture-handler/src/mocks/GestureButtons.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { TouchableNativeFeedback, View } from 'react-native'; +export const RawButton = ({ enabled, ...rest }: any) => ( + + + +); +export const BaseButton = RawButton; +export const RectButton = RawButton; +export const BorderlessButton = TouchableNativeFeedback; diff --git a/packages/react-native-gesture-handler/src/mocks/gestureComponents.tsx b/packages/react-native-gesture-handler/src/mocks/gestureComponents.tsx new file mode 100644 index 0000000000..a9270fc52e --- /dev/null +++ b/packages/react-native-gesture-handler/src/mocks/gestureComponents.tsx @@ -0,0 +1,23 @@ +import { + TouchableHighlight, + TouchableNativeFeedback, + TouchableOpacity, + TouchableWithoutFeedback, + ScrollView, + FlatList, + Switch, + TextInput, + DrawerLayoutAndroid, +} from 'react-native'; + +export default { + TouchableHighlight, + TouchableNativeFeedback, + TouchableOpacity, + TouchableWithoutFeedback, + ScrollView, + FlatList, + Switch, + TextInput, + DrawerLayoutAndroid, +} as const; diff --git a/packages/react-native-gesture-handler/src/mocks/hostDetector.tsx b/packages/react-native-gesture-handler/src/mocks/hostDetector.tsx new file mode 100644 index 0000000000..2739c45e0a --- /dev/null +++ b/packages/react-native-gesture-handler/src/mocks/hostDetector.tsx @@ -0,0 +1,5 @@ +import { View } from 'react-native'; + +const HostGestureDetector = View; + +export default HostGestureDetector; diff --git a/packages/react-native-gesture-handler/src/mocks/mocks.tsx b/packages/react-native-gesture-handler/src/mocks/mocks.tsx deleted file mode 100644 index e04a070782..0000000000 --- a/packages/react-native-gesture-handler/src/mocks/mocks.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import { - TouchableHighlight, - TouchableNativeFeedback, - TouchableOpacity, - TouchableWithoutFeedback, - ScrollView, - FlatList, - Switch, - TextInput, - DrawerLayoutAndroid, - View, -} from 'react-native'; -import { State } from '../State'; -import { Directions } from '../Directions'; - -const NOOP = () => { - // Do nothing -}; -const PanGestureHandler = View; -const attachGestureHandler = NOOP; -const createGestureHandler = NOOP; -const dropGestureHandler = NOOP; -const setGestureHandlerConfig = NOOP; -const updateGestureHandlerConfig = NOOP; -const flushOperations = NOOP; -const configureRelations = NOOP; -const setReanimatedAvailable = NOOP; -const install = NOOP; -const NativeViewGestureHandler = View; -const TapGestureHandler = View; -const ForceTouchGestureHandler = View; -const LongPressGestureHandler = View; -const PinchGestureHandler = View; -const RotationGestureHandler = View; -const FlingGestureHandler = View; -export const RawButton = ({ enabled, ...rest }: any) => ( - - - -); -export const BaseButton = RawButton; -export const RectButton = RawButton; -export const BorderlessButton = TouchableNativeFeedback; - -export default { - TouchableHighlight, - TouchableNativeFeedback, - TouchableOpacity, - TouchableWithoutFeedback, - ScrollView, - FlatList, - Switch, - TextInput, - DrawerLayoutAndroid, - NativeViewGestureHandler, - TapGestureHandler, - ForceTouchGestureHandler, - LongPressGestureHandler, - PinchGestureHandler, - RotationGestureHandler, - FlingGestureHandler, - PanGestureHandler, - attachGestureHandler, - createGestureHandler, - dropGestureHandler, - setGestureHandlerConfig, - updateGestureHandlerConfig, - configureRelations, - setReanimatedAvailable, - flushOperations, - install, - // Probably can be removed - Directions, - State, -} as const; diff --git a/packages/react-native-gesture-handler/src/mocks/module.tsx b/packages/react-native-gesture-handler/src/mocks/module.tsx new file mode 100644 index 0000000000..c6b957e1d0 --- /dev/null +++ b/packages/react-native-gesture-handler/src/mocks/module.tsx @@ -0,0 +1,25 @@ +const NOOP = () => { + // Do nothing +}; + +const attachGestureHandler = NOOP; +const createGestureHandler = NOOP; +const dropGestureHandler = NOOP; +const setGestureHandlerConfig = NOOP; +const updateGestureHandlerConfig = NOOP; +const flushOperations = NOOP; +const configureRelations = NOOP; +const setReanimatedAvailable = NOOP; +const install = NOOP; + +export default { + attachGestureHandler, + createGestureHandler, + dropGestureHandler, + setGestureHandlerConfig, + updateGestureHandlerConfig, + configureRelations, + setReanimatedAvailable, + flushOperations, + install, +} as const; diff --git a/packages/react-native-gesture-handler/src/v3/detectors/useEnsureGestureHandlerRootView.ts b/packages/react-native-gesture-handler/src/v3/detectors/useEnsureGestureHandlerRootView.ts index 2cb5949bbd..41fdf97698 100644 --- a/packages/react-native-gesture-handler/src/v3/detectors/useEnsureGestureHandlerRootView.ts +++ b/packages/react-native-gesture-handler/src/v3/detectors/useEnsureGestureHandlerRootView.ts @@ -1,12 +1,11 @@ import { use } from 'react'; -import { isTestEnv } from '../../utils'; import { Platform } from 'react-native'; import GestureHandlerRootViewContext from '../../GestureHandlerRootViewContext'; export function useEnsureGestureHandlerRootView() { const rootViewContext = use(GestureHandlerRootViewContext); - if (__DEV__ && !rootViewContext && !isTestEnv() && Platform.OS !== 'web') { + if (__DEV__ && !rootViewContext && Platform.OS !== 'web') { throw new Error( 'GestureDetector must be used as a descendant of GestureHandlerRootView. Otherwise the gestures will not be recognized. See https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/installation for more details.' ); From e0e3a915f9560b4dfe9ec51ee94543d9d1a7e8bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= <81448793+akwasniewski@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:40:18 +0100 Subject: [PATCH 225/236] Cleaning manually activated handlers (#3943) ## Description On android and web gestures activated with state manager were not cleaned up properly as they were never registered in the gesture orchestrator. ## Test plan Tested on the following example
```tsx import React from 'react'; import { StyleSheet, View } from 'react-native'; import { GestureHandlerRootView, GestureDetector, useLongPressGesture, GestureStateManager, LongPressGesture, } from 'react-native-gesture-handler'; import Animated, { useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated'; export const COLORS = { offWhite: '#f8f9ff', headerSeparator: '#eef0ff', PURPLE: '#b58df1', NAVY: '#001A72', RED: '#A41623', YELLOW: '#F2AF29', GREEN: '#0F956F', GRAY: '#ADB1C2', KINDA_RED: '#FFB2AD', KINDA_YELLOW: '#FFF096', KINDA_GREEN: '#C4E7DB', KINDA_BLUE: '#A0D5EF', }; export default function TwoPressables() { const isActivated = [ useSharedValue(0), useSharedValue(0), useSharedValue(0), useSharedValue(0), ]; const gestures: LongPressGesture[] = []; const createGestureConfig = (index: number) => ({ onActivate: () => { 'worklet'; isActivated[index].value = 1; console.log(`Box ${index}: long pressed`); const nextIndex = index + 1; if (nextIndex < gestures.length) { const nextGesture = gestures[nextIndex]; if (nextGesture) { GestureStateManager.activate(nextGesture.handlerTag); } } }, onFinalize: () => { 'worklet'; isActivated[index].value = 0; const nextIndex = index + 1; if (nextIndex < gestures.length) { const nextGesture = gestures[nextIndex]; if (nextGesture) { GestureStateManager.deactivate(nextGesture.handlerTag); } } }, disableReanimated: true, }); const g0 = useLongPressGesture(createGestureConfig(0)); const g1 = useLongPressGesture(createGestureConfig(1)); const g2 = useLongPressGesture(createGestureConfig(2)); const g3 = useLongPressGesture(createGestureConfig(3)); gestures[0] = g0; gestures[1] = g1; gestures[2] = g2; gestures[3] = g3; const colors = [COLORS.PURPLE, COLORS.NAVY, COLORS.GREEN, COLORS.RED]; function Box({ index }: { index: number }) { const animatedStyle = useAnimatedStyle(() => ({ opacity: isActivated[index].value === 1 ? 0.5 : 1, transform: [ { scale: withTiming(isActivated[index].value === 1 ? 0.95 : 1) }, ], })); return ( ); } return ( ); } const commonStyles = StyleSheet.create({ centerView: { flex: 1, justifyContent: 'center', alignItems: 'center', }, box: { height: 150, width: 150, borderRadius: 20, marginBottom: 30, }, }) ``` --- .../java/com/swmansion/gesturehandler/core/GestureHandler.kt | 4 ++++ .../swmansion/gesturehandler/react/RNGestureHandlerModule.kt | 4 ++++ .../src/v3/gestureStateManager.web.ts | 3 +++ 3 files changed, 11 insertions(+) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt index 30ba4fcf7e..3ca3e108d5 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt @@ -763,6 +763,10 @@ open class GestureHandler { protected open fun onCancel() {} protected open fun onFail() {} + fun recordHandlerIfNotPresent() { + hostDetectorView?.recordHandlerIfNotPresent(this) + } + private fun isButtonInConfig(clickedButton: Int): Boolean { if (mouseButton == 0) { return clickedButton == MotionEvent.BUTTON_PRIMARY 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 09b02ad1f1..7b2ab98640 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 @@ -155,6 +155,10 @@ class RNGestureHandlerModule(reactContext: ReactApplicationContext?) : } } + if (newState == GestureHandler.STATE_ACTIVE || newState == GestureHandler.STATE_BEGAN) { + handler.recordHandlerIfNotPresent() + } + when (newState) { GestureHandler.STATE_ACTIVE -> handler.activate(force = true) GestureHandler.STATE_BEGAN -> handler.begin() diff --git a/packages/react-native-gesture-handler/src/v3/gestureStateManager.web.ts b/packages/react-native-gesture-handler/src/v3/gestureStateManager.web.ts index 8705d72489..8132cafce0 100644 --- a/packages/react-native-gesture-handler/src/v3/gestureStateManager.web.ts +++ b/packages/react-native-gesture-handler/src/v3/gestureStateManager.web.ts @@ -1,6 +1,7 @@ import { State } from '../State'; import { tagMessage } from '../utils'; import IGestureHandler from '../web/handlers/IGestureHandler'; +import GestureHandlerOrchestrator from '../web/tools/GestureHandlerOrchestrator'; import NodeManager from '../web/tools/NodeManager'; import { GestureStateManagerType } from './gestureStateManager'; @@ -20,6 +21,7 @@ export const GestureStateManager: GestureStateManagerType = { const handler = NodeManager.getHandler(handlerTag); ensureHandlerAttached(handler); + GestureHandlerOrchestrator.instance.recordHandlerIfNotPresent(handler); handler.begin(); }, @@ -33,6 +35,7 @@ export const GestureStateManager: GestureStateManagerType = { handler.begin(); } + GestureHandlerOrchestrator.instance.recordHandlerIfNotPresent(handler); handler.activate(true); }, From 028fbf68d3724dc5677c66fc3b6dccc2d09e87b3 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 2 Feb 2026 09:44:41 +0100 Subject: [PATCH 226/236] [Native] Fix `shouldCancelWhenOutside` not tracking the view (#3942) ## Description Addresses the underlying issue of https://github.com/software-mansion/react-native-gesture-handler/pull/3906 The original issue described in the above PR was caused by wrong shadow node dimensions, which were fixed by https://github.com/software-mansion/react-native-gesture-handler/pull/3930. After that, the dimensions were good, but the long press was still failing. This was caused by `shouldCancelWhenOutside` checking the dimensions of the detector while the child was moved. This PR changes the logic so that the child's hitbox is checked when using the native detector. This should be enough for iOS, but on Android, further investigation is needed into whether the entire `transformedEvent` should be in the coordinate space of the detector or its child. ## Test plan See https://github.com/software-mansion/react-native-gesture-handler/pull/3906 test plan --- .../gesturehandler/core/GestureHandler.kt | 36 ++++++++++++++----- .../core/GestureHandlerOrchestrator.kt | 8 +---- .../apple/RNGestureHandler.mm | 13 +++++-- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt index 3ca3e108d5..833fd08195 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt @@ -9,6 +9,7 @@ import android.view.MotionEvent import android.view.MotionEvent.PointerCoords import android.view.MotionEvent.PointerProperties import android.view.View +import androidx.core.view.isNotEmpty import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.ReadableMap @@ -384,18 +385,37 @@ open class GestureHandler { } } - x = adaptedTransformedEvent.x - y = adaptedTransformedEvent.y numberOfPointers = adaptedTransformedEvent.pointerCount - isWithinBounds = isWithinBounds(view, x, y) - if (shouldCancelWhenOutside && !isWithinBounds) { - if (state == STATE_ACTIVE) { - cancel() - } else if (state == STATE_BEGAN) { + + // TODO: this is likely wrong, and the transformed event itself should be + // in the coordinate system of the child view, but I'm not sure of the + // consequences + if (view is RNGestureHandlerDetectorView && (view as RNGestureHandlerDetectorView).isNotEmpty()) { + val detector = view as RNGestureHandlerDetectorView + val outPoint = PointF() + GestureHandlerOrchestrator.transformPointToChildViewCoords( + adaptedTransformedEvent.x, + adaptedTransformedEvent.y, + detector, + detector.getChildAt(0), + outPoint, + ) + x = outPoint.x + y = outPoint.y + isWithinBounds = isWithinBounds(detector.getChildAt(0), x, y) + } else { + x = adaptedTransformedEvent.x + y = adaptedTransformedEvent.y + isWithinBounds = isWithinBounds(view, x, y) + } + + if (shouldCancelWhenOutside) { + if (!isWithinBounds && (state == STATE_ACTIVE || state == STATE_BEGAN)) { fail() + return } - return } + lastAbsolutePositionX = GestureUtils.getLastPointerX(adaptedTransformedEvent, true) lastAbsolutePositionY = GestureUtils.getLastPointerY(adaptedTransformedEvent, true) lastEventOffsetX = adaptedTransformedEvent.rawX - adaptedTransformedEvent.x diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt index 5907cd6ac1..30c7a98575 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt @@ -750,13 +750,7 @@ class GestureHandlerOrchestrator( return isLeafOrTransparent && isTransformedTouchPointInView(coords[0], coords[1], view) } - private fun transformPointToChildViewCoords( - x: Float, - y: Float, - parent: ViewGroup, - child: View, - outLocalPoint: PointF, - ) { + fun transformPointToChildViewCoords(x: Float, y: Float, parent: ViewGroup, child: View, outLocalPoint: PointF) { var localX = x + parent.scrollX - child.left var localY = y + parent.scrollY - child.top val matrix = child.matrix diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm index 7141ba2871..72be857c69 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm @@ -685,9 +685,16 @@ - (void)reset - (BOOL)containsPointInView { - CGPoint pt = [_recognizer locationInView:_recognizer.view]; - CGRect hitFrame = RNGHHitSlopInsetRect(_recognizer.view.bounds, _hitSlop); - return CGRectContainsPoint(hitFrame, pt); + RNGHUIView *viewToHitTest = _recognizer.view; + + if (_shouldCancelWhenOutside && [self usesNativeOrVirtualDetector] && [_recognizer.view.subviews count] > 0) { + viewToHitTest = _recognizer.view.subviews[0]; + } + + CGPoint location = [_recognizer locationInView:viewToHitTest]; + CGRect hitFrame = RNGHHitSlopInsetRect(viewToHitTest.bounds, _hitSlop); + + return CGRectContainsPoint(hitFrame, location); } - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer From 0d892af24637176bf5f65d18209934e02a886208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= <63123542+m-bert@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:06:23 +0100 Subject: [PATCH 227/236] [docs] State management (#3905) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This PR handles state management in our docs: - Moves information about states to "under the hood" section - Rewrites entire **states & events** page to focus more on callbacks - Updates `GestureStateManager` entry and moves it to **fundamentals** - Merges manual gesture guide with manual gesture docs ## Test plan Read docs 🤓 --------- Co-authored-by: Jakub Piasecki Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../docs/fundamentals/callbacks-events.mdx | 173 +++++++++++++++++ .../docs/fundamentals/state-manager.mdx | 160 ++++++++++++++++ .../docs/fundamentals/states-events.mdx | 89 --------- .../_manual_gesture_steps}/step1.md | 8 +- .../_manual_gesture_steps}/step2.md | 22 ++- .../gestures/_manual_gesture_steps/step3.md | 55 ++++++ .../gestures/_manual_gesture_steps/step4.md | 17 ++ .../gestures/_manual_gesture_steps/step5.md | 14 ++ .../gestures/_manual_gesture_steps/step6.md | 18 ++ .../gestures/_manual_gesture_steps/step7.md | 10 + .../_shared/base-continuous-gesture-config.md | 2 +- .../gestures/_shared/base-gesture-config.md | 10 +- .../docs/gestures/state-manager.md | 27 --- .../docs/gestures/use-fling-gesture.mdx | 4 +- .../docs/gestures/use-long-press-gesture.mdx | 4 +- .../docs/gestures/use-manual-gesture.mdx | 179 +++++++++++++++++- .../docs/gestures/use-native-gesture.mdx | 4 +- .../docs/gestures/use-pan-gesture.mdx | 10 +- .../docs/gestures/use-pinch-gesture.mdx | 2 +- .../docs/gestures/use-rotation-gesture.mdx | 2 +- .../docs/gestures/use-tap-gesture.mdx | 12 +- .../guides/manual-gestures/_steps/step3.md | 31 --- .../guides/manual-gestures/_steps/step4.md | 15 -- .../guides/manual-gestures/_steps/step5.md | 13 -- .../guides/manual-gestures/_steps/step6.md | 17 -- .../guides/manual-gestures/_steps/step7.md | 10 - .../docs/guides/manual-gestures/index.md | 62 ------ .../docs/guides/troubleshooting.md | 55 ------ .../docs/under-the-hood/state.md | 92 +-------- packages/docs-gesture-handler/package.json | 3 +- .../CallbacksFlowCharts/FlowChart.jsx | 126 ++++++++++++ .../CallbacksFlowCharts/GestureEventChart.jsx | 47 +++++ .../CallbacksFlowCharts/TouchEventChart.jsx | 39 ++++ .../src/examples/CallbacksFlowCharts/index.ts | 2 + .../src/theme/MDXComponents/Details.js | 2 +- packages/docs-gesture-handler/yarn.lock | 5 + .../apple/RNGestureHandlerDetector.mm | 6 +- 37 files changed, 897 insertions(+), 450 deletions(-) create mode 100644 packages/docs-gesture-handler/docs/fundamentals/callbacks-events.mdx create mode 100644 packages/docs-gesture-handler/docs/fundamentals/state-manager.mdx delete mode 100644 packages/docs-gesture-handler/docs/fundamentals/states-events.mdx rename packages/docs-gesture-handler/docs/{guides/manual-gestures/_steps => gestures/_manual_gesture_steps}/step1.md (63%) rename packages/docs-gesture-handler/docs/{guides/manual-gestures/_steps => gestures/_manual_gesture_steps}/step2.md (75%) create mode 100644 packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step3.md create mode 100644 packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step4.md create mode 100644 packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step5.md create mode 100644 packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step6.md create mode 100644 packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step7.md delete mode 100644 packages/docs-gesture-handler/docs/gestures/state-manager.md delete mode 100644 packages/docs-gesture-handler/docs/guides/manual-gestures/_steps/step3.md delete mode 100644 packages/docs-gesture-handler/docs/guides/manual-gestures/_steps/step4.md delete mode 100644 packages/docs-gesture-handler/docs/guides/manual-gestures/_steps/step5.md delete mode 100644 packages/docs-gesture-handler/docs/guides/manual-gestures/_steps/step6.md delete mode 100644 packages/docs-gesture-handler/docs/guides/manual-gestures/_steps/step7.md delete mode 100644 packages/docs-gesture-handler/docs/guides/manual-gestures/index.md create mode 100644 packages/docs-gesture-handler/src/examples/CallbacksFlowCharts/FlowChart.jsx create mode 100644 packages/docs-gesture-handler/src/examples/CallbacksFlowCharts/GestureEventChart.jsx create mode 100644 packages/docs-gesture-handler/src/examples/CallbacksFlowCharts/TouchEventChart.jsx create mode 100644 packages/docs-gesture-handler/src/examples/CallbacksFlowCharts/index.ts diff --git a/packages/docs-gesture-handler/docs/fundamentals/callbacks-events.mdx b/packages/docs-gesture-handler/docs/fundamentals/callbacks-events.mdx new file mode 100644 index 0000000000..e556caa5b0 --- /dev/null +++ b/packages/docs-gesture-handler/docs/fundamentals/callbacks-events.mdx @@ -0,0 +1,173 @@ +--- +id: callbacks-events +title: Gesture callbacks & events +sidebar_label: Gesture callbacks & events +sidebar_position: 7 +--- + +import { GestureEventFlowChart, TouchEventFlowChart } from '@site/src/examples/CallbacksFlowCharts' +import CollapsibleCode from '@site/src/components/CollapsibleCode'; + +At any given time, each handler instance has an assigned [state](/docs/under-the-hood/state) that can change when new touch events occur or can be forced to change by the touch system in certain circumstances. You can hook into state transitions using specific [gesture callbacks](#callbacks). + +When `Reanimated` is installed, all callbacks are automatically workletized. For more details, refer to the [Integration with Reanimated](/docs/fundamentals/reanimated-interactions#automatic-workletization-of-gesture-callbacks) section. + +## Callbacks flow + +### GestureEvent callbacks + + + +Note that some of these callbacks are complementary: + +- if `onBegin` was called, it is guaranteed that `onFinalize` will be called later. +- if `onActivate` was called, it is guaranteed that `onDeactivate` will be called later. + +### TouchEvent callbacks + + + +## Callbacks + +### onBegin + +```ts +onBegin: (event: GestureEvent) => void +``` + +Called when a handler begins to recognize gestures. If `onBegin` was called, it is guaranteed that `onFinalize` will be called later. + +### onActivate + +```ts +onActivate: (event: GestureEvent) => void +``` + +Called when activation criteria for handler are met. If `onActivate` was called, it is guaranteed that `onDeactivate` will be called later. + +### onUpdate + +```ts +onUpdate: (event: GestureEvent) => void +``` + +Called each time a pointer tracked by the gesture changes state, typically due to movement, after the gesture has been activated. + +### onDeactivate + +```ts +onDeactivate: (event: GestureEvent, didSucceed: boolean) => void +``` + +Called after when handler stops recognizing gestures, but only if handler activated. It is called before `onFinalize`. If the handler was interrupted, the `didSucceed` argument is set to `false`. Otherwise it is set to `true`. + +### onFinalize + +```ts +onFinalize: (event: GestureEvent, didSucceed: boolean) => void +``` + +Called when handler stops recognizing gestures. If handler managed to activate, the `didSucceed` argument is set to `true` and `onFinalize` will be called right after `onDeactivate`. Otherwise it is set to `false`. + +### onTouchesDown + +```ts +onTouchesDown: (event: GestureTouchEvent) => void +``` + +Called when new pointers are placed on the screen. It may carry information about more than one pointer because the events are batched. + +### onTouchesMove + +```ts +onTouchesMove: (event: GestureTouchEvent) => void +``` + +Called when pointers are moved on the screen. It may carry information about more than one pointer because the events are batched. + +### onTouchesUp + +```ts +onTouchesUp: (event: GestureTouchEvent) => void +``` + +Called when pointers are lifted from the screen. It may carry information about more than one pointer because the events are batched. + +### onTouchesCancelled + +```ts +onTouchesCancelled: (event: GestureTouchEvent) => void +``` + +Called when there will be no more information about this pointer. It may be called because the gesture has ended or was interrupted. It may carry information about more than one pointer because the events are batched. + +## Events + +### GestureEvent + + = { + handlerTag: number; + numberOfPointers: number; + pointerType: PointerType; +} & HandlerData; + +export enum PointerType { + TOUCH, + STYLUS, + MOUSE, + KEY, + OTHER, +} +`}/> + +`GestureEvent` contains properties common to all gestures (`handlerTag`, `numberOfPointers`, `pointerType`) along with gesture-specific data defined in each gesture's documentation. + +### TouchEvent + + + +`TouchEvent` carries information about raw touch events, like touching the screen or moving the finger. diff --git a/packages/docs-gesture-handler/docs/fundamentals/state-manager.mdx b/packages/docs-gesture-handler/docs/fundamentals/state-manager.mdx new file mode 100644 index 0000000000..fb31dc06db --- /dev/null +++ b/packages/docs-gesture-handler/docs/fundamentals/state-manager.mdx @@ -0,0 +1,160 @@ +--- +id: state-manager +title: State manager +sidebar_label: State manager +sidebar_position: 6 +--- + +import CollapsibleCode from '@site/src/components/CollapsibleCode'; + +RNGH3 allows to manually control gestures lifecycle by using `GestureStateManager`. + +## State management + +Manual state management is based on `handlerTag`. There are two ways of manual state control. + +### Inside gesture definition + +If you want to manipulate gesture's state in its callbacks, you can get `handlerTag` from event parameter. + + { + // highlight-next-line + GestureStateManager.activate(e.handlerTag); + }, + onActivate: () => { + console.log('LongPress activated!'); + }, + }); + + return ( + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'space-around', + }, + + box: { + width: 150, + height: 150, + backgroundColor: 'blue', + }, +}); +`}/> + + +### Outside gesture definition + +If you want to control gesture lifecycle outside of it, you can use `handlerTag` from created gesture object. + + { + console.log('Pan activated!'); + }, + }); + + const longPress = useLongPressGesture({ + onActivate: () => { + // highlight-next-line + GestureStateManager.activate(pan.handlerTag); + }, + }); + + return ( + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'space-around', + }, + + box: { + width: 150, + height: 150, + backgroundColor: 'blue', + }, +}); +`}/> + +## Methods + +### begin + +```tsx +begin: (handlerTag: number) => void; +``` + +Triggers [`onBegin`](/docs/fundamentals/callbacks-events#onbegin) callback on gesture with specified `handlerTag`. + +### activate + +```tsx +activate: (handlerTag: number) => void; +``` + +Triggers [`onActivate`](/docs/fundamentals/callbacks-events#onactivate) callback on gesture with specified `handlerTag`. + +### deactivate + +```tsx +deactivate: (handlerTag: number) => void; +``` + +If the gesture had activated, it triggers the [`onDeactivate`](/docs/fundamentals/callbacks-events#ondeactivate) callback. It also triggers the [`onFinalize`](/docs/fundamentals/callbacks-events#onfinalize) callback on gesture with the specified `handlerTag`. + +### fail + +```tsx +fail: (handlerTag: number) => void; +``` + +Triggers [`onFinalize`](/docs/fundamentals/callbacks-events#onfinalize) callback on gesture with specified `handlerTag`. If gesture had activated, it will also trigger [`onDeactivate`](/docs/fundamentals/callbacks-events#ondeactivate) callback. diff --git a/packages/docs-gesture-handler/docs/fundamentals/states-events.mdx b/packages/docs-gesture-handler/docs/fundamentals/states-events.mdx deleted file mode 100644 index a2b9befeb4..0000000000 --- a/packages/docs-gesture-handler/docs/fundamentals/states-events.mdx +++ /dev/null @@ -1,89 +0,0 @@ ---- -id: states-events -title: Gesture states & events -sidebar_label: Gesture states & events -sidebar_position: 7 ---- - -Every gesture can be treated as ["state machine"](https://en.wikipedia.org/wiki/Finite-state_machine). -At any given time, each handler instance has an assigned state that can change when new touch events occur or can be forced to change by the touch system in certain circumstances. - -A gesture can be in one of the six possible states: - -- #### UNDETERMINED - - This is the initial state of each gesture recognizer and it goes into this state after it's done recognizing a gesture. - -- #### FAILED - - A gesture recognizer received some touches but for some reason didn't recognize them. For example, if a finger travels more distance than a defined `maxDist` property allows, then the gesture won't become active but will fail instead. Afterwards, it's state will be reset to `UNDETERMINED`. - -- #### BEGAN - - Gesture recognizer has started receiving touch stream but hasn't yet received enough data to either [fail](#failed) or [activate](#active). - -- #### CANCELLED - - The gesture recognizer has received a signal (possibly new touches or a command from the touch system controller) resulting in the cancellation of a continuous gesture. The gesture's state will become `CANCELLED` until it is finally reset to the initial state, `UNDETERMINED`. - -- #### ACTIVE - - Recognizer has recognized a gesture. It will become and stay in the `ACTIVE` state until the gesture finishes (e.g. when user lifts the finger) or gets cancelled by the touch system. Under normal circumstances the state will then turn into `END`. In the case that a gesture is cancelled by the touch system, its state would then become `CANCELLED`. - -- #### END - - The gesture recognizer has received touches signalling the end of a gesture. Its state will become `END` until it is reset to `UNDETERMINED`. - -## State flows - -The most typical flow of state is when a gesture picks up on an initial touch event, then recognizes it, then acknowledges its ending and resets itself back to the initial state. - -The flow looks as follows: - -import GestureStateFlowExample from '@site/src/examples/GestureStateFlowExample'; - -} - label="Drag or long-press the circle" - larger={true} -/> - -## Events - -There are three types of events in RNGH2: `StateChangeEvent`, `GestureEvent` and `PointerEvent`. The `StateChangeEvent` is send every time a gesture moves to a different state, while `GestureEvent` is send every time a gesture is updated. The first two carry a gesture-specific data and a `state` property, indicating the current state of the gesture. `StateChangeEvent` also carries a `oldState` property indicating the previous state of the gesture. `PointerEvent` carries information about raw touch events, like touching the screen or moving the finger. These events are handled internally before they are passed along to the correct callbacks: - -### `onBegin` - -Is called when a gesture transitions to the [`BEGAN`](#began) state. - -### `onStart` - -Is called when a gesture transitions to the [`ACTIVE`](#active) state. - -### `onEnd` - -Is called when a gesture transitions from the [`ACTIVE`](#active) state to the [`END`](#end), [`FAILED`](#failed), or [`CANCELLED`](#cancelled) state. If the gesture transitions to the [`END`](#end) state, the `success` argument is set to `true` otherwise it is set to `false`. - -### `onFinalize` - -Is called when a gesture transitions to the [`END`](#end), [`FAILED`](#failed), or [`CANCELLED`](#cancelled) state. If the gesture transitions to the [`END`](#end) state, the `success` argument is set to `true` otherwise it is set to `false`. If the gesture transitions from the [`ACTIVE`](#active) state, it will be called after `onEnd`. - -### `onUpdate` - -Is called every time a gesture is updated while it is in the [`ACTIVE`](#active) state. - -### `onPointerDown` - -Is called when new pointers are placed on the screen. It may carry information about more than one pointer because the events are batched. - -### `onPointerMove` - -Is called when pointers are moved on the screen. It may carry information about more than one pointer because the events are batched. - -### `onPointerUp` - -Is called when pointers are lifted from the screen. It may carry information about more than one pointer because the events are batched. - -### `onPointerCancelled` - -Is called when there will be no more information about this pointer. It may be called because the gesture has ended or was interrupted. It may carry information about more than one pointer because the events are batched. diff --git a/packages/docs-gesture-handler/docs/guides/manual-gestures/_steps/step1.md b/packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step1.md similarity index 63% rename from packages/docs-gesture-handler/docs/guides/manual-gestures/_steps/step1.md rename to packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step1.md index 4c6331c1c8..040cb97cb3 100644 --- a/packages/docs-gesture-handler/docs/guides/manual-gestures/_steps/step1.md +++ b/packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step1.md @@ -1,7 +1,7 @@ -```jsx -interface Pointer { - visible: boolean; +```tsx +type Pointer = { x: number; y: number; -} + visible: boolean; +}; ``` diff --git a/packages/docs-gesture-handler/docs/guides/manual-gestures/_steps/step2.md b/packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step2.md similarity index 75% rename from packages/docs-gesture-handler/docs/guides/manual-gestures/_steps/step2.md rename to packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step2.md index 40079ee2bd..d02ade2d45 100644 --- a/packages/docs-gesture-handler/docs/guides/manual-gestures/_steps/step2.md +++ b/packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step2.md @@ -1,14 +1,22 @@ -```jsx +```tsx import { StyleSheet } from 'react-native'; import Animated, { useAnimatedStyle, - useSharedValue, + SharedValue, } from 'react-native-reanimated'; -function PointerElement(props: { - pointer: Animated.SharedValue, - active: Animated.SharedValue, -}) { +type Pointer = { + x: number; + y: number; + visible: boolean; +}; + +type PointerElementProps = { + pointer: SharedValue; + active: SharedValue; +}; + +function PointerElement(props: PointerElementProps) { const animatedStyle = useAnimatedStyle(() => ({ transform: [ { translateX: props.pointer.value.x }, @@ -25,8 +33,6 @@ function PointerElement(props: { return ; } -// ... - const styles = StyleSheet.create({ pointer: { width: 60, diff --git a/packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step3.md b/packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step3.md new file mode 100644 index 0000000000..3a9160e04a --- /dev/null +++ b/packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step3.md @@ -0,0 +1,55 @@ +```tsx +import { StyleSheet } from 'react-native'; +import { + GestureDetector, + GestureHandlerRootView, + GestureStateManager, + useManualGesture, +} from 'react-native-gesture-handler'; +import Animated, { + useAnimatedStyle, + SharedValue, + useSharedValue, +} from 'react-native-reanimated'; + +... + +export default function Example() { + const trackedPointers: SharedValue[] = []; + const active = useSharedValue(false); + + for (let i = 0; i < 10; i++) { + trackedPointers[i] = useSharedValue({ + x: 0, + y: 0, + visible: false, + }); + } + + const gesture = useManualGesture({}); + + return ( + + + + {trackedPointers.map((pointer, index) => ( + + ))} + + + + ); +} + +const styles = StyleSheet.create({ + pointer: { + width: 60, + height: 60, + borderRadius: 30, + backgroundColor: 'red', + position: 'absolute', + marginStart: -30, + marginTop: -30, + }, +}); +``` diff --git a/packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step4.md b/packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step4.md new file mode 100644 index 0000000000..8a8e773ed1 --- /dev/null +++ b/packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step4.md @@ -0,0 +1,17 @@ +```tsx +const gesture = useManualGesture({ + onTouchesDown: (e) => { + for (const touch of e.changedTouches) { + trackedPointers[touch.id].value = { + x: touch.x, + y: touch.y, + visible: true, + }; + } + + if (e.numberOfTouches >= 2) { + GestureStateManager.activate(e.handlerTag); + } + }, +}); +``` diff --git a/packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step5.md b/packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step5.md new file mode 100644 index 0000000000..c27d7cb5bb --- /dev/null +++ b/packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step5.md @@ -0,0 +1,14 @@ +```tsx +const gesture = useManualGesture({ + ... + onTouchesMove: (e) => { + for (const touch of e.changedTouches) { + trackedPointers[touch.id].value = { + x: touch.x, + y: touch.y, + visible: true, + }; + } + }, +}); +``` diff --git a/packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step6.md b/packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step6.md new file mode 100644 index 0000000000..c232d392ff --- /dev/null +++ b/packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step6.md @@ -0,0 +1,18 @@ +```tsx +const gesture = useManualGesture({ + ... + onTouchesUp: (e) => { + for (const touch of e.changedTouches) { + trackedPointers[touch.id].value = { + x: touch.x, + y: touch.y, + visible: false, + }; + } + + if (e.numberOfTouches === 0) { + GestureStateManager.deactivate(e.handlerTag); + } + }, +}); +``` diff --git a/packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step7.md b/packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step7.md new file mode 100644 index 0000000000..c9da993477 --- /dev/null +++ b/packages/docs-gesture-handler/docs/gestures/_manual_gesture_steps/step7.md @@ -0,0 +1,10 @@ +```tsx +const gesture = useManualGesture({ + ... + onActivate: () => { + active.value = true; + }, + onDeactivate: () => { + active.value = false; + }, +``` diff --git a/packages/docs-gesture-handler/docs/gestures/_shared/base-continuous-gesture-config.md b/packages/docs-gesture-handler/docs/gestures/_shared/base-continuous-gesture-config.md index 00b24ecba9..c4b1b4056e 100644 --- a/packages/docs-gesture-handler/docs/gestures/_shared/base-continuous-gesture-config.md +++ b/packages/docs-gesture-handler/docs/gestures/_shared/base-continuous-gesture-config.md @@ -4,4 +4,4 @@ manualActivation: boolean | SharedValue; ``` -When `true` the handler will not activate by itself even if its activation criteria are met. Instead you can manipulate its state using [state manager](/docs/gestures/state-manager/). +When `true` the handler will not activate by itself even if its activation criteria are met. Instead you can manipulate its state using [state manager](/docs/fundamentals/state-manager/). diff --git a/packages/docs-gesture-handler/docs/gestures/_shared/base-gesture-config.md b/packages/docs-gesture-handler/docs/gestures/_shared/base-gesture-config.md index 6134c89980..4c541a815c 100644 --- a/packages/docs-gesture-handler/docs/gestures/_shared/base-gesture-config.md +++ b/packages/docs-gesture-handler/docs/gestures/_shared/base-gesture-config.md @@ -5,8 +5,8 @@ enabled: boolean | SharedValue; ``` Indicates whether the given handler should be analyzing stream of touch events or not. -When set to `false` we can be sure that the handler's state will **never** become [`ACTIVE`](/docs/fundamentals/states-events#active). -If the value gets updated while the handler already started recognizing a gesture, then the handler's state it will immediately change to [`FAILED`](/docs/fundamentals/states-events#failed) or [`CANCELLED`](/docs/fundamentals/states-events#cancelled) (depending on its current state). +When set to `false` we can be sure that the handler will **never** activate. +If the value gets updated while the handler already started recognizing a gesture, then the handler will stop processing gestures immediately. Default value is `true`. ### shouldCancelWhenOutside @@ -15,7 +15,7 @@ Default value is `true`. shouldCancelWhenOutside: boolean | SharedValue; ``` -When `true` the handler will [cancel](/docs/fundamentals/states-events#cancelled) or [fail](/docs/fundamentals/states-events#failed) recognition (depending on its current state) whenever the finger leaves the area of the connected view. +When `true` the handler will stop recognition whenever the finger leaves the area of the connected view. Default value of this property is different depending on the handler type. Most handlers' `shouldCancelWhenOutside` property defaults to `false` except for the [`LongPressGesture`](/docs/gestures/use-long-press-gesture) and [`TapGesture`](/docs/gestures/use-tap-gesture) which default to `true`. @@ -42,7 +42,7 @@ type HitSlop = | Record<'height' | 'bottom', number>; ``` -This parameter enables control over what part of the connected view area can be used to [begin](/docs/fundamentals/states-events#began) recognizing the gesture. +This parameter enables control over what part of the connected view area can be used to begin recognizing the gesture. When a negative number is provided the bounds of the view will reduce the area by the given number of points in each of the sides evenly. Instead you can pass an object to specify how each boundary side should be reduced by providing different number of points for `left`, `right`, `top` or `bottom` sides. @@ -69,7 +69,7 @@ cancelsTouchesInView: boolean | SharedValue; ``` Accepts a boolean value. -When `true`, the gesture will cancel touches for native UI components (`UIButton`, `UISwitch`, etc) it's attached to when it becomes [`ACTIVE`](/docs/fundamentals/states-events#active). +When `true`, the gesture will cancel touches for native UI components (`UIButton`, `UISwitch`, etc) it's attached to upon activation. Default value is `true`. ### runOnJS diff --git a/packages/docs-gesture-handler/docs/gestures/state-manager.md b/packages/docs-gesture-handler/docs/gestures/state-manager.md deleted file mode 100644 index f4c39cdfdc..0000000000 --- a/packages/docs-gesture-handler/docs/gestures/state-manager.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -id: state-manager -title: Gesture state manager -sidebar_label: Gesture state manager -sidebar_position: 15 ---- - -`GestureStateManager` allows to manually control the state of the gestures. Please note that `react-native-reanimated` is required to use it, since it allows for synchronously executing methods in worklets. - -## Methods - -### `begin()` - -Transition the gesture to the [`BEGAN`](/docs/2.x/fundamentals/states-events#began) state. This method will have no effect if the gesture has already activated or finished. - -### `activate()` - -Transition the gesture to the [`ACTIVE`](/docs/2.x/fundamentals/states-events#active) state. This method will have no effect if the handler is already active, or has finished. -If the gesture is [`exclusive`](/docs/2.x/fundamentals/gesture-composition) with another one, the activation will be delayed until the gesture with higher priority fails. - -### `end()` - -Transition the gesture to the [`END`](/docs/2.x/fundamentals/states-events#end) state. This method will have no effect if the handler has already finished. - -### `fail()` - -Transition the gesture to the [`FAILED`](/docs/2.x/fundamentals/states-events#failed) state. This method will have no effect if the handler has already finished. diff --git a/packages/docs-gesture-handler/docs/gestures/use-fling-gesture.mdx b/packages/docs-gesture-handler/docs/gestures/use-fling-gesture.mdx index 5cb582966a..4aa6c21118 100644 --- a/packages/docs-gesture-handler/docs/gestures/use-fling-gesture.mdx +++ b/packages/docs-gesture-handler/docs/gestures/use-fling-gesture.mdx @@ -31,8 +31,8 @@ import BaseGestureCallbacks from './\_shared/base-gesture-callbacks.mdx'; import SharedValueInfo from './\_shared/shared-value-info.md'; A discrete gesture that activates when the movement is sufficiently long and fast. -Gesture gets [ACTIVE](/docs/fundamentals/states-events#active) when movement is sufficiently long and it does not take too much time. -When gesture gets activated it will turn into [END](/docs/fundamentals/states-events#end) state when finger is released. +Gesture activates when movement is sufficiently long and it does not take too much time. +When gesture gets activated it will end when finger is released. The gesture will fail to recognize if the finger is lifted before being activated.
diff --git a/packages/docs-gesture-handler/docs/gestures/use-long-press-gesture.mdx b/packages/docs-gesture-handler/docs/gestures/use-long-press-gesture.mdx index 69c832b82f..811b2dd365 100644 --- a/packages/docs-gesture-handler/docs/gestures/use-long-press-gesture.mdx +++ b/packages/docs-gesture-handler/docs/gestures/use-long-press-gesture.mdx @@ -31,7 +31,7 @@ import BaseGestureCallbacks from './\_shared/base-gesture-callbacks.mdx'; import SharedValueInfo from './\_shared/shared-value-info.md'; A discrete gesture that activates when the corresponding view is pressed for a sufficiently long time. -This gesture's state will turn into [END](/docs/fundamentals/states-events#end) immediately after the finger is released. +This gesture's state will end immediately after the finger is released. The gesture will fail to recognize a touch event if the finger is lifted before the minimum required time or if the finger is moved further than the allowable distance.
@@ -102,7 +102,7 @@ Minimum time, expressed in milliseconds, that a finger must remain pressed on th maxDistance: number | SharedValue; ``` -Maximum distance, expressed in points, that defines how far the finger is allowed to travel during a long press gesture. If the finger travels further than the defined distance and the gesture hasn't yet [activated](/docs/fundamentals/states-events#active), it will fail to recognize the gesture. The default value is 10. +Maximum distance, expressed in points, that defines how far the finger is allowed to travel during a long press gesture. If the finger travels further than the defined distance and the gesture hasn't yet activated, it will fail to recognize the gesture. The default value is 10. ### mouseButton (Web & Android only) diff --git a/packages/docs-gesture-handler/docs/gestures/use-manual-gesture.mdx b/packages/docs-gesture-handler/docs/gestures/use-manual-gesture.mdx index b4c5545785..edbf89b89b 100644 --- a/packages/docs-gesture-handler/docs/gestures/use-manual-gesture.mdx +++ b/packages/docs-gesture-handler/docs/gestures/use-manual-gesture.mdx @@ -11,32 +11,197 @@ import BaseGestureCallbacks from './\_shared/base-gesture-callbacks.mdx'; import BaseContinuousGestureCallbacks from './\_shared/base-continuous-gesture-callbacks.mdx'; import SharedValueInfo from './\_shared/shared-value-info.md'; +import Step, { Divider } from '@site/src/theme/Step'; +import Step1 from './\_manual_gesture_steps/step1.md'; +import Step2 from './\_manual_gesture_steps/step2.md'; +import Step3 from './\_manual_gesture_steps/step3.md'; +import Step4 from './\_manual_gesture_steps/step4.md'; +import Step5 from './\_manual_gesture_steps/step5.md'; +import Step6 from './\_manual_gesture_steps/step6.md'; +import Step7 from './\_manual_gesture_steps/step7.md'; -A plain gesture that has no specific activation criteria nor event data set. Its state has to be controlled manually using a [state manager](/docs/gestures/state-manager). It will not fail when all the pointers are lifted from the screen. + +A plain gesture that has no specific activation criteria nor event data set. Its state has to be controlled manually using a [state manager](/docs/fundamentals/state-manager). It will not fail when all the pointers are lifted from the screen. + +:::tip +If you need to modify the activation criteria of gestures other than `Manual`, check their configuration for relevant properties or use `manualActivation` to manage their state directly. +::: ## Example -```jsx +To demonstrate how to make a manual gesture we will make a simple one that tracks all pointers on the screen. + + + First, we need a way to store information about the pointer: whether it should be visible and its position. + + + + + We also need a component to mark where a pointer is. In order to accomplish that we will make a component that accepts two [shared values](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#shared-value): one holding information about the pointer using the interface we just created, the other holding a bool indicating whether the gesture has activated. + In this example when the gesture is not active, the ball representing it will be blue and when it is active the ball will be red and slightly bigger. + + + + + Now we have to make a component that will handle the gesture and draw all the pointer indicators. We will store data about pointers in an array and render them inside an [`Animated.View`](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/your-first-animation#using-an-animated-component). + + + + + We have our components set up and we can finally get to making the gesture! We will start with `onTouchesDown` where we need to set position of the pointers and make them visible. We can get this information from the touches property of the event. In this case we will also check how many pointers are on the screen and activate the gesture if there are at least two. + + + + + Next, we will handle pointer movement. In `onTouchesMove` we will simply update the position of moved pointers. + + + + + We also need to handle lifting fingers from the screen, which corresponds to `onTouchesUp`. Here we will just hide the pointers that were lifted and end the gesture if there are no more pointers on the screen. + Note that we are not handling `onTouchesCancelled` as in this very basic case we don't expect it to happen, however you should clear data about cancelled pointers (most of the time all active ones) when it is called. + + + + + Now that our pointers are being tracked correctly and we have the state management, we can handle activation and ending of the gesture. In our case, we will simply set the active shared value either to `true` or `false`. + + + +
+Full example code + +```tsx +import { StyleSheet } from 'react-native'; import { GestureDetector, GestureHandlerRootView, + GestureStateManager, useManualGesture, } from 'react-native-gesture-handler'; -import Animated from 'react-native-reanimated'; +import Animated, { + useAnimatedStyle, + SharedValue, + useSharedValue, +} from 'react-native-reanimated'; + +type Pointer = { + x: number; + y: number; + visible: boolean; +}; + +type PointerElementProps = { + pointer: SharedValue; + active: SharedValue; +}; -export default function App() { - const manualGesture = useManualGesture({}); +function PointerElement(props: PointerElementProps) { + const animatedStyle = useAnimatedStyle(() => ({ + transform: [ + { translateX: props.pointer.value.x }, + { translateY: props.pointer.value.y }, + { + scale: + (props.pointer.value.visible ? 1 : 0) * + (props.active.value ? 1.3 : 1), + }, + ], + backgroundColor: props.active.value ? 'red' : 'blue', + })); + + return ; +} + +export default function Example() { + const trackedPointers: SharedValue[] = []; + const active = useSharedValue(false); + + for (let i = 0; i < 10; i++) { + trackedPointers[i] = useSharedValue({ + x: 0, + y: 0, + visible: false, + }); + } + + const gesture = useManualGesture({ + onTouchesDown: (e) => { + for (const touch of e.changedTouches) { + trackedPointers[touch.id].value = { + x: touch.x, + y: touch.y, + visible: true, + }; + } + + if (e.numberOfTouches >= 2) { + GestureStateManager.activate(e.handlerTag); + } + }, + onTouchesMove: (e) => { + for (const touch of e.changedTouches) { + trackedPointers[touch.id].value = { + x: touch.x, + y: touch.y, + visible: true, + }; + } + }, + onTouchesUp: (e) => { + for (const touch of e.changedTouches) { + trackedPointers[touch.id].value = { + x: touch.x, + y: touch.y, + visible: false, + }; + } + + if (e.numberOfTouches === 0) { + GestureStateManager.deactivate(e.handlerTag); + } + }, + onActivate: () => { + active.value = true; + }, + onDeactivate: () => { + active.value = false; + }, + }); return ( - - + + + {trackedPointers.map((pointer, index) => ( + + ))} + ); } + +const styles = StyleSheet.create({ + pointer: { + width: 60, + height: 60, + borderRadius: 30, + backgroundColor: 'red', + position: 'absolute', + marginStart: -30, + marginTop: -30, + }, +}); ``` +
+ + +## Modifying existing gestures + +While manual gestures open great possibilities we are aware that reimplementing pinch or rotation from scratch just because you need to activate in specific circumstances or require position of the fingers, would be a waste of time as those gestures are already available. Therefore, you can use touch events with every gesture to extract more detailed information about the gesture than what the basic events alone provide. We also added a `manualActivation` modifier on all continuous gestures, which prevents the gesture it is applied to from activating automatically, giving you full control over its behavior. + ## Config diff --git a/packages/docs-gesture-handler/docs/gestures/use-native-gesture.mdx b/packages/docs-gesture-handler/docs/gestures/use-native-gesture.mdx index 509a8ae0eb..73f525c703 100644 --- a/packages/docs-gesture-handler/docs/gestures/use-native-gesture.mdx +++ b/packages/docs-gesture-handler/docs/gestures/use-native-gesture.mdx @@ -97,7 +97,7 @@ Do not use `Native` gesture with components exported by React Native Gesture Han shouldActivateOnStart: boolean | SharedValue; ``` -When `true`, underlying handler will activate unconditionally when it receives any touches in [`BEGAN`](/docs/fundamentals/states-events#began) or [`UNDETERMINED`](/docs/fundamentals/states-events#undetermined) state. +When `true`, underlying handler will activate unconditionally when it receives any touches. ### disallowInterruption @@ -105,7 +105,7 @@ When `true`, underlying handler will activate unconditionally when it receives a disallowInterruption: boolean | SharedValue; ``` -When `true`, cancels all other gesture handlers when changes its state to [`ACTIVE`](/docs/fundamentals/states-events#active). +When `true`, cancels all other gesture handlers when activates. diff --git a/packages/docs-gesture-handler/docs/gestures/use-pan-gesture.mdx b/packages/docs-gesture-handler/docs/gestures/use-pan-gesture.mdx index d662dddd87..c58c470323 100644 --- a/packages/docs-gesture-handler/docs/gestures/use-pan-gesture.mdx +++ b/packages/docs-gesture-handler/docs/gestures/use-pan-gesture.mdx @@ -34,7 +34,7 @@ import SharedValueInfo from './\_shared/shared-value-info.md'; A continuous gesture that can recognize a panning (dragging) gesture and track its movement. -The gesture [activates](/docs/fundamentals/states-events#active) when a finger is placed on the screen and moved some initial distance. +The gesture activates when a finger is placed on the screen and moved some initial distance. Configurations such as a minimum initial distance, specific vertical or horizontal pan detection and number of fingers required for activation (allowing for multifinger swipes) may be specified. @@ -128,7 +128,7 @@ If you wish to track the "center of mass" virtual pointer and account for its ch minDistance: number | SharedValue; ``` -Minimum distance the finger (or multiple finger) need to travel before the gesture [activates](/docs/fundamentals/states-events#active). Expressed in points. +Minimum distance the finger (or multiple finger) need to travel before the gesture activates. Expressed in points. ### minPointers @@ -136,7 +136,7 @@ Minimum distance the finger (or multiple finger) need to travel before the gestu minPointers: number | SharedValue; ``` -A number of fingers that is required to be placed before gesture can [activate](/docs/fundamentals/states-events#active). Should be a higher or equal to 0 integer. +A number of fingers that is required to be placed before gesture can activate. Should be a higher or equal to 0 integer. ### maxPointers @@ -144,7 +144,7 @@ A number of fingers that is required to be placed before gesture can [activate]( maxPointers: number | SharedValue; ``` -When the given number of fingers is placed on the screen and gesture hasn't yet [activated](/docs/fundamentals/states-events#active) it will fail recognizing the gesture. Should be a higher or equal to 0 integer. +When the given number of fingers is placed on the screen and gesture hasn't yet activated it will fail recognizing the gesture. Should be a higher or equal to 0 integer. ### activateAfterLongPress @@ -152,7 +152,7 @@ When the given number of fingers is placed on the screen and gesture hasn't yet activateAfterLongPress: number | SharedValue; ``` -Duration in milliseconds of the `LongPress` gesture before `Pan` is allowed to [activate](/docs/fundamentals/states-events#active). If the finger is moved during that period, the gesture will [fail](/docs/fundamentals/states-events#failed). Should be a higher or equal to 0 integer. Default value is 0, meaning no `LongPress` is required to [activate](/docs/fundamentals/states-events#active) the `Pan`. +Duration in milliseconds of the `LongPress` gesture before `Pan` is allowed to activate. If the finger is moved during that period, the gesture will fail. Should be a higher or equal to 0 integer. Default value is 0, meaning no `LongPress` is required to activate the `Pan`. ### activeOffsetX diff --git a/packages/docs-gesture-handler/docs/gestures/use-pinch-gesture.mdx b/packages/docs-gesture-handler/docs/gestures/use-pinch-gesture.mdx index 51fb80760a..8b730dde13 100644 --- a/packages/docs-gesture-handler/docs/gestures/use-pinch-gesture.mdx +++ b/packages/docs-gesture-handler/docs/gestures/use-pinch-gesture.mdx @@ -33,7 +33,7 @@ import BaseContinuousGestureCallbacks from './\_shared/base-continuous-gesture-c import SharedValueInfo from './\_shared/shared-value-info.md'; A continuous gesture that recognizes pinch gesture. It allows for tracking the distance between two fingers and use that information to scale or zoom your content. -The gesture [activates](/docs/fundamentals/states-events#active) when fingers are placed on the screen and change their position. +The gesture activates when fingers are placed on the screen and move away from each other or pull closer together. Gesture callback can be used for continuous tracking of the pinch gesture. It provides information about velocity, anchor (focal) point of gesture and scale. The distance between the fingers is reported as a scale factor. At the beginning of the gesture, the scale factor is 1.0. As the distance between the two fingers increases, the scale factor increases proportionally. diff --git a/packages/docs-gesture-handler/docs/gestures/use-rotation-gesture.mdx b/packages/docs-gesture-handler/docs/gestures/use-rotation-gesture.mdx index 7df09148c1..6f74aabf3c 100644 --- a/packages/docs-gesture-handler/docs/gestures/use-rotation-gesture.mdx +++ b/packages/docs-gesture-handler/docs/gestures/use-rotation-gesture.mdx @@ -34,7 +34,7 @@ import SharedValueInfo from './\_shared/shared-value-info.md'; A continuous gesture that can recognize a rotation gesture and track its movement. -The gesture [activates](/docs/fundamentals/states-events#active) when fingers are placed on the screen and change position in a proper way. +The gesture activates when fingers are placed on the screen and rotate around a common point. Gesture callback can be used for continuous tracking of the rotation gesture. It provides information about the gesture such as the amount rotated, the focal point of the rotation (anchor), and its instantaneous velocity. diff --git a/packages/docs-gesture-handler/docs/gestures/use-tap-gesture.mdx b/packages/docs-gesture-handler/docs/gestures/use-tap-gesture.mdx index fc61c590b9..25acce6956 100644 --- a/packages/docs-gesture-handler/docs/gestures/use-tap-gesture.mdx +++ b/packages/docs-gesture-handler/docs/gestures/use-tap-gesture.mdx @@ -37,7 +37,7 @@ The pointers involved in these gestures must not move significantly from their i The required number of taps and allowed distance from initial position may be configured. For example, you might configure tap gesture recognizers to detect single taps, double taps, or triple taps. -In order for a gesture to [activate](/docs/fundamentals/states-events#active), specified gesture requirements such as minPointers, numberOfTaps, maxDistance, maxDuration, and maxDelay (explained below) must be met. Immediately after the gesture [activates](/docs/fundamentals/states-events#active), it will [end](/docs/fundamentals/states-events#end). +In order for a gesture to activate, specified gesture requirements such as minPointers, numberOfTaps, maxDistance, maxDuration, and maxDelay (explained below) must be met. Immediately after the gesture activates, it will end.