Skip to content

Instantly share code, notes, and snippets.

@ikhsanalatsary
Forked from osamaqarem/PanPinchPhoto.tsx
Created March 5, 2021 11:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ikhsanalatsary/f79e77e116086beb83398055e5cc6742 to your computer and use it in GitHub Desktop.
Save ikhsanalatsary/f79e77e116086beb83398055e5cc6742 to your computer and use it in GitHub Desktop.
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