1

Here is a Swipe component implemented with React native 0,70.1 and react native gesture handler 2.8 and react native reanimated 3.0. What the Swipe does is to allow user to swipe images in a gallery right or left while allowing user to zoom an image with pinch gesture:

import React, { useEffect, useMemo} from "react";
import { Dimensions, StyleSheet, View, Platform, TouchableWithoutFeedback } from "react-native";
//import FastImage from 'react-native-fast-image';
//import CachedImage from 'react-native-image-cache-wrapper';
import CachedImage from '../app/CachedImage';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  useAnimatedRef,
  //useAnimatedGestureHandler,
  //useAnimatedReaction,
  withTiming,
  withSpring,
  useDerivedValue,
  runOnJS,
  runOnUI
} from "react-native-reanimated";
import {
  snapPoint,
  useVector,
} from "react-native-redash";
import {
  Gesture,
  GestureDetector,
  GestureHandlerRootView,
} from "react-native-gesture-handler";

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

export default Swipe = ({route, navigation}) => {
    const {images, initIndex} = route.params;
    console.log("route params screen dim swipe ", `${width}, ${height}`);
    const index = useSharedValue(initIndex);
    const snapPoints = images.map((_, i) => i * -width);
    const offsetX = useSharedValue(initIndex * -width); //initIndex ? useSharedValue(initIndex * -width) : useSharedValue(0);
    const translationX = useSharedValue(0);
    const translateX = useSharedValue(0);
    //pinch
    const scale = useSharedValue(1);
    const focalX = useSharedValue(0);
    const focalY = useSharedValue(0);
  

    //for snap in swipe
    const lowVal = useDerivedValue(() => {return (index.value + 1) * -width}); //shared value, with .value
    const highVal = useDerivedValue(() => {return (index.value - 1) * -width});  //shared value, with .value
    const snapPt = (e) => {
        "worklet"; 
        return snapPoint(translateX.value, e.velocityX, snapPoints);
    };

    const pan = useMemo(() => Gesture.Pan()
      .onUpdate((event) => {
        translateX.value = offsetX.value + event.translationX;
        translationX.value = event.translationX;
        console.log("pan event : ", event);
        //console.log("scale.value : ", scale.value);
      })
      .onEnd((event)=>{
        let val = snapPt(event);
        //console.log("snapPT : ", val);
        let sp = Math.min(Math.max(lowVal.value, val), highVal.value);  //clamp in RN redash
        //console.log("snapTo : ", sp);
        //console.log("event.translationX : ", event.translationX);
        if (event.translationX !== 0) {
            translateX.value = withTiming(sp);
            offsetX.value = event.translationX < 0 ? Math.max(Math.min(translateX.value, sp), (images.length - 1) * -width) : Math.min(Math.max(translateX.value, sp), 0);  
            //console.log("onEnd offsetX.value : ", offsetX.value);
        
        };
        //update index
        //console.log("translateX.value : ", translateX.value);
        index.value = Math.abs(sp)/width; //Math.floor(translateX.value/-width);  
        console.log("onEnd offsetx.value : ", offsetX.value)    
      }),[]);

    const pinch = useMemo(() => Gesture.Pinch()
      .onUpdate((event) => {
        console.log("pinch event : ", event);
        focalX.value = event.focalX - width / 2;
        focalY.value = event.focalY - height / 2;
        console.log("index in pinch :", `${focalX.value}, ${focalY.value}, ${index.value}`);
        //
        scale.value = event.scale;
        
      })
      .onEnd((event) => {
        scale.value = withTiming(1);
        focalX.value = withTiming(offsetX.value);
        focalY.value = withTiming(0);
        
      })
      .simultaneousWithExternalGesture(pan)   //work together with pan gesture
    );

    const swipeStyle = useAnimatedStyle(() => {
        return {
            transform: [{ translateX: translateX.value}]  //for swipe
        }
    }, []);



    //display the image a user has clicked
    useEffect(() => {
        translateX.value = offsetX.value;
    }, [initIndex]);

    return (
      <GestureHandlerRootView>
        <GestureDetector gesture={pinch}>
          <GestureDetector gesture={pan}>
            <Animated.View style={styles.container}>
                  <Animated.View style={[{width: width * images.length, height, flexDirection: "row"}, swipeStyle]}>
                      
                    {images.map((img_source, i) => {
                        //const isActive = useDerivedValue(() => {return index.value === i});
                        const pinchStyle = useAnimatedStyle(() => {
                          return {
                            transform: [
                              
                              //{ translateX: focalX.value },
                              //{ translateY: focalY.value },
                              { translateX: (-width / 2 + focalX.value + offsetX.value)},
                              { translateY: (-height / 2 + focalY.value)},
                              { scale: scale.value },
                              { translateX: (width / 2 - focalX.value - offsetX.value)},
                              { translateY: (height / 2 - focalY.value)},
                              
                              
                            ],
                          };
                        }, []); 
                        return (
                        <Animated.View key={i} style={[styles.picture, pinchStyle]}>
                            <View style={[styles.image]}>
                              <TouchableWithoutFeedback onPress={() => navigation.goBack()} style={styles.button}>
                                <View style={styles.button}>
                                  <CachedImage 
                                              source={{uri:img_source.path.split("?")[0]}} 
                                              sourceAlt={{uri:img_source.path}} 
                                              resizeMode={"center"}
                                              style={[styles.image]}
                                  />
                                </View>
                              </TouchableWithoutFeedback>
                            </View>
                        </Animated.View>
                        );
                    })}
                    
                    </Animated.View>
                </Animated.View>
              </GestureDetector> 
            </GestureDetector> 
        </GestureHandlerRootView>  
    );
}

Here is the 2 images which are bing swiped:

enter image description here

When pinch to zoom on 2nd image, part of the 1st image was displayed as well. How to fix this in pinch style?

enter image description here

user938363
  • 9,990
  • 38
  • 137
  • 303
  • one way to fix this is removing pinch gesture from this component and creating a separate component only for images which are being mapped. And wrapping this image component with pinch gesture, In this way each mapped image will have separate gesture detector. – Rohit S K Jun 06 '23 at 06:19

1 Answers1

1

Flip the sign of offsetX in pinchStyle solved the problem. The problem is that adding offsetX on translateX makes the broader line to the middle of 1st image and therefore part of the 1st image goes into the pinch to zoom on 2nd image.

  const pinchStyle = useAnimatedStyle(() => {
      return {
          transform: [
                         
              { translateX: (-width / 2 + focalX.value - offsetX.value)}, //<<== change to '-' offsetX. 
              { translateY: (-height / 2 + focalY.value)},
                          
              { scale: scale.value },
              { translateX: (width / 2 - focalX.value + offsetX.value)}, //<<== change to '+' offsetX
              { translateY: (height / 2 - focalY.value)},
                          
                          
             ],
            };
       }, []); 
user938363
  • 9,990
  • 38
  • 137
  • 303