-
-
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.