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;
问题分析
- 动画样式错误:在
useAnimatedStyle中用withTiming包裹x.value,会导致拖拽时的实时位移被动画函数覆盖,无法响应手势的实时滑动。 - 切换逻辑缺失:
onEnd阶段只处理了位移动画,没有调用previousImage/nextImage来切换图片。 - 状态未重置:切换图片后没有重置
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
相关产品推荐
相关产品推荐

