From 5b535c9a7507eb72e53c0c99b7fc7be6e420b31c Mon Sep 17 00:00:00 2001 From: Sultan Al-Maari Date: Mon, 27 Jan 2025 15:55:26 +0100 Subject: [PATCH] feat: Add `simultaneousWithExternalGesture` to ReanimatedSwipable (#3324) ## Description This PR adds a new prop to `RenimatedSwipable` that allows adding a gesture config that can be used simultaneously with `ReanimatedSwipeable`'s gesture config. The new prop is called `simultaneousWithExternalGesture`, it uses the the cross-component interaction methodology that's mentioned in the [documentation here](https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#simultaneouswithexternalgesture). I also update the docs for it ### Motivation I've been recently using ReanimatedSwipeable component, and I tried to add an additional gesture config to enable haptic feedback and "release to run function" kind of interaction After looking through the issues in the repo, I found this issue #2862, I tried to apply it, but it turned out that it was only applicable to the Swipable component I tried to find a way to add a gesture config that would work simultaneously with the swipeable one but I couldn't find a way to add it. So I looked through the documentation of RNGH for simultaneous gesture handlers and came up with this solution ## Test plan I already tested it locally on both iOS and Android, and the callbacks work as expected Not sure if there's something else to do here since this is my first contribution --- docs/docs/components/reanimated_swipeable.md | 16 ++ example/tsconfig.json | 3 + src/components/ReanimatedSwipeable.tsx | 161 +++++++++++-------- 3 files changed, 115 insertions(+), 65 deletions(-) diff --git a/docs/docs/components/reanimated_swipeable.md b/docs/docs/components/reanimated_swipeable.md index 2c545a2b93..a6a7aa2e26 100644 --- a/docs/docs/components/reanimated_swipeable.md +++ b/docs/docs/components/reanimated_swipeable.md @@ -128,6 +128,22 @@ style object for the container (`Animated.View`), for example to override `overf style object for the children container (`Animated.View`), for example to apply `flex: 1`. +### `simultaneousWithExternalGesture` + +A gesture configuration to be recognized simultaneously with the swipeable gesture. This is useful for allowing other gestures to work simultaneously with swipeable gesture handler. + +For example, to enable a pan gesture alongside the swipeable gesture: + +```jsx +const panGesture = Gesture.Pan(); + + + + +``` + +More details can be found in the [gesture composition documentation](../fundamentals/gesture-composition.md#simultaneouswithexternalgesture). + ### `enableTrackpadTwoFingerGesture` (iOS only) Enables two-finger gestures on supported devices, for example iPads with trackpads. diff --git a/example/tsconfig.json b/example/tsconfig.json index 9b85006f51..bf0d3c670c 100644 --- a/example/tsconfig.json +++ b/example/tsconfig.json @@ -5,6 +5,9 @@ "baseUrl": ".", "paths": { "react-native-gesture-handler": ["../src/index.ts"], + "react-native-gesture-handler/ReanimatedSwipeable": [ + "../src/components/ReanimatedSwipeable.tsx" + ], "react-native-gesture-handler/jest-utils": ["../src/jestUtils/index.ts"] } }, diff --git a/src/components/ReanimatedSwipeable.tsx b/src/components/ReanimatedSwipeable.tsx index 08e241b101..fc085eef30 100644 --- a/src/components/ReanimatedSwipeable.tsx +++ b/src/components/ReanimatedSwipeable.tsx @@ -9,6 +9,7 @@ import React, { useImperativeHandle, useMemo, } from 'react'; +import { GestureRef } from '../handlers/gestures/gesture'; import { GestureObjects as Gesture } from '../handlers/gestures/gestureObjects'; import { GestureDetector } from '../handlers/gestures/GestureDetector'; import { @@ -202,6 +203,14 @@ export interface SwipeableProps * 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?: + | Exclude + | Exclude[]; } export interface SwipeableMethods { @@ -247,6 +256,7 @@ const Swipeable = forwardRef( onSwipeableClose, renderLeftActions, renderRightActions, + simultaneousWithExternalGesture, ...remainingProps } = props; @@ -635,73 +645,94 @@ const Swipeable = forwardRef( const dragStarted = useSharedValue(false); - const tapGesture = useMemo( - () => - Gesture.Tap() - .shouldCancelWhenOutside(true) - .onStart(() => { - if (rowState.value !== 0) { - close(); - } - }), - [close, rowState] - ); - - const panGesture = useMemo( - () => - 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); - } + const tapGesture = useMemo(() => { + const tap = Gesture.Tap() + .shouldCancelWhenOutside(true) + .onStart(() => { + if (rowState.value !== 0) { + close(); + } + }); + + if (!simultaneousWithExternalGesture) { + return tap; + } + + if (Array.isArray(simultaneousWithExternalGesture)) { + tap.simultaneousWithExternalGesture(...simultaneousWithExternalGesture); + } else { + tap.simultaneousWithExternalGesture(simultaneousWithExternalGesture); + } + + 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; - }), - [ - dragOffsetFromLeftEdge, - dragOffsetFromRightEdge, - dragStarted, - enableTrackpadTwoFingerGesture, - enabled, - handleRelease, - onSwipeableCloseStartDrag, - onSwipeableOpenStartDrag, - rowState, - updateAnimatedEvent, - updateElementWidths, - userDrag, - ] - ); + + updateAnimatedEvent(); + } + ) + .onEnd( + (event: GestureStateChangeEvent) => { + handleRelease(event); + } + ) + .onFinalize(() => { + dragStarted.value = false; + }); + + if (!simultaneousWithExternalGesture) { + return pan; + } + + if (Array.isArray(simultaneousWithExternalGesture)) { + pan.simultaneousWithExternalGesture(...simultaneousWithExternalGesture); + } else { + pan.simultaneousWithExternalGesture(simultaneousWithExternalGesture); + } + + return pan; + }, [ + dragOffsetFromLeftEdge, + dragOffsetFromRightEdge, + dragStarted, + enableTrackpadTwoFingerGesture, + enabled, + handleRelease, + onSwipeableCloseStartDrag, + onSwipeableOpenStartDrag, + rowState, + updateAnimatedEvent, + updateElementWidths, + userDrag, + simultaneousWithExternalGesture, + ]); useImperativeHandle(ref, () => swipeableMethods, [swipeableMethods]);