UNPKG

react-native-reanimated-image-viewer

Version:
233 lines 9.04 kB
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