Skip to content

Commit

Permalink
feat: Add simultaneousWithExternalGesture to ReanimatedSwipable (#3324
Browse files Browse the repository at this point in the history
)

## 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

<!--
Description and motivation for this PR.

Include 'Fixes #<number>' if this is fixing some issue.
-->

## 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

<!--
Describe how did you test this change here.
-->
  • Loading branch information
MrSltun authored Jan 27, 2025
1 parent ce10992 commit 5b535c9
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 65 deletions.
16 changes: 16 additions & 0 deletions docs/docs/components/reanimated_swipeable.md
Original file line number Diff line number Diff line change
Expand Up @@ -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();

<GestureDetector gesture={panGesture}>
<ReanimatedSwipeable simultaneousWithExternalGesture={panGesture} />
</GestureDetector>
```

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.
Expand Down
3 changes: 3 additions & 0 deletions example/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
},
Expand Down
161 changes: 96 additions & 65 deletions src/components/ReanimatedSwipeable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -202,6 +203,14 @@ export interface SwipeableProps
* apply `flex: 1`
*/
childrenContainerStyle?: StyleProp<ViewStyle>;

/**
* 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<GestureRef, number>
| Exclude<GestureRef, number>[];
}

export interface SwipeableMethods {
Expand Down Expand Up @@ -247,6 +256,7 @@ const Swipeable = forwardRef<SwipeableMethods, SwipeableProps>(
onSwipeableClose,
renderLeftActions,
renderRightActions,
simultaneousWithExternalGesture,
...remainingProps
} = props;

Expand Down Expand Up @@ -635,73 +645,94 @@ const Swipeable = forwardRef<SwipeableMethods, SwipeableProps>(

const dragStarted = useSharedValue<boolean>(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<PanGestureHandlerEventPayload>) => {
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<PanGestureHandlerEventPayload>) => {
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<PanGestureHandlerEventPayload>) => {
handleRelease(event);
}
)
.onFinalize(() => {
dragStarted.value = false;
}),
[
dragOffsetFromLeftEdge,
dragOffsetFromRightEdge,
dragStarted,
enableTrackpadTwoFingerGesture,
enabled,
handleRelease,
onSwipeableCloseStartDrag,
onSwipeableOpenStartDrag,
rowState,
updateAnimatedEvent,
updateElementWidths,
userDrag,
]
);

updateAnimatedEvent();
}
)
.onEnd(
(event: GestureStateChangeEvent<PanGestureHandlerEventPayload>) => {
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]);

Expand Down

0 comments on commit 5b535c9

Please sign in to comment.