4

I am using PanGestureHandler and PinchGestureHandler to allow a large image to be panned and zoomed in/out on the screen. However, once I introduce the scaling transformation, panning behaves differently.

I have three goals with this implementation:

  1. I am trying to scale down the image to fit a specific height when the view is first loaded. This is so the user can see as much of the image as possible. If you only apply a scale transformation to the image, translation values of 0 will not put the image in the upper left corner (as a result of the centered scale origin).
  2. I am trying to make it so that when someone uses the pinch gesture to zoom, the translation values are adjusted as well to make it seem as if the zoom origin is at the point where the user initiated the gesture (React Native only currently supports a centered origin for scale transformations). To accomplish this, I assume I will need to adjust the translation values when a user zooms (if the scale is anything other than 1).
  3. When panning after a zoom, I want the pan to track the user's finger (rather than moving faster/slower) by adjusting the translation values as they relate to the scale from the zoom.

Here is what I have so far:

import React, { useRef, useCallback } from 'react';
import { StyleSheet, Animated, View, LayoutChangeEvent } from 'react-native';
import {
    PanGestureHandler,
    PinchGestureHandler,
    PinchGestureHandlerStateChangeEvent,
    State,
    PanGestureHandlerStateChangeEvent,
} from 'react-native-gesture-handler';

const IMAGE_DIMENSIONS = {
    width: 2350,
    height: 1767,
} as const;

export default function App() {
    const scale = useRef(new Animated.Value(1)).current;
    const translateX = useRef(new Animated.Value(0)).current;
    const translateY = useRef(new Animated.Value(0)).current;
    const setInitialPanZoom = useCallback((event: LayoutChangeEvent) => {
        const { height: usableHeight } = event.nativeEvent.layout;
        const scaleRatio = usableHeight / IMAGE_DIMENSIONS.height;
        scale.setValue(scaleRatio);
        // TODO: should these translation values be set based on the scale?
        translateY.setValue(0);
        translateX.setValue(0);
    }, []);
    // Zoom
    const onZoomEvent = Animated.event(
        [
            {
                nativeEvent: { scale },
            },
        ],
        {
            useNativeDriver: true,
        }
    );
    const onZoomStateChange = (event: PinchGestureHandlerStateChangeEvent) => {
        if (event.nativeEvent.oldState === State.ACTIVE) {
            // Do something
        }
    };
    // Pan
    const handlePanGesture = Animated.event([{ nativeEvent: { translationX: translateX, translationY: translateY } }], {
        useNativeDriver: true,
    });
    const onPanStateChange = (_event: PanGestureHandlerStateChangeEvent) => {
        // Extract offset so that panning resumes from previous location, rather than resetting
        translateX.extractOffset();
        translateY.extractOffset();
    };
    return (
        <View style={styles.container}>
            <PanGestureHandler
                minPointers={1}
                maxPointers={1}
                onGestureEvent={handlePanGesture}
                onHandlerStateChange={onPanStateChange}
            >
                <Animated.View style={styles.imageContainer} onLayout={setInitialPanZoom}>
                    <PinchGestureHandler onGestureEvent={onZoomEvent} onHandlerStateChange={onZoomStateChange}>
                        <Animated.View style={{ transform: [{ scale }, { translateY }, { translateX }] }}>
                            <Animated.Image
                                source={require('./assets/my-image.png')}
                                style={{
                                    width: IMAGE_DIMENSIONS.width,
                                    height: IMAGE_DIMENSIONS.height,
                                }}
                                resizeMode="contain"
                            />
                        </Animated.View>
                    </PinchGestureHandler>
                </Animated.View>
            </PanGestureHandler>
        </View>
    );
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: '#fff',
        alignItems: 'center',
        justifyContent: 'center',
    },
    imageContainer: {
        flex: 1,
        backgroundColor: 'orange',
        width: '100%',
    },
});

I have tried something along the lines of subtracting the difference in dimensions from the translation values:

translateX.setValue(0 - (IMAGE_DIMENSIONS.width / 2) - (IMAGE_DIMENSIONS.width * scaleRatio / 2))

The numbers don't quite work with this implementation, so I'm probably not doing this right. Also, this would only account for my first goal, and I am guessing that I will need to do something like interpolate the translation values based on the scale value to accomplish the other two.

Conor Strejcek
  • 382
  • 2
  • 15

0 Answers0