How to land on the image a user has clicked in image swipe?

794 Views Asked by At

My React Native 0.64 app uses react-native-gesture-handler 1.16/react-native-reanimated 2.1 to swipe images left and right. The idea is when user clicks a image in image gallery, then the image was displayed occupying the full screen and the rest of images in the gallery can be viewed one by one by swipe left or right. Here is the full code which swipe works but with one catch: the image clicked is not presented but the first image in gallery is always shown first. How to land on the image a user has clicked?

import React, { useRef } from "react";
import { Dimensions, StyleSheet, View, Platform } from "react-native";
import FastImage from 'react-native-fast-image';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  useAnimatedRef,
  useAnimatedGestureHandler,
  useAnimatedReaction,
  withTiming,
  withSpring,
  useDerivedValue,
} from "react-native-reanimated";
import {
  snapPoint,
} from "react-native-redash";
import {
  PanGestureHandler,
  PinchGestureHandler,
  State,
  TouchableOpacity,
} from "react-native-gesture-handler";

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

export default Swipe = ({route, navigation}) => {
    const {images, initIndex} = route.params;  //initIndex is the index of image clicked
    console.log("route params swipe ", route.params);
    const index = useSharedValue(initIndex);
    const snapPoints = images.map((_, i) => i * -width);
    const panRef = useAnimatedRef();
    const pinchRef = useAnimatedRef();
    const offsetX = initIndex ? useSharedValue(initIndex * -width) : useSharedValue(0);  //The init offsetX is set according to initIndex a user clicked.
    const translationX = useSharedValue(0);
    const translateX = useSharedValue(0); //if translateX was assgined with offsetX, then swipe stops working
    //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 panHandler = useAnimatedGestureHandler(
        {
         onStart: () => {
            translateX.value = offsetX.value;  //As soon as a user swipe, this line allow the 
 //image is moved to initIndex-th image. But how to land on the image a user has clicked?
         },
         onActive: (event) => {
            translateX.value = offsetX.value + event.translationX;
            translationX.value = event.translationX;
         },
         onEnd: (event) => {
            let val = snapPt(event);
            let sp = Math.min(Math.max(lowVal.value, val), highVal.value);  //clamp in RN redash
            if (event.translationX !== 0) {
                translateX.value = withTiming(sp);
                offsetX.value = translateX.value;
            };
            //update index
            index.value = Math.floor(translateX.value/-width);          
         }, 
        }, []
    )

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

    return (
        <PanGestureHandler
            onGestureEvent={panHandler}
            ref={panRef}
            minDist={10}
            avgTouches
        >
        <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});
                    
                    return (
                    <View key={i} style={styles.picture}>
                        <View style={[
                            styles.image,
                        ]}>
                          <TouchableOpacity onPress={() => navigation.goBack()} style={styles.button}>
                            <View style={styles.button}>
                              <FastImage 
                                          source={{uri:img_source.path}} 
                                          resizeMode={FastImage.resizeMode.contain} 
                                          style={styles.image}
                              />
                            </View>
                          </TouchableOpacity>
                        </View>
                    </View>
                    );
                })}
                
                </Animated.View>
            </Animated.View>
            </PanGestureHandler>    
    );
}

const styles = StyleSheet.create({
container: {
  ...StyleSheet.absoluteFillObject,
  backgroundColor: "black",
},

picture: {
  width,
  height,
  overflow: "hidden",
},
image: {
  //...StyleSheet.absoluteFillObject,
  flex:1,
  width: undefined,
  height: undefined,
  //resizeMode: "cover",
},
button: {
  width: width,
  height:height,
},

});

Another issue the code is that when touching the image before swipe the image jerks left or right 1/3-1/2 screen width. This makes the swipe un-smooth. I would like to know how to eliminate the image jerk and make the swipe smooth from beginning to the end.

1

There are 1 best solutions below

0
On

To land on the image a user has clicked, a useEffect hook is added :

useEffect(() => {
        translateX.value = offsetX.value;
    }, [initIndex]);

The onStart needs to be removed completely.

The reason for image jerking is caused by misrepresenting of offsetX value. The offsetX shall be at the border of the screen (multiple of the screen width). However when the image width is less than the screen width, the offsetX may be at the border of the image which is a little less or more (depending on if swipe left or swipe right) and this causes the image jerking left or right when swiping initially. In this case, the offsetX needs to be set to the border of the screen (multiple of screen width).