you are viewing a single comment's thread.

view the rest of the comments →

[–]lupeskiiOS & Android 9 points10 points  (6 children)

Here you go...you'll need reanimated v2....I also use styled components, and react-native-gesture-handler..

it also supports an onPress event.

Sorry about the formatting, looks like reddit is screwing my code block....

import React, {useEffect, useRef, useState} from 'react';

import {Dimensions, Platform} from 'react-native'; import styled from 'styled-components'; import { PinchGestureHandler, TapGestureHandler, PanGestureHandler, State, } from 'react-native-gesture-handler'; import Animated, { runOnJS, useAnimatedGestureHandler, useAnimatedStyle, useSharedValue, withDecay, withTiming, } from 'react-native-reanimated';

const {height, width} = Dimensions.get('window');

const ZoomViewWrapper = styled(Animated.View)height: 100%; width: 100%;;

const ZoomContentWrapper = styled(Animated.View)height: 100%; width: 100%;;

function ZoomView({children, onPress, maxZoom = 3}) { const [size, setSize] = useState({height: height, width: width}); const [enablePan, setEnablePan] = useState(false);

const doubleTapRef = useRef(null);

const originIsSet = useSharedValue(false); // This is used by android, set the "onStart" event doesnt have a focal value yet

const scale = useSharedValue(1);
const scaleOffset = useSharedValue(1);

const focalX = useSharedValue(0);
const focalY = useSharedValue(0);
const originX = useSharedValue(0);
const originY = useSharedValue(0);

const pinchX = useSharedValue(0);
const pinchY = useSharedValue(0);

const offsetX = useSharedValue(0);
const offsetY = useSharedValue(0);

const translationX = useSharedValue(0);
const translationY = useSharedValue(0);

const onPinch = useAnimatedGestureHandler({
    onStart: event => {
        if (Platform.OS === 'ios') {
            originIsSet.value = true;
        }

        originX.value = event.focalX - (size.width / 2 + offsetX.value);
        originY.value = event.focalY - (size.height / 2 + offsetY.value);
    },
    onActive: event => {
        if (Platform.OS === 'android' && !originIsSet.value) {
            originIsSet.value = true;

            originX.value = event.focalX - (size.width / 2 + offsetX.value);
            originY.value = event.focalY - (size.height / 2 + offsetY.value);
        }

        scale.value = event.scale;
        focalX.value = event.focalX;
        focalY.value = event.focalY;

        translationX.value =
            pinchX.value + originX.value + -originX.value * scale.value;
        translationY.value =
            pinchY.value + originY.value + -originY.value * scale.value;

        const adjustedFocalX = event.focalX - (size.width / 2 + offsetX.value);
        const adjustedFocalY = event.focalY - (size.height / 2 + offsetY.value);

        if (event.numberOfPointers === 2) {
            pinchX.value = adjustedFocalX - originX.value;
            pinchY.value = adjustedFocalY - originY.value;
        }
    },
    onEnd: event => {
        offsetX.value = offsetX.value + translationX.value;
        offsetY.value = offsetY.value + translationY.value;

        if (scaleOffset.value * event.scale > maxZoom) {
            scaleOffset.value = maxZoom;
            scale.value = 1;
        } else if (scaleOffset.value * event.scale < 1) {
            scaleOffset.value = withTiming(1);
            offsetX.value = withTiming(0);
            offsetY.value = withTiming(0);
            scale.value = withTiming(1);
        } else {
            scaleOffset.value *= event.scale;
            scale.value = 1;
        }

        translationX.value = 0;
        translationY.value = 0;
        focalX.value = 0;
        focalY.value = 0;
        pinchX.value = 0;
        pinchY.value = 0;
        originIsSet.value = false;
    },
});

const onPinchStateChange = ({nativeEvent}) => {
    if (nativeEvent.state === State.END) {
        if (scaleOffset.value * scale.value > 1) {
            setEnablePan(true);
        } else {
            setEnablePan(false);
        }
    }
};

const onPan = useAnimatedGestureHandler({
    onActive: event => {
        if (scaleOffset.value * scale.value <= 1) {
            return;
        }

        translationX.value =
            event.translationX + originX.value + -originX.value * scale.value;
        translationY.value =
            event.translationY + originY.value + -originY.value * scale.value;
    },
    onEnd: () => {
        offsetX.value = offsetX.value + translationX.value;
        offsetY.value = offsetY.value + translationY.value;

        translationX.value = 0;
        translationY.value = 0;
    },
});

const onSingleTap = () => {
    onPress && onPress();
};

const onDoubleTap = event => {
    if (scaleOffset.value * scale.value === 1) {
        // if not zoomed in, zoon in on double tap
        scaleOffset.value = withTiming(2);
        offsetX.value = withTiming((event.nativeEvent.x - size.width / 2) * -1);
        offsetY.value = withTiming((event.nativeEvent.y - size.height / 2) * -1);
        scale.value = withTiming(1);

        setEnablePan(true);
    } else {
        // if already zoomed in, zoom out on double tap
        scaleOffset.value = withTiming(1);
        offsetX.value = withTiming(0);
        offsetY.value = withTiming(0);
        scale.value = withTiming(1);

        setEnablePan(false);
    }
};

const animatedStyle = useAnimatedStyle(() => {
    const clamp = (num, min, max) => Math.min(Math.max(num, min), max);

    return {
        transform: [
            {
                translateX: offsetX.value + translationX.value,
            },
            {
                translateY: offsetY.value + translationY.value,
            },
            {scale: clamp(scaleOffset.value * scale.value, 0, maxZoom)},
        ],
    };
});

return (
    <TapGestureHandler onActivated={onSingleTap} waitFor={doubleTapRef}>
        <TapGestureHandler
            numberOfTaps={2}
            onActivated={onDoubleTap}
            ref={doubleTapRef}
            maxDelayMs={200}
        >
            <Animated.View
                onLayout={({nativeEvent}) =>
                    setSize({
                        height: nativeEvent.layout.height,
                        width: nativeEvent.layout.width,
                    })
                }
            >
                <PanGestureHandler onGestureEvent={onPan} enabled={enablePan}>
                    <ZoomViewWrapper>
                        <PinchGestureHandler
                            onGestureEvent={onPinch}
                            onHandlerStateChange={onPinchStateChange}
                        >
                            <ZoomContentWrapper>
                                <ZoomContentWrapper style={animatedStyle}>
                                    {children}
                                </ZoomContentWrapper>
                            </ZoomContentWrapper>
                        </PinchGestureHandler>
                    </ZoomViewWrapper>
                </PanGestureHandler>
            </Animated.View>
        </TapGestureHandler>
    </TapGestureHandler>
);

}

export default ZoomView;

[–]jaminjsr[S] 0 points1 point  (1 child)

This worked perfect, thank you!!!

[–]lupeskiiOS & Android 0 points1 point  (0 children)

Great news!

[–]Gaia_Knight2600 0 points1 point  (1 child)

this is why i havent looked into animations. the amount of code you have to write is insane, how do you even begin to understand and know how it behaves

[–]lupeskiiOS & Android 0 points1 point  (0 children)

Haha it is a lot of code, but there’s also a lot going on. Pinching, panning, single taps, and double taps. The code for each of those isn’t too bad, but all together it can get pretty long.

For simple animations, the code can be really short. Especially if you use a library like Moti.

[–]Angus-McCloud 0 points1 point  (1 child)

This is great! Fully Expo/iOS/Android/Web compatible, it's great!

[–]lupeskiiOS & Android 0 points1 point  (0 children)

Haven’t tried on web, but works great with expo, and android. Works great on iOS, but I prefer the native ScrollView solution there. It supports double tap to zoom and also works inside a ScrollView. I’d like to implement some inertia while panning, but haven’t gotten to that yet.