You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native实现左右滑动切换上/下一张图片的方案

React Native 左右滑动切换图片的手势动画实现方案

现有一个集成手势控制的React Native组件,代码可正常运行,但无法实现左右滑动切换图片的功能:要求右滑显示上一张图,左滑显示下一张图,同时呈现当前图退出、新图进入的动画效果。

原代码

import React, { useState } from 'react';
import { FlatList, Image, Modal, Text, View ,StyleSheet,Dimensions, TouchableOpacity} from 'react-native';
import { GestureHandlerRootView, PanGestureHandler, } from 'react-native-gesture-handler';
import Animated ,{useAnimatedStyle,useAnimatedGestureHandler, useSharedValue, withTiming,Easing} from 'react-native-reanimated';
  
const { width, height } = Dimensions.get('window');
// 假设arrayOfObjects是图片数据数组,结构为[{id: number, imageUrl: string}, ...]
const arrayOfObjects = [];

const GridListing = () => {
  const [selectedImage, setSelectedImage] = useState(null);
  const x = useSharedValue(0);

  const handleImagePress = (index) => {
    setSelectedImage(index);
  };

  const closeImagePreview = () => {
    setSelectedImage(null);
  }

  const nextImage = () => {
    const currentIndex = arrayOfObjects.findIndex((obj) => obj.id === selectedImage.id);
    if (currentIndex === arrayOfObjects.length - 1) {
      closeImagePreview();
    } else {
      setSelectedImage(arrayOfObjects[currentIndex + 1]);
    }
  }
  
  const previousImage = () => {
    const currentIndex = arrayOfObjects.findIndex((obj) => obj.id === selectedImage.id);
    if (currentIndex === 0) {
      closeImagePreview();
    } else {
      setSelectedImage(arrayOfObjects[currentIndex - 1]);
    }
  }

  const animationContainerStyle = useAnimatedStyle(() => ({
    transform:  [{ translateX: withTiming(x.value,{
      duration:100,
      easing:Easing.linear,
    }) }]
  }));

  const handleSwipe = useAnimatedGestureHandler({
    onActive: (event) => {
      if (Math.abs(event.translationX) > Math.abs(event.translationY)) {
        // Horizontal swipe
        if (event.translationX > 0) {
          console.log('Swiping right');
          x.value = event.translationX

        } else {
          console.log('Swiping left',event.translationX);
          x.value = event.translationX
      
        }
      } else {
        // Vertical swipe
        if (event.translationY > 0) {
          console.log('Swiping down');
        } else {
          console.log('Swiping up');
        }
      }
    },
    onEnd:()=>{
      if (x.value>width/2) {
        x.value = withTiming(width,{easing:Easing.linear}) 
        console.log("hey")
        const currentIndex = arrayOfObjects.findIndex(obj => obj.id === selectedImage?.id);
        console.log("currentIndex ",currentIndex)
        
      
        // setSelectedImage(arrayOfObjects[(currentIndex + 1) % arrayOfObjects.length]);
      }else if (x.value < -width / 2) {
        x.value = withTiming(-width,{easing:Easing.linear}) 
        console.log("ho")
      }else{
        x.value=withTiming(0,{easing:Easing.linear}) 
        console.log("oo")
      }
    }
  })

  return (
    <View style={styles.container}>
      <FlatList
        data={arrayOfObjects}
        renderItem={({ item, index }) => (
          <TouchableOpacity style={styles.itemContainer} onPress={() => handleImagePress(item)}>
            <Image style={styles.thumbnail} source={{ uri: item.imageUrl }} />
             </TouchableOpacity>
        )}
        numColumns={3}
        keyExtractor={(item) => item.id.toString()}
      />
      <Modal
        transparent
        visible={selectedImage !== null}
        onRequestClose={() => setSelectedImage(null)}
        style={styles.modalContainer}
      >
        <GestureHandlerRootView style={{flex:1}}> 
          <PanGestureHandler onGestureEvent={handleSwipe}>
            <Animated.View style={[styles.modalContainer,animationContainerStyle]} >
          
            <Image
                source={{ uri: selectedImage?.imageUrl }}
                style={{ width: '100%', height: '100%' }}
                resizeMode="contain"
              />
            </Animated.View>


          </PanGestureHandler>
      </GestureHandlerRootView>
      </Modal>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 4,
  },
  itemContainer: {
    flex: 1,
    margin: 2,
    aspectRatio: 1,
  },
  thumbnail: {
    width: '100%',
    height: '100%',
    borderRadius: 8,
  },
  modalContainer: {
    flex: 1,
    backgroundColor: 'rgba(0,0,0,0.9)',
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default GridListing;

问题分析

  1. 动画样式错误:在useAnimatedStyle中用withTiming包裹x.value,会导致拖拽时的实时位移被动画函数覆盖,无法响应手势的实时滑动。
  2. 切换逻辑缺失:onEnd阶段只处理了位移动画,没有调用previousImage/nextImage来切换图片。
  3. 状态未重置:切换图片后没有重置x.value为0,导致新图片会带着之前的动画位移状态。

修复后的完整代码

import React, { useState, useEffect } from 'react';
import { FlatList, Image, Modal, View, StyleSheet, Dimensions, TouchableOpacity } from 'react-native';
import { GestureHandlerRootView, PanGestureHandler } from 'react-native-gesture-handler';
import Animated, {
  useAnimatedStyle,
  useAnimatedGestureHandler,
  useSharedValue,
  withTiming,
  Easing,
  runOnJS,
} from 'react-native-reanimated';

const { width, height } = Dimensions.get('window');
// 示例图片数据,替换为你的实际数据
const arrayOfObjects = [
  { id: 1, imageUrl: 'https://picsum.photos/600/800?random=1' },
  { id: 2, imageUrl: 'https://picsum.photos/600/800?random=2' },
  { id: 3, imageUrl: 'https://picsum.photos/600/800?random=3' },
];

const GridListing = () => {
  const [selectedImage, setSelectedImage] = useState(null);
  const x = useSharedValue(0);

  // 切换图片后重置动画位移
  useEffect(() => {
    x.value = 0;
  }, [selectedImage]);

  const handleImagePress = (item) => {
    setSelectedImage(item);
  };

  const closeImagePreview = () => {
    setSelectedImage(null);
  };

  const nextImage = () => {
    const currentIndex = arrayOfObjects.findIndex((obj) => obj.id === selectedImage.id);
    if (currentIndex === arrayOfObjects.length - 1) {
      closeImagePreview();
    } else {
      setSelectedImage(arrayOfObjects[currentIndex + 1]);
    }
  };

  const previousImage = () => {
    const currentIndex = arrayOfObjects.findIndex((obj) => obj.id === selectedImage.id);
    if (currentIndex === 0) {
      closeImagePreview();
    } else {
      setSelectedImage(arrayOfObjects[currentIndex - 1]);
    }
  };

  // 移除withTiming,确保拖拽时实时响应位移
  const animationContainerStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: x.value }],
  }));

  const handleSwipe = useAnimatedGestureHandler({
    onActive: (event) => {
      // 只处理水平滑动(忽略垂直滑动)
      if (Math.abs(event.translationX) > Math.abs(event.translationY)) {
        x.value = event.translationX;
      }
    },
    onEnd: () => {
      // 判断滑动距离是否达到切换阈值(屏幕宽度的1/3)
      if (x.value > width / 3) {
        // 右滑:执行退出动画后切换上一张
        x.value = withTiming(width, { duration: 200, easing: Easing.ease }, () => {
          runOnJS(previousImage)();
        });
      } else if (x.value < -width / 3) {
        // 左滑:执行退出动画后切换下一张
        x.value = withTiming(-width, { duration: 200, easing: Easing.ease }, () => {
          runOnJS(nextImage)();
        });
      } else {
        // 滑动距离不足,回弹到原位
        x.value = withTiming(0, { duration: 200, easing: Easing.ease });
      }
    },
  });

  return (
    <View style={styles.container}>
      <FlatList
        data={arrayOfObjects}
        renderItem={({ item }) => (
          <TouchableOpacity style={styles.itemContainer} onPress={() => handleImagePress(item)}>
            <Image style={styles.thumbnail} source={{ uri: item.imageUrl }} />
          </TouchableOpacity>
        )}
        numColumns={3}
        keyExtractor={(item) => item.id.toString()}
      />
      <Modal
        transparent
        visible={selectedImage !== null}
        onRequestClose={closeImagePreview}
      >
        <GestureHandlerRootView style={{ flex: 1 }}>
          <PanGestureHandler onGestureEvent={handleSwipe}>
            <Animated.View style={[styles.modalContainer, animationContainerStyle]}>
              <Image
                source={{ uri: selectedImage?.imageUrl }}
                style={{ width: '100%', height: '100%' }}
                resizeMode="contain"
              />
            </Animated.View>
          </PanGestureHandler>
        </GestureHandlerRootView>
      </Modal>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 4,
  },
  itemContainer: {
    flex: 1,
    margin: 2,
    aspectRatio: 1,
  },
  thumbnail: {
    width: '100%',
    height: '100%',
    borderRadius: 8,
  },
  modalContainer: {
    flex: 1,
    backgroundColor: 'rgba(0,0,0,0.9)',
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default GridListing;

关键修改点说明

  • 修复动画样式:移除useAnimatedStyle中的withTiming,让x.value直接响应手势的实时位移,保证拖拽时的流畅性。
  • 补充切换逻辑:在onEnd阶段,根据滑动方向调用previousImage或nextImage,并通过runOnJS在动画结束后执行React状态更新。
  • 重置动画状态:通过useEffect监听selectedImage变化,切换图片后自动将x.value重置为0,确保新图片从屏幕中央开始显示。
  • 调整滑动阈值:将切换阈值改为屏幕宽度的1/3,更符合用户的滑动习惯。

内容的提问来源于stack exchange,提问作者Ahmed Yasin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 20:43:51