diff --git a/FabricExample/tsconfig.json b/FabricExample/tsconfig.json index 887ba2edf..d2b92fee2 100644 --- a/FabricExample/tsconfig.json +++ b/FabricExample/tsconfig.json @@ -11,9 +11,7 @@ "react-native-gesture-handler/DrawerLayout": [ "../src/components/DrawerLayout.tsx" ], - "react-native-gesture-handler/jest-utils": [ - "../src/jestUtils/index.ts" - ] + "react-native-gesture-handler/jest-utils": ["../src/jestUtils/index.ts"] } }, "include": ["src/**/*.ts", "src/**/*.tsx", "index.js"] diff --git a/MacOSExample/tsconfig.json b/MacOSExample/tsconfig.json index 6fe9cb8e2..0ad57e237 100644 --- a/MacOSExample/tsconfig.json +++ b/MacOSExample/tsconfig.json @@ -11,9 +11,7 @@ "react-native-gesture-handler/DrawerLayout": [ "../src/components/DrawerLayout.tsx" ], - "react-native-gesture-handler/jest-utils": [ - "../src/jestUtils/index.ts" - ] + "react-native-gesture-handler/jest-utils": ["../src/jestUtils/index.ts"] } } } diff --git a/ReanimatedSwipeable/package.json b/ReanimatedSwipeable/package.json new file mode 100644 index 000000000..9b8283043 --- /dev/null +++ b/ReanimatedSwipeable/package.json @@ -0,0 +1,6 @@ +{ + "main": "../lib/commonjs/components/ReanimatedSwipeable", + "module": "../lib/module/components/ReanimatedSwipeable", + "react-native": "../src/components/ReanimatedSwipeable", + "types": "../lib/typescript/components/ReanimatedSwipeable.d.ts" +} diff --git a/e2e/web-tests/babel.config.js b/e2e/web-tests/babel.config.js index 65662f5cf..88caeb641 100644 --- a/e2e/web-tests/babel.config.js +++ b/e2e/web-tests/babel.config.js @@ -14,6 +14,8 @@ module.exports = function (api) { '../../src/components/DrawerLayout', 'react-native-gesture-handler/Swipeable': '../../src/components/Swipeable', + 'react-native-gesture-handler/ReanimatedSwipeable': + '../../src/components/ReanimatedSwipeable', 'react-native-gesture-handler': '../../src/index', }, }, diff --git a/e2e/web-tests/tsconfig.json b/e2e/web-tests/tsconfig.json index b4d3170b3..6ab3bd039 100644 --- a/e2e/web-tests/tsconfig.json +++ b/e2e/web-tests/tsconfig.json @@ -1,7 +1,7 @@ { // "extends": "expo/tsconfig.base", "extends": "../../tsconfig.json", - "compilerOptions": { + "compilerOptions": { // "strict": true, "baseUrl": ".", "paths": { @@ -15,7 +15,7 @@ "react-native-gesture-handler/jest-utils": [ "../../src/jestUtils/index.ts" ], - "react": ["./node_modules/@types/react"], + "react": ["./node_modules/@types/react"] } }, "include": ["./tests/*.ts", "./components/*.tsx", "./App.tsx"] diff --git a/example/App.tsx b/example/App.tsx index 34a9c4239..f44a2e1dd 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -35,6 +35,7 @@ import ContextMenu from './src/release_tests/contextMenu'; import NestedTouchables from './src/release_tests/nestedTouchables'; import NestedButtons from './src/release_tests/nestedButtons'; import PointerType from './src/release_tests/pointerType'; +import SwipeableReanimation from './src/release_tests/swipeableReanimation'; import NestedGestureHandlerRootViewWithModal from './src/release_tests/nestedGHRootViewWithModal'; import { PinchableBox } from './src/recipes/scaleAndRotate'; import PanAndScroll from './src/recipes/panAndScroll'; @@ -140,6 +141,7 @@ const EXAMPLES: ExamplesSection[] = [ { name: 'MouseButtons', component: MouseButtons }, { name: 'ContextMenu (web only)', component: ContextMenu }, { name: 'PointerType', component: PointerType }, + { name: 'Swipeable Reanimation', component: SwipeableReanimation }, { name: 'RectButton (borders)', component: RectButtonBorders }, ], }, diff --git a/example/babel.config.js b/example/babel.config.js index 6a5161428..3966c5011 100644 --- a/example/babel.config.js +++ b/example/babel.config.js @@ -8,6 +8,8 @@ module.exports = function (api) { { extensions: ['.js', '.ts', '.tsx'], alias: { + 'react-native-gesture-handler/ReanimatedSwipeable': + '../src/components/ReanimatedSwipeable', 'react-native-gesture-handler': '../src/index', }, }, diff --git a/example/src/new_api/swipeable/AppleStyleSwipeableRow.tsx b/example/src/new_api/swipeable/AppleStyleSwipeableRow.tsx index 0c67cc885..1cd1d7eea 100644 --- a/example/src/new_api/swipeable/AppleStyleSwipeableRow.tsx +++ b/example/src/new_api/swipeable/AppleStyleSwipeableRow.tsx @@ -8,7 +8,9 @@ import Animated, { interpolate, useAnimatedStyle, } from 'react-native-reanimated'; -import Swipeable, { SwipeableMethods } from 'src/new_api/swipeable/Swipeable'; +import Swipeable, { + SwipeableMethods, +} from 'react-native-gesture-handler/ReanimatedSwipeable'; interface AppleStyleSwipeableRowProps { children?: ReactNode; diff --git a/example/src/new_api/swipeable/GmailStyleSwipeableRow.tsx b/example/src/new_api/swipeable/GmailStyleSwipeableRow.tsx index 8b19e7e79..465a6efca 100644 --- a/example/src/new_api/swipeable/GmailStyleSwipeableRow.tsx +++ b/example/src/new_api/swipeable/GmailStyleSwipeableRow.tsx @@ -8,7 +8,9 @@ import Animated, { interpolate, useAnimatedStyle, } from 'react-native-reanimated'; -import Swipeable, { SwipeableMethods } from 'src/new_api/swipeable/Swipeable'; +import Swipeable, { + SwipeableMethods, +} from 'react-native-gesture-handler/ReanimatedSwipeable'; interface LeftActionProps { dragX: SharedValue; diff --git a/example/src/new_api/swipeable/index.tsx b/example/src/new_api/swipeable/index.tsx index ae1712ebd..f595afad8 100644 --- a/example/src/new_api/swipeable/index.tsx +++ b/example/src/new_api/swipeable/index.tsx @@ -42,11 +42,13 @@ const SwipeableRow = ({ item, index }: { item: DataRow; index: number }) => { } }; +const Separator = () => ; + export default function App() { return ( } + ItemSeparatorComponent={Separator} renderItem={({ item, index }) => ( )} diff --git a/example/src/release_tests/swipeableReanimation/index.tsx b/example/src/release_tests/swipeableReanimation/index.tsx new file mode 100644 index 000000000..a9a931264 --- /dev/null +++ b/example/src/release_tests/swipeableReanimation/index.tsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { Text, Animated, StyleSheet, View } from 'react-native'; + +import { + Swipeable, + GestureHandlerRootView, +} from 'react-native-gesture-handler'; +import ReanimatedSwipeable from 'react-native-gesture-handler/ReanimatedSwipeable'; +import Reanimated, { + SharedValue, + useAnimatedStyle, +} from 'react-native-reanimated'; + +function LeftAction(prog: SharedValue, drag: SharedValue) { + const styleAnimation = useAnimatedStyle(() => { + console.log('[R] showLeftProgress:', prog.value); + console.log('[R] appliedTranslation:', drag.value); + + return { + transform: [{ translateX: drag.value - 50 }], + }; + }); + + return ( + + Text + + ); +} + +function RightAction(prog: SharedValue, drag: SharedValue) { + const styleAnimation = useAnimatedStyle(() => { + console.log('[R] showRightProgress:', prog.value); + console.log('[R] appliedTranslation:', drag.value); + + return { + transform: [{ translateX: drag.value + 50 }], + }; + }); + + return ( + + Text + + ); +} + +function LegacyLeftAction(prog: any, drag: any) { + prog.addListener((value: any) => { + console.log('[L] showLeftProgress:', value.value); + }); + drag.addListener((value: any) => { + console.log('[L] appliedTranslation:', value.value); + }); + + const trans = Animated.subtract(drag, 50); + + return ( + + Text + + ); +} + +function LegacyRightAction(prog: any, drag: any) { + prog.addListener((value: any) => { + console.log('[L] showRightProgress:', value.value); + }); + drag.addListener((value: any) => { + console.log('[L] appliedTranslation:', value.value); + }); + + const trans = Animated.add(drag, 50); + + return ( + + Text + + ); +} + +export default function Example() { + return ( + + + + + [Reanimated] Swipe me! + + + + + + [Legacy] Swipe me! + + + + + ); +} + +const styles = StyleSheet.create({ + leftAction: { width: 50, height: 50, backgroundColor: 'crimson' }, + rightAction: { width: 50, height: 50, backgroundColor: 'purple' }, + separator: { + width: '100%', + borderTopWidth: 1, + }, + swipeable: { + height: 50, + backgroundColor: 'papayawhip', + alignItems: 'center', + }, +}); diff --git a/package.json b/package.json index 9a1bd622d..d9c592ea0 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "android/noreanimated/src/main/java/", "apple/", "Swipeable/", + "ReanimatedSwipeable/", "jest-utils/", "DrawerLayout/", "README.md", diff --git a/example/src/new_api/swipeable/Swipeable.tsx b/src/components/ReanimatedSwipeable.tsx similarity index 70% rename from example/src/new_api/swipeable/Swipeable.tsx rename to src/components/ReanimatedSwipeable.tsx index 05d308ea3..aee977bb5 100644 --- a/example/src/new_api/swipeable/Swipeable.tsx +++ b/src/components/ReanimatedSwipeable.tsx @@ -6,18 +6,19 @@ import React, { ForwardedRef, forwardRef, useCallback, - useEffect, useImperativeHandle, useRef, } from 'react'; +import { GestureObjects as Gesture } from '../handlers/gestures/gestureObjects'; +import { GestureDetector } from '../handlers/gestures/GestureDetector'; import { - Gesture, - GestureDetector, GestureStateChangeEvent, GestureUpdateEvent, +} from '../handlers/gestureHandlerCommon'; +import { PanGestureHandlerEventPayload, PanGestureHandlerProps, -} from 'react-native-gesture-handler'; +} from '../handlers/PanGestureHandler'; import Animated, { Extrapolation, SharedValue, @@ -173,8 +174,6 @@ export interface SwipeableProps swipeable: SwipeableMethods ) => React.ReactNode; - useNativeAnimations?: boolean; - animationOptions?: Record; /** @@ -243,6 +242,9 @@ const Swipeable = forwardRef( overshootFriction = defaultProps.overshootFriction, } = props; + const overshootLeftProp = props.overshootLeft; + const overshootRightProp = props.overshootRight; + const calculateCurrentOffset = useCallback(() => { 'worklet'; if (rowState.value === 1) { @@ -257,10 +259,8 @@ const Swipeable = forwardRef( 'worklet'; rightWidth.value = Math.max(0, rowWidth.value - rightOffset.value); - const { - overshootLeft = leftWidth.value > 0, - overshootRight = rightWidth.value > 0, - } = props; + const overshootLeft = overshootLeftProp ?? leftWidth.value > 0; + const overshootRight = overshootRightProp ?? rightWidth.value > 0; const startOffset = rowState.value === 1 @@ -317,54 +317,85 @@ const Swipeable = forwardRef( ); }; + const dispatchImmediateEvents = useCallback( + (fromValue: number, toValue: number) => { + if (toValue > 0 && props.onSwipeableWillOpen) { + props.onSwipeableWillOpen('left'); + } else if (toValue < 0 && props.onSwipeableWillOpen) { + props.onSwipeableWillOpen('right'); + } else if (props.onSwipeableWillClose) { + const closingDirection = fromValue > 0 ? 'left' : 'right'; + props.onSwipeableWillClose(closingDirection); + } + }, + [ + props, + props.onSwipeableWillClose, + props.onSwipeableWillOpen, + swipeableMethods, + ] + ); + + const dispatchEndEvents = useCallback( + (fromValue: number, toValue: number) => { + if (toValue > 0 && props.onSwipeableOpen) { + props.onSwipeableOpen('left', swipeableMethods.current); + } else if (toValue < 0 && props.onSwipeableOpen) { + props.onSwipeableOpen('right', swipeableMethods.current); + } else if (props.onSwipeableClose) { + const closingDirection = fromValue > 0 ? 'left' : 'right'; + props.onSwipeableClose(closingDirection, swipeableMethods.current); + } + }, + [props, props.onSwipeableClose, props.onSwipeableOpen, swipeableMethods] + ); + + const animationOptionsProp = props.animationOptions; + const animateRow = useCallback( (fromValue: number, toValue: number, velocityX?: number) => { 'worklet'; - rowState.value = Math.sign(toValue); + const springConfig = { + duration: 1000, + dampingRatio: 0.9, + stiffness: 500, + velocity: velocityX, + overshootClamping: true, + ...animationOptionsProp, + }; + appliedTranslation.value = withSpring( toValue, - { - duration: 1000, - dampingRatio: 1.2, - stiffness: 500, - velocity: velocityX, - ...props.animationOptions, - }, + springConfig, (isFinished) => { if (isFinished) { - if (toValue > 0 && props.onSwipeableOpen) { - runOnJS(props.onSwipeableOpen)( - 'left', - swipeableMethods.current - ); - } else if (toValue < 0 && props.onSwipeableOpen) { - runOnJS(props.onSwipeableOpen)( - 'right', - swipeableMethods.current - ); - } else if (props.onSwipeableClose) { - const closingDirection = fromValue > 0 ? 'left' : 'right'; - runOnJS(props.onSwipeableClose)( - closingDirection, - swipeableMethods.current - ); - } + runOnJS(dispatchEndEvents)(fromValue, toValue); } } ); - if (toValue > 0 && props.onSwipeableWillOpen) { - runOnJS(props.onSwipeableWillOpen)('left'); - } else if (toValue < 0 && props.onSwipeableWillOpen) { - runOnJS(props.onSwipeableWillOpen)('right'); - } else if (props.onSwipeableWillClose) { - const closingDirection = fromValue > 0 ? 'left' : 'right'; - runOnJS(props.onSwipeableWillClose)(closingDirection); - } + const progressTarget = toValue === 0 ? 0 : 1; + + // velocity is in px, while progress is in % + springConfig.velocity = 0; + + showLeftProgress.value = + leftWidth.value > 0 ? withSpring(progressTarget, springConfig) : 0; + showRightProgress.value = + rightWidth.value > 0 ? withSpring(progressTarget, springConfig) : 0; + + runOnJS(dispatchImmediateEvents)(fromValue, toValue); }, - [appliedTranslation, props, rowState, swipeableMethods] + [ + showLeftProgress, + appliedTranslation, + dispatchEndEvents, + dispatchImmediateEvents, + animationOptionsProp, + rowState, + ] ); const onRowLayout = ({ nativeEvent }: LayoutChangeEvent) => { @@ -379,47 +410,39 @@ const Swipeable = forwardRef( dragOffsetFromRightEdge = 10, } = props; - useEffect(() => { - swipeableMethods.current = { - close() { - 'worklet'; - animateRow(calculateCurrentOffset(), 0); - }, - openLeft() { - 'worklet'; - animateRow(calculateCurrentOffset(), leftWidth.value); - }, - openRight() { - 'worklet'; - rightWidth.value = rowWidth.value - rightOffset.value; - animateRow(calculateCurrentOffset(), -rightWidth.value); - }, - reset() { - 'worklet'; - userDrag.value = 0; - appliedTranslation.value = 0; - rowState.value = 0; - }, - }; - }, [ - animateRow, - calculateCurrentOffset, - appliedTranslation, - leftWidth, - rightOffset, - rightWidth, - rowState, - rowWidth, - userDrag, - ]); + swipeableMethods.current = { + close() { + 'worklet'; + animateRow(calculateCurrentOffset(), 0); + }, + openLeft() { + 'worklet'; + animateRow(calculateCurrentOffset(), leftWidth.value); + }, + openRight() { + 'worklet'; + rightWidth.value = rowWidth.value - rightOffset.value; + animateRow(calculateCurrentOffset(), -rightWidth.value); + }, + reset() { + 'worklet'; + userDrag.value = 0; + showLeftProgress.value = 0; + appliedTranslation.value = 0; + rowState.value = 0; + }, + }; - const leftAnimatedStyle = useAnimatedStyle(() => ({ - transform: [ - { - translateX: leftActionTranslate.value, - }, - ], - })); + const leftAnimatedStyle = useAnimatedStyle( + () => ({ + transform: [ + { + translateX: leftActionTranslate.value, + }, + ], + }), + [leftActionTranslate] + ); const leftElement = renderLeftActions && ( @@ -436,13 +459,16 @@ const Swipeable = forwardRef( ); - const rightAnimatedStyle = useAnimatedStyle(() => ({ - transform: [ - { - translateX: rightActionTranslate.value, - }, - ], - })); + const rightAnimatedStyle = useAnimatedStyle( + () => ({ + transform: [ + { + translateX: rightActionTranslate.value, + }, + ], + }), + [rightActionTranslate] + ); const rightElement = renderRightActions && ( @@ -459,6 +485,9 @@ const Swipeable = forwardRef( ); + const leftThresholdProp = props.leftThreshold; + const rightThresholdProp = props.rightThreshold; + const handleRelease = ( event: GestureStateChangeEvent ) => { @@ -468,10 +497,8 @@ const Swipeable = forwardRef( rightWidth.value = rowWidth.value - rightOffset.value; - const { - leftThreshold = leftWidth.value / 2, - rightThreshold = rightWidth.value / 2, - } = props; + const leftThreshold = leftThresholdProp ?? leftWidth.value / 2; + const rightThreshold = rightThresholdProp ?? rightWidth.value / 2; const startOffsetX = calculateCurrentOffset() + userDrag.value / friction; const translationX = (userDrag.value + DRAG_TOSS * velocityX) / friction; @@ -510,6 +537,9 @@ const Swipeable = forwardRef( } }); + const onSwipeableOpenStartDrag = props.onSwipeableOpenStartDrag; + const onSwipeableCloseStartDrag = props.onSwipeableCloseStartDrag; + const panGesture = Gesture.Pan() .onUpdate((event: GestureUpdateEvent) => { userDrag.value = event.translationX; @@ -523,16 +553,24 @@ const Swipeable = forwardRef( ? 'left' : 'right'; - if (rowState.value === 0 && props.onSwipeableOpenStartDrag) { - runOnJS(props.onSwipeableOpenStartDrag)(direction); - } else if (props.onSwipeableCloseStartDrag) { - runOnJS(props.onSwipeableCloseStartDrag)(direction); + if (rowState.value === 0 && onSwipeableOpenStartDrag) { + runOnJS(onSwipeableOpenStartDrag)(direction); + } else if (rowState.value !== 0 && onSwipeableCloseStartDrag) { + runOnJS(onSwipeableCloseStartDrag)(direction); } updateAnimatedEvent(); }) - .onEnd((event) => { - handleRelease(event); - }); + .onEnd( + (event: GestureStateChangeEvent) => { + handleRelease(event); + } + ); + + if (props.enableTrackpadTwoFingerGesture) { + panGesture.enableTrackpadTwoFingerGesture( + props.enableTrackpadTwoFingerGesture + ); + } panGesture.activeOffsetX([ -dragOffsetFromRightEdge, @@ -544,27 +582,31 @@ const Swipeable = forwardRef( swipeableMethods, ]); - const animatedStyle = useAnimatedStyle(() => ({ - transform: [{ translateX: appliedTranslation.value }], - pointerEvents: rowState.value === 0 ? 'auto' : 'box-only', - })); + const animatedStyle = useAnimatedStyle( + () => ({ + transform: [{ translateX: appliedTranslation.value }], + pointerEvents: rowState.value === 0 ? 'auto' : 'box-only', + }), + [appliedTranslation, rowState] + ); + + const containerStyle = props.containerStyle; + const childrenContainerStyle = props.childrenContainerStyle; - const composedGesture = Gesture.Race(panGesture, tapGesture); return ( - - {leftElement} - {rightElement} - - - {children} - - - + + + {leftElement} + {rightElement} + + + {children} + + + + ); } ); diff --git a/tsconfig.json b/tsconfig.json index e3fe5c53b..5a6772d93 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,5 +21,5 @@ "noUnusedParameters": true, "noUnusedLocals": true }, - "include": ["src/**/*.ts", "src/**/*.tsx", "jestSetup.js"], + "include": ["src/**/*.ts", "src/**/*.tsx", "jestSetup.js"] }