Created
March 5, 2021 06:18
-
-
Save osamaqarem/d3670f152e70fcc8ae2b63cb37870ec7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as React from 'react' | |
import { Image, StatusBar, StyleSheet, useWindowDimensions } from 'react-native' | |
import { | |
PanGestureHandler, | |
PinchGestureHandler, | |
PinchGestureHandlerGestureEvent, | |
TapGestureHandler, | |
TapGestureHandlerGestureEvent, | |
} from 'react-native-gesture-handler' | |
import Animated, { | |
Easing, | |
useAnimatedGestureHandler, | |
useAnimatedRef, | |
useAnimatedStyle, | |
useSharedValue, | |
withTiming, | |
} from 'react-native-reanimated' | |
const BASE_SCALE = 1 | |
const MAX_SCALE = 5 | |
type PinchContext = { | |
scaleOffset?: number | |
initalScale?: number | |
} | |
function clamp(value: number, min: number, max: number) { | |
'worklet' | |
return Math.min(Math.max(value, min), max) | |
} | |
const Photo = (props: any) => { | |
const { width: screenWidth, height: screenHeight } = useWindowDimensions() | |
const aspectWidth = screenWidth | |
const aspectHeight = Math.min( | |
screenHeight - (StatusBar.currentHeight ?? 0), | |
(screenWidth * props.height) / props.width, | |
) | |
const panHandlerRef = React.useRef() | |
const tapHandlerRef = React.useRef() | |
const imageRef = useAnimatedRef() | |
const scale = useSharedValue(1) | |
const transX = useSharedValue(0) | |
const transY = useSharedValue(0) | |
const transXOffset = useSharedValue(0) | |
const transYOffset = useSharedValue(0) | |
function getImgWidth() { | |
'worklet' | |
const imgWidthWide = aspectWidth | |
const imgWidthThin = (aspectHeight * props.width) / props.height | |
return Math.min(imgWidthWide, imgWidthThin) * scale.value | |
} | |
function getImgHeight() { | |
'worklet' | |
return aspectHeight * scale.value | |
} | |
const pinchHandler = useAnimatedGestureHandler< | |
PinchGestureHandlerGestureEvent, | |
PinchContext | |
>({ | |
onActive: (e, ctx) => { | |
function handleOriginOfScaleTransform() { | |
const transformOriginX = screenWidth / 2 | |
const transformOriginY = screenHeight / 2 | |
const imgWidth = getImgWidth() | |
const imgHeight = getImgHeight() | |
const leftEdge = (imgWidth - screenWidth) / scale.value / 2 | |
const rightEdge = -leftEdge | |
const topEdge = (imgHeight - screenHeight) / scale.value / 2 | |
const bottomEdge = -topEdge | |
const initialScale = ctx.initalScale ?? 1 | |
const scaleDiff = Math.abs(scale.value - (ctx.initalScale ?? 0)) | |
const displacementX = | |
((transformOriginX - e.focalX) / initialScale) * scaleDiff + | |
transXOffset.value | |
const displacementY = | |
((transformOriginY - e.focalY) / initialScale) * scaleDiff + | |
transYOffset.value | |
if (imgWidth > screenWidth && scale.value <= MAX_SCALE) { | |
transX.value = clamp(displacementX, rightEdge, leftEdge) | |
} | |
if (imgHeight > screenHeight && scale.value <= MAX_SCALE) { | |
transY.value = clamp(displacementY, bottomEdge, topEdge) | |
} | |
} | |
scale.value = (ctx.scaleOffset ?? 0) + e.scale | |
handleOriginOfScaleTransform() | |
}, | |
onFinish: (e, ctx) => { | |
transXOffset.value = transX.value | |
transYOffset.value = transY.value | |
ctx.initalScale = scale.value | |
function resetZoom() { | |
scale.value = withTiming(BASE_SCALE, { | |
duration: 200, | |
easing: Easing.inOut(Easing.ease), | |
}) | |
ctx.scaleOffset = 0 | |
} | |
function resetPanXY() { | |
transX.value = withTiming(0, { | |
duration: 200, | |
easing: Easing.inOut(Easing.ease), | |
}) | |
transY.value = withTiming(0, { | |
duration: 200, | |
easing: Easing.inOut(Easing.ease), | |
}) | |
} | |
if (scale.value < BASE_SCALE) { | |
resetZoom() | |
resetPanXY() | |
} else if (scale.value > MAX_SCALE) { | |
ctx.scaleOffset = MAX_SCALE - 1 | |
ctx.initalScale = MAX_SCALE | |
scale.value = withTiming(MAX_SCALE, { | |
duration: 200, | |
easing: Easing.inOut(Easing.ease), | |
}) | |
} else { | |
ctx.scaleOffset = scale.value - BASE_SCALE | |
} | |
}, | |
}) | |
const panHandler = useAnimatedGestureHandler({ | |
onActive: (e, ctx) => { | |
if (scale.value > BASE_SCALE) { | |
const imgWidth = getImgWidth() | |
const imgHeight = getImgHeight() | |
function handlePanX() { | |
const leftEdge = (imgWidth - screenWidth) / scale.value / 2 | |
const rightEdge = -leftEdge | |
transX.value = clamp( | |
transXOffset.value + e.translationX, | |
rightEdge, | |
leftEdge, | |
) | |
} | |
function handlePanY() { | |
const topEdge = (imgHeight - screenHeight) / scale.value / 2 | |
const bottomEdge = -topEdge | |
transY.value = clamp( | |
transYOffset.value + e.translationY, | |
bottomEdge, | |
topEdge, | |
) | |
} | |
if (imgWidth > screenWidth && scale.value <= MAX_SCALE) { | |
handlePanX() | |
} | |
if (imgHeight > screenHeight && scale.value <= MAX_SCALE) { | |
handlePanY() | |
} | |
} | |
}, | |
onFinish: (e, ctx) => { | |
console.log('pan onFinish') | |
transXOffset.value = transX.value | |
transYOffset.value = transY.value | |
function resetPanX() { | |
transX.value = withTiming(0, { | |
duration: 200, | |
easing: Easing.inOut(Easing.ease), | |
}) | |
transXOffset.value = 0 | |
} | |
function resetPanY() { | |
transY.value = withTiming(0, { | |
duration: 200, | |
easing: Easing.inOut(Easing.ease), | |
}) | |
transYOffset.value = 0 | |
} | |
if (scale.value < BASE_SCALE) { | |
resetPanX() | |
resetPanY() | |
} | |
}, | |
}) | |
const tapHandler = useAnimatedGestureHandler< | |
TapGestureHandlerGestureEvent, | |
any | |
>({ | |
onActive: (e, ctx) => { | |
// TODO: | |
}, | |
}) | |
const transforms = useAnimatedStyle(() => { | |
return { | |
transform: [ | |
{ | |
scale: scale.value, | |
}, | |
{ | |
translateX: transX.value, | |
}, | |
{ | |
translateY: transY.value, | |
}, | |
], | |
} | |
}) | |
return ( | |
<PinchGestureHandler | |
onGestureEvent={pinchHandler} | |
simultaneousHandlers={[panHandlerRef, tapHandlerRef]}> | |
<Animated.View style={styles.pinchView}> | |
<PanGestureHandler | |
maxPointers={1} | |
onGestureEvent={panHandler} | |
ref={panHandlerRef}> | |
<Animated.View style={[styles.panView, transforms]}> | |
<TapGestureHandler ref={tapHandlerRef} onGestureEvent={tapHandler}> | |
<Animated.View> | |
<Image | |
source={{ uri: props.uri }} | |
style={{ | |
height: aspectHeight ? aspectHeight : 0, | |
width: aspectWidth ? aspectWidth : 0, | |
}} | |
resizeMode="contain" | |
ref={imageRef as any} | |
/> | |
</Animated.View> | |
</TapGestureHandler> | |
</Animated.View> | |
</PanGestureHandler> | |
</Animated.View> | |
</PinchGestureHandler> | |
) | |
} | |
const styles = StyleSheet.create({ | |
pinchView: { | |
flex: 1, | |
backgroundColor: '#000', | |
justifyContent: 'center', | |
alignItems: 'center', | |
}, | |
panView: { | |
flex: 1, | |
justifyContent: 'center', | |
alignItems: 'center', | |
}, | |
}) | |
export default Photo |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment