Skip to content

Instantly share code, notes, and snippets.

@intergalacticspacehighway
Last active April 23, 2024 20:16
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save intergalacticspacehighway/9e931614199915cb4694209f12bf6f11 to your computer and use it in GitHub Desktop.
Save intergalacticspacehighway/9e931614199915cb4694209f12bf6f11 to your computer and use it in GitHub Desktop.
Pinch to zoom reanimated + gesture handler
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>
);
@ansh
Copy link

ansh commented Sep 22, 2023

This seems overly complicated.

@intergalacticspacehighway
Copy link
Author

This started simple believe me, but got complicated to support a continuous single pointer gesture after zooming in (which I thought was sweet, although no one would care much of it's existence) and an android edge case which is commented, for which I made 2 PRs. software-mansion/react-native-gesture-handler#1798
software-mansion/react-native-gesture-handler#1800

@ansh
Copy link

ansh commented Sep 22, 2023

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.

@intergalacticspacehighway
Copy link
Author

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!

@ansh
Copy link

ansh commented Sep 25, 2023

Nice, thanks for the link! Looking great :)

I'll see if I can simplify it and upload a gist

@surafelbm
Copy link

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment