Skip to content

Instantly share code, notes, and snippets.

@Drzaln
Created June 5, 2024 08:32
Show Gist options
  • Select an option

  • Save Drzaln/4a93e1601c512a6d4dc4c8762f2462e1 to your computer and use it in GitHub Desktop.

Select an option

Save Drzaln/4a93e1601c512a6d4dc4c8762f2462e1 to your computer and use it in GitHub Desktop.
React Native Image Preview (double tap and pinch to zoom)
// use react-native-reanimated v3 and react-native-gesture-handler v2
import React from 'react';
import {Dimensions, StyleSheet} from 'react-native';
import {
GestureHandlerRootView,
GestureDetector,
Gesture,
} from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
} from 'react-native-reanimated';
const {width, height} = Dimensions.get('window');
interface ImagePreviewProps {
source: {uri: string};
}
const ImagePreview: React.FC<ImagePreviewProps> = ({source}) => {
const baseScale = useSharedValue(1);
const pinchScale = useSharedValue(1);
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const offsetX = useSharedValue(0);
const offsetY = useSharedValue(0);
const pinchGesture = Gesture.Pinch()
.onUpdate(event => {
pinchScale.value = event.scale;
})
.onEnd(() => {
baseScale.value *= pinchScale.value;
pinchScale.value = 1;
if (baseScale.value < 1) {
baseScale.value = 1;
translateX.value = withSpring(0);
translateY.value = withSpring(0);
} else if (baseScale.value > 4) {
baseScale.value = 4;
}
offsetX.value = translateX.value;
offsetY.value = translateY.value;
});
const panGesture = Gesture.Pan()
.onUpdate(event => {
translateX.value = offsetX.value + event.translationX;
translateY.value = offsetY.value + event.translationY;
})
.onEnd(() => {
offsetX.value = translateX.value;
offsetY.value = translateY.value;
});
const doubleTapGesture = Gesture.Tap()
.numberOfTaps(2)
.onEnd((event, success) => {
if (success) {
if (baseScale.value > 1) {
baseScale.value = withTiming(1, {duration: 300});
translateX.value = withTiming(0, {duration: 300});
translateY.value = withTiming(0, {duration: 300});
offsetX.value = 0;
offsetY.value = 0;
} else {
const focalPointX = event.x;
const focalPointY = event.y;
const adjustedTranslateX = (focalPointX - width / 2) * -1;
const adjustedTranslateY = (focalPointY - height / 2) * -1;
baseScale.value = withTiming(2, {duration: 300});
translateX.value = withTiming(adjustedTranslateX, {duration: 300});
translateY.value = withTiming(adjustedTranslateY, {duration: 300});
offsetX.value = adjustedTranslateX;
offsetY.value = adjustedTranslateY;
}
}
});
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{translateX: translateX.value},
{translateY: translateY.value},
{scale: baseScale.value * pinchScale.value},
],
};
});
return (
<GestureHandlerRootView style={styles.container}>
<GestureDetector
gesture={Gesture.Exclusive(
doubleTapGesture,
Gesture.Simultaneous(pinchGesture, panGesture),
)}>
<Animated.Image
source={source}
style={[styles.image, animatedStyle]}
resizeMode="contain"
/>
</GestureDetector>
</GestureHandlerRootView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
image: {
width: width,
height: height,
},
});
export default ImagePreview;
// ---------------------------------------------------
// How to use
// <ImagePreview source={{uri: imageUri}} />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment