-
-
Save intergalacticspacehighway/9e931614199915cb4694209f12bf6f11 to your computer and use it in GitHub Desktop.
import React, { useMemo, useState } from "react"; | |
import { LayoutChangeEvent, StyleSheet } from "react-native"; | |
import { | |
PinchGestureHandler, | |
PinchGestureHandlerGestureEvent, | |
} from "react-native-gesture-handler"; | |
import Animated, { | |
useAnimatedGestureHandler, | |
useAnimatedStyle, | |
useSharedValue, | |
withSpring, | |
} from "react-native-reanimated"; | |
const useLayout = () => { | |
const [layout, setLayout] = useState< | |
LayoutChangeEvent["nativeEvent"]["layout"] | undefined | |
>(); | |
const onLayout = (e) => { | |
setLayout(e.nativeEvent.layout); | |
}; | |
return { onLayout, layout }; | |
}; | |
export const PinchToZoom = ({ children }) => { | |
const scale = useSharedValue(1); | |
const origin = { x: useSharedValue(0), y: useSharedValue(0) }; | |
const translation = { x: useSharedValue(0), y: useSharedValue(0) }; | |
const { onLayout, layout } = useLayout(); | |
const handler = useAnimatedGestureHandler<PinchGestureHandlerGestureEvent>({ | |
onStart(e, ctx: any) { | |
// On android, we get focalX and focalY 0 in onStart callback. So, use a flag and set initial focalX and focalY in onActive | |
// 😢 https://github.com/software-mansion/react-native-gesture-handler/issues/546 | |
ctx.start = true; | |
}, | |
onActive(e, ctx: any) { | |
if (ctx.start) { | |
origin.x.value = e.focalX; | |
origin.y.value = e.focalY; | |
ctx.offsetFromFocalX = origin.x.value; | |
ctx.offsetFromFocalY = origin.y.value; | |
ctx.prevTranslateOriginX = origin.x.value; | |
ctx.prevTranslateOriginY = origin.y.value; | |
ctx.prevPointers = e.numberOfPointers; | |
ctx.start = false; | |
} | |
scale.value = e.scale; | |
if (ctx.prevPointers !== e.numberOfPointers) { | |
ctx.offsetFromFocalX = e.focalX; | |
ctx.offsetFromFocalY = e.focalY; | |
ctx.prevTranslateOriginX = ctx.translateOriginX; | |
ctx.prevTranslateOriginY = ctx.translateOriginY; | |
} | |
ctx.translateOriginX = | |
ctx.prevTranslateOriginX + e.focalX - ctx.offsetFromFocalX; | |
ctx.translateOriginY = | |
ctx.prevTranslateOriginY + e.focalY - ctx.offsetFromFocalY; | |
translation.x.value = ctx.translateOriginX - origin.x.value; | |
translation.y.value = ctx.translateOriginY - origin.y.value; | |
ctx.prevPointers = e.numberOfPointers; | |
}, | |
onEnd() { | |
scale.value = withSpring(1, { | |
stiffness: 60, | |
overshootClamping: true, | |
}); | |
translation.x.value = withSpring(0, { | |
stiffness: 60, | |
overshootClamping: true, | |
}); | |
translation.y.value = withSpring(0, { | |
stiffness: 60, | |
overshootClamping: true, | |
}); | |
}, | |
}); | |
const imageLeftForSettingTransformOrigin = layout ? -layout.height / 2 : 0; | |
const imageTopForSettingTransformOrigin = layout ? -layout.width / 2 : 0; | |
const animatedStyles = useAnimatedStyle(() => { | |
return { | |
transform: [ | |
{ translateX: translation.x.value }, | |
{ | |
translateY: translation.y.value, | |
}, | |
{ translateX: imageLeftForSettingTransformOrigin + origin.x.value }, | |
{ translateY: imageTopForSettingTransformOrigin + origin.y.value }, | |
{ | |
scale: scale.value, | |
}, | |
{ translateX: -(imageLeftForSettingTransformOrigin + origin.x.value) }, | |
{ translateY: -(imageTopForSettingTransformOrigin + origin.y.value) }, | |
], | |
}; | |
}, [imageTopForSettingTransformOrigin, imageLeftForSettingTransformOrigin]); | |
const clonedChildren = useMemo( | |
() => | |
React.cloneElement(children, { | |
style: [StyleSheet.flatten(children.props.style), animatedStyles], | |
}), | |
[children] | |
); | |
return ( | |
<PinchGestureHandler onGestureEvent={handler}> | |
<Animated.View onLayout={onLayout}>{clonedChildren}</Animated.View> | |
</PinchGestureHandler> | |
); | |
}; | |
const Example = () => ( | |
<PinchToZoom> | |
<Animated.Image | |
style={{ width: 277, height: 368 }} | |
source={{ | |
uri: "https://images.unsplash.com/photo-1536152470836-b943b246224c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=876&q=80", | |
}} | |
/> | |
</PinchToZoom> | |
); |
yes, this one was created a while ago. Here is the updated one (it looks a bit more complicated though). I think the reason I didn't use Pan, Pinch, and Rotate was again due to the continuous single-pointer after the zooming gesture that I talked about (it was getting weird with simultaneous, I forgot the exact issue I faced though 😅). It is not that important and the logic can surely be simplified if we don't support that. If I were to re-implement it now, I'd surely simplify it!
Nice, thanks for the link! Looking great :)
I'll see if I can simplify it and upload a gist
@intergalacticspacehighway @ansh Hey thanks for providing this, can you please take a look at this question i posted, been struggling with this
https://stackoverflow.com/questions/77288163/react-native-pinch-gesture-handler-zoom-position
Interesting. Thank you for your response.
What I am confused about is why did you use the old way of doing things with PinchGestureHandler? You could create a combined new Gesture with the new Gesture API in RNGH.
So you could do:
Gesture.Pan() and Gesture.Pinch() and Gesture.Rotate() and then combine them with Gesture.Simultaneous(Pan, Pinch, Rotate). This way you’d be able to get exactly what you need without deriving it.