Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save chanphiromsok/18ed917d4161144a231f89a4ef0bac75 to your computer and use it in GitHub Desktop.
Save chanphiromsok/18ed917d4161144a231f89a4ef0bac75 to your computer and use it in GitHub Desktop.
RN: Swipeable Item with Reanimated 3 - Lightweight & Performant List Item with Swipe Actions
import React, {
forwardRef,
PropsWithChildren,
useImperativeHandle,
memo,
useMemo,
useCallback,
} from 'react';
import {
Gesture,
GestureDetector,
PanGesture,
} from 'react-native-gesture-handler';
import Animated, {
runOnJS,
useAnimatedStyle,
useSharedValue,
withTiming,
Easing,
} from 'react-native-reanimated';
import {StyleSheet, View} from 'react-native';
export interface SwipeableHandle {
closeSwipeable: () => void;
}
interface SwipeableProps extends PropsWithChildren {
width: number;
height?: number;
onSwipeableOpen?: () => void;
renderRightItem?: () => React.ReactNode;
}
interface SwipeableConfig {
MIN_POSITION: number;
MAX_POSITION: number;
VELOCITY_THRESHOLD: number;
}
interface RightItemMemoProps {
width: number;
height?: number;
renderRightItem?: () => React.ReactNode;
}
interface AnimatedContentProps {
children?: React.ReactNode;
panHandlers: PanGesture;
animatedStyle: {
transform: {
translateX: number;
}[];
};
}
const TIMING_CONFIG = {
duration: 300,
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
};
const RightItemMemo = memo(
({width, height, renderRightItem}: RightItemMemoProps) => {
const rightItemStyle = useMemo(
() => [styles.rightItem, {width, height}],
[width, height],
);
return <View style={rightItemStyle}>{renderRightItem?.()}</View>;
},
() => true,
);
const AnimatedContentMemo = memo(
({children, panHandlers, animatedStyle}: AnimatedContentProps) => (
<GestureDetector gesture={panHandlers}>
<Animated.View style={animatedStyle}>{children}</Animated.View>
</GestureDetector>
),
() => true,
);
const Swipeable = forwardRef<SwipeableHandle, SwipeableProps>(
({children, onSwipeableOpen, width, height, renderRightItem}, ref) => {
const Swipeable_CONFIG = useMemo<SwipeableConfig>(
() => ({
MIN_POSITION: -width,
MAX_POSITION: 0,
VELOCITY_THRESHOLD: 0.1,
}),
[width],
);
const onSwipeableOpenMemo = useCallback(() => {
onSwipeableOpen?.();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useImperativeHandle(ref, () => ({
closeSwipeable: () => {
translateX.value = withTiming(Swipeable_CONFIG.MAX_POSITION, {
...TIMING_CONFIG,
});
},
}));
const startX = useSharedValue(0);
const translateX = useSharedValue(0);
const handlePositionChange = useCallback((translationX: number) => {
'worklet';
const newPosition = startX.value + translationX;
translateX.value = Math.min(
Swipeable_CONFIG.MAX_POSITION,
Math.max(Swipeable_CONFIG.MIN_POSITION, newPosition),
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleGestureEnd = useCallback(
(velocityX: number) => {
'worklet';
const currentPosition = translateX.value;
const halfMinPosition = Swipeable_CONFIG.MIN_POSITION / 2;
const isHighVelocity =
Math.abs(velocityX) > Swipeable_CONFIG.VELOCITY_THRESHOLD;
const targetPosition = isHighVelocity
? velocityX < 0
? Swipeable_CONFIG.MIN_POSITION
: Swipeable_CONFIG.MAX_POSITION
: currentPosition < halfMinPosition
? Swipeable_CONFIG.MIN_POSITION
: Swipeable_CONFIG.MAX_POSITION;
translateX.value = withTiming(targetPosition, {
...TIMING_CONFIG,
duration: isHighVelocity ? 200 : 300,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const panGesture = Gesture.Pan()
.onStart(() => {
startX.value = translateX.value;
if (onSwipeableOpen) {
runOnJS(onSwipeableOpenMemo)();
}
})
.onChange(({translationX}) => {
handlePositionChange(translationX);
})
.onEnd(({velocityX}) => {
handleGestureEnd(velocityX);
})
.activeOffsetX([-20, 20]);
const animatedStyle = useAnimatedStyle(
() => ({
transform: [{translateX: translateX.value}],
}),
[],
);
return (
<View style={styles.container}>
{renderRightItem && (
<RightItemMemo
width={width}
height={height}
renderRightItem={renderRightItem}
/>
)}
<AnimatedContentMemo
panHandlers={panGesture}
animatedStyle={animatedStyle}>
{children}
</AnimatedContentMemo>
</View>
);
},
);
const styles = StyleSheet.create({
container: {
position: 'relative',
backfaceVisibility: 'hidden',
shadowColor: 'transparent',
shadowOpacity: 0,
elevation: 0,
},
rightItem: {
position: 'absolute',
right: 0,
zIndex: -1,
overflow: 'hidden',
backfaceVisibility: 'hidden',
shadowColor: 'transparent',
shadowOpacity: 0,
elevation: 0,
},
});
Swipeable.displayName = 'Swipeable';
RightItemMemo.displayName = 'RightItem';
AnimatedContentMemo.displayName = 'AnimatedContent';
export default memo(Swipeable, () => true);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment