diff --git a/packages/docs-gesture-handler/docs/components/reanimated_swipeable.mdx b/packages/docs-gesture-handler/docs/components/reanimated_swipeable.mdx index 817ec0310d..9618776f83 100644 --- a/packages/docs-gesture-handler/docs/components/reanimated_swipeable.mdx +++ b/packages/docs-gesture-handler/docs/components/reanimated_swipeable.mdx @@ -16,9 +16,9 @@ This component is a drop-in replacement for the `Swipeable` component, rewritten -Reanimated `Swipeable` is designed for implementing swipeable rows or similar interactions. It places its children inside a pannable container that enables horizontal swiping to the left and right. Depending on the direction of the swipe, one of two "action" containers will be displayed, which can be configured using the [`renderLeftActions`](#renderleftactions) or [`renderRightActions`](#renderrightactions) props. +`ReanimatedSwipeable` is designed for implementing swipeable rows or similar interactions. It places its children inside a pannable container that enables horizontal swiping to the left and right. Depending on the direction of the swipe, one of two "action" containers will be displayed, which can be configured using the [`renderLeftActions`](#renderleftactions) or [`renderRightActions`](#renderrightactions) props. -To use Reanimated `Swipeable`, first ensure that Reanimated is installed and that your app is wrapped in `GestureHandlerRootView`. You can then import it as follows: +To use `ReanimatedSwipeable`, first ensure that Reanimated is installed and that your app is wrapped in `GestureHandlerRootView`. You can then import it as follows: ```ts import Swipeable from 'react-native-gesture-handler/ReanimatedSwipeable'; @@ -55,18 +55,18 @@ Distance from the right edge at which released panel will animate to the open st ### dragOffsetFromLeft ```ts -dragOffsetFromLeft?: number; +dragOffsetFromLeft?: number | SharedValue; ``` -The minimum horizontal distance from the starting point required to trigger a right-swipe gesture. Defaults to `10`. +The horizontal offset from the starting point required to trigger a right-swipe gesture. Defaults to `10`. ### dragOffsetFromRight ```ts -dragOffsetFromRight?: number; +dragOffsetFromRight?: number | SharedValue; ``` -The minimum horizontal distance from the starting point required to trigger a left-swipe gesture. Defaults to `10`. +The horizontal offset from the starting point required to trigger a left-swipe gesture. Defaults to `-10`. ### overshootLeft @@ -240,12 +240,12 @@ blocksExternalGesture?: AnyGesture | AnyGesture[]; Gestures that `Swipeable` will prevent from activating (see [gesture composition section](/docs/fundamentals/gesture-composition#simultaneouswith)). - + ### enableTrackpadTwoFingerGesture ```ts -enableTrackpadTwoFingerGesture?: boolean; +enableTrackpadTwoFingerGesture?: boolean | SharedValue; ``` 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. @@ -253,7 +253,7 @@ Enables two-finger gestures on supported devices, for example iPads with trackpa ### enabled ```ts -enabled: boolean; +enabled?: boolean | SharedValue; ``` Indicates whether `ReanimatedSwipeable` should be analyzing the stream of touch events or not. Defaults to `true`. @@ -261,7 +261,7 @@ Indicates whether `ReanimatedSwipeable` should be analyzing the stream of touch ### testID ```ts -testID: string; +testID?: string; ``` Sets a `testID` property, allowing for querying `ReanimatedSwipeable` for it in tests. @@ -274,7 +274,7 @@ expandedLabel="Hide composed types definitions" lineBounds={[0, 1]} collapsed={false} src={` -hitSlop: HitSlop | SharedValue; +hitSlop?: HitSlop | SharedValue; type HitSlop = | number diff --git a/packages/docs-gesture-handler/docs/guides/upgrading-to-3.mdx b/packages/docs-gesture-handler/docs/guides/upgrading-to-3.mdx index a7a1aeb6c1..cf4c4e6e23 100644 --- a/packages/docs-gesture-handler/docs/guides/upgrading-to-3.mdx +++ b/packages/docs-gesture-handler/docs/guides/upgrading-to-3.mdx @@ -299,6 +299,12 @@ Although the legacy JS implementation of the buttons is still available, they al +### ReanimatedSwipeable + +`ReanimatedSwipeable` has been rewritten using the new hook API. Additionally, `enabled`, `hitSlop`, `enableTrackpadTwoFingerGesture`, `dragOffsetFromLeft` and `dragOffsetFromRight` props now accept [`SharedValues`](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#shared-value). + +`dragOffsetFromRight` now accepts negative values. If you were using it with positive values, make sure to change the sign when migrating. + ### Touchables [`Touchable`](/docs/components/touchable) can also be used as a substitute for old, deprecated `Touchables`. diff --git a/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx b/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx index fa0b0fb82a..3c4d402aec 100644 --- a/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx +++ b/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx @@ -1,5 +1,10 @@ import type { ForwardedRef } from 'react'; -import { useCallback, useImperativeHandle, useMemo } from 'react'; +import React, { + useCallback, + useEffect, + useImperativeHandle, + useMemo, +} from 'react'; import type { LayoutChangeEvent } from 'react-native'; import { I18nManager, StyleSheet, View } from 'react-native'; import Animated, { @@ -14,9 +19,16 @@ import Animated, { withSpring, } from 'react-native-reanimated'; +import { Reanimated } from '../../handlers/gestures/reanimatedWrapper'; +import { tagMessage } from '../../utils'; import { GestureDetector } from '../../v3/detectors'; import type { PanGestureActiveEvent } from '../../v3/hooks/gestures'; import { usePanGesture, useTapGesture } from '../../v3/hooks/gestures'; +import { + maybeUnpackValue, + SHARED_VALUE_OFFSET, +} from '../../v3/hooks/utils/reanimatedUtils'; +import type { SharedValueOrT } from '../../v3/types'; import type { SwipeableMethods, SwipeableProps, @@ -45,7 +57,7 @@ const Swipeable = (props: SwipeableProps) => { children, enableTrackpadTwoFingerGesture = DEFAULT_ENABLE_TRACKING_TWO_FINGER_GESTURE, dragOffsetFromLeft = DEFAULT_DRAG_OFFSET, - dragOffsetFromRight = DEFAULT_DRAG_OFFSET, + dragOffsetFromRight = -DEFAULT_DRAG_OFFSET, friction = DEFAULT_FRICTION, overshootFriction = DEFAULT_OVERSHOOT_FRICTION, onSwipeableOpenStartDrag, @@ -63,6 +75,40 @@ const Swipeable = (props: SwipeableProps) => { ...remainingProps } = props; + if (__DEV__) { + const checkValue = (value: SharedValueOrT) => { + 'worklet'; + if (maybeUnpackValue(value) > 0) { + throw new Error( + tagMessage('dragOffsetFromRight should be non-positive.') + ); + } + }; + + checkValue(dragOffsetFromRight); + + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + if (!Reanimated?.isSharedValue(dragOffsetFromRight)) { + return; + } + + const listenerId = Math.random() + SHARED_VALUE_OFFSET; + + Reanimated?.runOnUI(() => { + 'worklet'; + dragOffsetFromRight.addListener(listenerId, checkValue); + })(); + + return () => { + Reanimated?.runOnUI(() => { + 'worklet'; + dragOffsetFromRight.removeListener(listenerId); + })(); + }; + }, [dragOffsetFromRight, checkValue]); + } + const shouldEnableTap = useSharedValue(false); const rowState = useSharedValue(0); @@ -468,9 +514,9 @@ const Swipeable = (props: SwipeableProps) => { }); const panGesture = usePanGesture({ - enabled: enabled !== false, + enabled: enabled ?? true, enableTrackpadTwoFingerGesture: enableTrackpadTwoFingerGesture, - activeOffsetX: [-dragOffsetFromRight, dragOffsetFromLeft], + activeOffsetX: [dragOffsetFromRight, dragOffsetFromLeft], simultaneousWith, requireToFail, block, diff --git a/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts b/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts index 392e3a44f0..1e1246f709 100644 --- a/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts +++ b/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts @@ -3,17 +3,14 @@ import type { StyleProp, ViewStyle } from 'react-native'; import type { SharedValue } from 'react-native-reanimated'; import type { HitSlop } from '../../handlers/gestureHandlerCommon'; -import type { AnyGesture } from '../../v3/types'; +import type { AnyGesture, WithSharedValue } from '../../v3/types'; export enum SwipeDirection { LEFT = 'left', RIGHT = 'right', } -export interface SwipeableProps { - /** - * - */ +export type SwipeableProps = { ref?: React.Ref; /** @@ -23,27 +20,6 @@ export interface SwipeableProps { children?: React.ReactNode; - /** - * Indicates whether `ReanimatedSwipeable` should be analyzing stream of touch events or not. - * @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture#enabledvalue-boolean - */ - enabled?: boolean; - - /** - * 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. - * @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture#hitslopsettings - */ - hitSlop?: HitSlop; - - /** - * 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 @@ -66,16 +42,6 @@ export interface SwipeableProps { */ rightThreshold?: number; - /** - * The minimum horizontal distance from the starting point required to trigger a right-swipe gesture. Defaults to `10`. - */ - dragOffsetFromLeft?: number; - - /** - * The minimum horizontal distance from the starting point required to trigger a left-swipe gesture. Defaults to `10`. - */ - dragOffsetFromRight?: 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 @@ -202,7 +168,35 @@ export interface SwipeableProps { * used with the swipeable's gesture handler. */ block?: AnyGesture | AnyGesture[]; -} +} & WithSharedValue<{ + /** + * Indicates whether `ReanimatedSwipeable` should be analyzing stream of touch events or not. + * @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture#enabledvalue-boolean + */ + enabled?: boolean | undefined; + /** + * 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. + * @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture#hitslopsettings + */ + hitSlop?: HitSlop | undefined; + /** + * 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 | undefined; + /** + * The horizontal offset from the starting point required to trigger a right-swipe gesture. Defaults to 10. + */ + dragOffsetFromLeft?: number; + + /** + * The horizontal offset from the starting point required to trigger a left-swipe gesture. Defaults to -10. + */ + dragOffsetFromRight?: number; +}>; export interface SwipeableMethods { close: () => void; diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/reanimatedUtils.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/reanimatedUtils.ts index 30ead9484b..c785103e75 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/utils/reanimatedUtils.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/reanimatedUtils.ts @@ -20,7 +20,7 @@ function hash(str: string) { return h >>> 0; } -const SHARED_VALUE_OFFSET = 1.618; +export const SHARED_VALUE_OFFSET = 1.618; // Don't transfer entire NativeProxy to the UI thread const { updateGestureHandlerConfig } = NativeProxy; diff --git a/skills/gesture-handler-3-migration/SKILL.md b/skills/gesture-handler-3-migration/SKILL.md index ac6e82efd4..3cce8b87bf 100644 --- a/skills/gesture-handler-3-migration/SKILL.md +++ b/skills/gesture-handler-3-migration/SKILL.md @@ -186,6 +186,8 @@ The implementation of buttons has been updated, resolving most button-related is When migrating buttons, you should use new `Touchable` component instead. To replace `BaseButton` use `Touchable` with default props, to replace `RectButton` use `Touchable` with `underlayColor="black"` and to replace `BorderlessButton` use `Touchable` with `activeOpacity={0.3}`. +ReanimatedSwipeable prop `dragOffsetFromRight` now accepts negative values. If it was used with positive values, make sure to change the sign. + Legacy Touchables (`TouchableOpacity`, `TouchableHighlight`, `TouchableWithoutFeedback`, `TouchableNativeFeedback`) from Gesture Handler are also deprecated and should be replaced with `Touchable`. To replace `TouchableOpacity` use `Touchable` with `activeOpacity={0.2}` and to replace `TouchableHighlight` use `Touchable` with `activeUnderlayOpacity={1}`. To replace `TouchableWithoutFeedback` use a plain `Touchable`. `TouchableNativeFeedback` can be replaced with `Touchable` by setting `androidRipple` property. At minimum, it should be set to `{foreground: true}`, to mimic `TouchableNativeFeedback` ripple effect. Other components have also been internally rewritten using the new hook API but are exported under their original names, so no changes are necessary on your part. However, if you need to use the previous implementation for any reason, the legacy components are also available and are prefixed with `Legacy`, e.g., `ScrollView` is now available as `LegacyScrollView`.