react-native-reanimated-image-viewer
Version:
A image viewer for React Native created with Reanimated
233 lines • 9.04 kB
JavaScript
import React, { useMemo } from "react";
import { useWindowDimensions } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withDecay, withTiming, } from "react-native-reanimated";
export default function ImageViewer({ imageUrl, width, height, onSingleTap, onRequestClose, }) {
const dimensions = useWindowDimensions();
const scale = useSharedValue(1);
const savedScale = useSharedValue(1);
const translateY = useSharedValue(0);
const savedTranslateY = useSharedValue(0);
const translateX = useSharedValue(0);
const savedTranslateX = useSharedValue(0);
const MAX_ZOOM_SCALE = 3;
const { width: finalWidth, height: finalHeight } = useMemo(() => {
function ruleOfThree(firstValue, firstResult, secondValue) {
const secondResult = (firstResult * secondValue) / firstValue;
return secondResult;
}
const resizedBasedOnWidth = {
width: dimensions.width,
height: ruleOfThree(width, dimensions.width, height),
};
const resizedBasedOnHeight = {
width: ruleOfThree(height, dimensions.height, width),
height: dimensions.height,
};
if (width === height) {
const smallestScreenDimension = Math.min(dimensions.width, dimensions.height);
return {
width: smallestScreenDimension,
height: smallestScreenDimension,
};
}
else if (width > height) {
return resizedBasedOnWidth;
}
else {
if (resizedBasedOnHeight.width > dimensions.width) {
return resizedBasedOnWidth;
}
return resizedBasedOnHeight;
}
}, [width, height, dimensions.width, dimensions.height]);
const pinchGesture = Gesture.Pinch()
.onStart(() => {
savedScale.value = scale.value;
})
.onUpdate((event) => {
scale.value = savedScale.value * event.scale;
});
const panGesture = Gesture.Pan()
.onStart(() => {
savedTranslateX.value = translateX.value;
savedTranslateY.value = translateY.value;
})
.onUpdate((event) => {
if (scale.value < 1) {
return;
}
const realImageWidth = finalWidth * scale.value;
const maxTranslateX = realImageWidth <= dimensions.width
? 0
: (realImageWidth - dimensions.width) / 2;
const minTranslateX = realImageWidth <= dimensions.width
? 0
: -(realImageWidth - dimensions.width) / 2;
const possibleNewTranslateX = savedTranslateX.value + event.translationX;
if (possibleNewTranslateX > maxTranslateX) {
translateX.value = maxTranslateX;
}
else if (possibleNewTranslateX < minTranslateX) {
translateX.value = minTranslateX;
}
else {
translateX.value = possibleNewTranslateX;
}
if (scale.value > 1) {
const realImageHeight = finalHeight * scale.value;
const maxTranslateY = realImageHeight <= dimensions.height
? 0
: (realImageHeight - dimensions.height) / 2;
const minTranslateY = realImageHeight <= dimensions.height
? 0
: -(realImageHeight - dimensions.height) / 2;
const possibleNewTranslateY = savedTranslateY.value + event.translationY;
if (possibleNewTranslateY > maxTranslateY) {
translateY.value = maxTranslateY;
}
else if (possibleNewTranslateY < minTranslateY) {
translateY.value = minTranslateY;
}
else {
translateY.value = possibleNewTranslateY;
}
}
else {
translateY.value = savedTranslateY.value + event.translationY;
}
})
.onEnd((event) => {
if (scale.value === 1) {
if (event.translationY < -50) {
if (event.velocityY < -2000 || event.translationY < -200) {
runOnJS(onRequestClose)();
return;
}
}
translateY.value = withTiming(0);
translateX.value = withTiming(0);
}
else if (scale.value < 1) {
scale.value = withTiming(1);
translateX.value = withTiming(0);
translateY.value = withTiming(0);
}
else if (scale.value > MAX_ZOOM_SCALE) {
scale.value = withTiming(MAX_ZOOM_SCALE);
}
else {
const realImageWidth = finalWidth * scale.value;
const maxTranslateX = realImageWidth <= dimensions.width
? 0
: (realImageWidth - dimensions.width) / 2;
const minTranslateX = realImageWidth <= dimensions.width
? 0
: -(realImageWidth - dimensions.width) / 2;
translateX.value = withDecay({
velocity: event.velocityX,
clamp: [minTranslateX, maxTranslateX],
});
const realImageHeight = finalHeight * scale.value;
const maxTranslateY = realImageHeight <= dimensions.height
? 0
: (realImageHeight - dimensions.height) / 2;
const minTranslateY = realImageHeight <= dimensions.height
? 0
: -(realImageHeight - dimensions.height) / 2;
translateY.value = withDecay({
velocity: event.velocityY,
clamp: [minTranslateY, maxTranslateY],
});
}
});
const singleTap = Gesture.Tap().onEnd(() => {
onSingleTap && runOnJS(onSingleTap)();
});
const doubleTap = Gesture.Tap()
.onStart((event) => {
if (scale.value > 1) {
scale.value = withTiming(1);
translateX.value = withTiming(0);
translateY.value = withTiming(0);
}
else {
scale.value = withTiming(MAX_ZOOM_SCALE);
const realImageWidth = finalWidth * MAX_ZOOM_SCALE;
const maxTranslateX = (realImageWidth - dimensions.width) / 2;
const minTranslateX = -(realImageWidth - dimensions.width) / 2;
const possibleNewTranslateX = (finalWidth / 2 - event.x) * MAX_ZOOM_SCALE;
let newTranslateX = 0;
if (possibleNewTranslateX > maxTranslateX) {
newTranslateX = maxTranslateX;
}
else if (possibleNewTranslateX < minTranslateX) {
newTranslateX = minTranslateX;
}
else {
newTranslateX = possibleNewTranslateX;
}
translateX.value = withTiming(newTranslateX);
const realImageHeight = finalHeight * MAX_ZOOM_SCALE;
const maxTranslateY = realImageHeight <= dimensions.height
? 0
: (realImageHeight - dimensions.height) / 2;
const minTranslateY = realImageHeight <= dimensions.height
? 0
: -(realImageHeight - dimensions.height) / 2;
const possibleNewTranslateY = (finalHeight / 2 - event.y) * MAX_ZOOM_SCALE;
let newTranslateY = 0;
if (possibleNewTranslateY > maxTranslateY) {
newTranslateY = maxTranslateY;
}
else if (possibleNewTranslateY < minTranslateY) {
newTranslateY = minTranslateY;
}
else {
newTranslateY = possibleNewTranslateY;
}
translateY.value = withTiming(newTranslateY);
}
})
.numberOfTaps(2);
const imageContainerAnimatedStyle = useAnimatedStyle(() => {
return {
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
],
};
});
const imageAnimatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
scale: scale.value,
},
],
};
}, []);
const composedGestures = Gesture.Simultaneous(pinchGesture, panGesture);
const allGestures = Gesture.Exclusive(composedGestures, doubleTap, singleTap);
return (<GestureDetector gesture={allGestures}>
<Animated.View style={{
flex: 1,
alignItems: "center",
justifyContent: "center",
backgroundColor: "#000",
}}>
<Animated.View style={imageContainerAnimatedStyle}>
<Animated.Image style={[
imageAnimatedStyle,
{
width: finalWidth,
height: finalHeight,
},
]} source={{
uri: imageUrl,
}}/>
</Animated.View>
</Animated.View>
</GestureDetector>);
}
//# sourceMappingURL=ImageViewer.js.map