React Native中如何带延迟依次渲染组件并实现平滑过渡?
问题
我有一个包含3个元素的数据数组,每个元素需要映射为一个组件:
const arr = [ { id: 1, storeName: 'papa jones besiktas', }, { id: 2, storeName: 'levent store', }, { id: 3, storeName: 'sariyer store', }, ];
需求是让每个组件在View中停留指定时长后切换显示下一个组件,同时实现平滑过渡效果——新组件平滑进入、旧组件平滑退出,避免生硬替换。我已经处理了部分动画逻辑,但不知道怎么实现这种平滑渲染的方式,请问在React Native中该怎么做?以下是我当前的代码:
const [curr, setCurr] = useState(0); const [listData, setListData] = useState([ { id: 1, storeName: 'papa jones besiktas', }, { id: 2, storeName: 'levent store', }, { id: 3, storeName: 'sariyer store', }, ]); const [renderComponentsList, setRenderComponentsList] = useState([]); const {colors} = useTheme(); let bounceLittleItem = useRef(new Animated.Value(-80)).current; let bounceBigItem = useRef(new Animated.Value(-100)).current; let scaleUp = useRef(new Animated.Value(0.5)).current; const styles = useMemo( () => StyleSheet.create({ mainContainer: { backgroundColor: colors.CONTRAST_PRIMARY, height: 100, borderRadius: 30, marginHorizontal: `${(100 - GUTTERS.SCREEN_WIDTH_IN_NUMBER) / 2}%`, flexDirection: 'column', alignItems: 'flex-start', justifyContent: 'center', paddingHorizontal: 15, marginTop: 25, overflow: 'hidden', }, orderContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', }, storeImage: { width: 45, height: 45, backgroundColor: 'yellow', borderRadius: 40, marginRight: 20, }, orderStatusText: { color: '#fff', fontFamily: 'BalooPaaji2-SemiBold', }, storeContainer: { backgroundColor: '#fff', paddingHorizontal: 10, paddingVertical: 5, borderRadius: 20, }, storeOneBeforeLastImage: { width: 25, height: 25, backgroundColor: 'yellow', borderRadius: 40, // marginRight: 20, opacity: 0.5, }, }), [colors], ); useEffect(() => { const inter = setInterval(() => { let i = 1; setCurr((prev) => (prev + i > listData.length - 1 ? 0 : prev + i)); const found = listData.find((el, index) => index === curr); setRenderComponentsList((prevState) => [...prevState, found]); i = i + 1 > listData.length ? 0 : i + 1; }, 3000); return () => clearInterval(inter); }, [curr, listData]); useEffect(() => { Animated.timing(bounceBigItem, { toValue: -30, duration: 2000, useNativeDriver: true, }); }, [bounceBigItem]); const runAnimation = useCallback(() => { Animated.sequence([ Animated.timing(bounceLittleItem, { toValue: -30, duration: 2000, useNativeDriver: true, }), Animated.parallel([ Animated.timing(bounceLittleItem, { toValue: 20, duration: 1000, useNativeDriver: true, }), Animated.timing(scaleUp, { toValue: 1, duration: 1000, useNativeDriver: true, }), ]), Animated.delay(2000), Animated.timing(bounceLittleItem, { toValue: 100, duration: 1000, useNativeDriver: true, }), ]).start(() => { bounceLittleItem.setValue(-80); scaleUp.setValue(0.5); runAnimation(); }); }, [bounceLittleItem, scaleUp]); useEffect(() => runAnimation(), [runAnimation]); const renderViewItem = useCallback( (index) => { if (renderComponentsList?.length === 0) return; return renderComponentsList.map((el) => { return ( <Animated.View key={el.id} style={[ styles.orderContainer, { transform: [{translateY: bounceLittleItem}, {scale: scaleUp}], }, ]} > <View style={[styles.storeImage]} /> <Text style={styles.orderStatusText}>{el.storeName}</Text> </Animated.View> ); }) }, [bounceLittleItem, listData, scaleUp, styles.orderContainer, styles.orderStatusText, styles.storeImage], ); return <View style={styles.mainContainer}>{renderViewItem(curr)}</View>; };
解决方案
你的代码目前存在几个核心问题:定时器逻辑混乱导致状态更新异常、渲染列表不断累加组件、动画与组件切换不同步。下面是修正后的实现思路和代码:
核心调整点
- 简化状态管理:不需要维护
renderComponentsList,直接通过索引定位当前/上一个要显示的组件,减少冗余状态。 - 同步动画与组件切换:让动画流程控制组件索引更新,确保旧组件退出、新组件进入的过渡完全同步。
- 独立动画变量:为当前和上一个组件分别设置动画变量,实现双向过渡效果。
完整修正代码
import React, { useState, useEffect, useRef, useCallback } from 'react'; import { View, Text, StyleSheet, Animated } from 'react-native'; import { useTheme } from '@react-navigation/native'; const StoreCarousel = () => { const [currIndex, setCurrIndex] = useState(0); const [prevIndex, setPrevIndex] = useState(null); const listData = [ { id: 1, storeName: 'papa jones besiktas' }, { id: 2, storeName: 'levent store' }, { id: 3, storeName: 'sariyer store' }, ]; const { colors } = useTheme(); // 为新旧组件分别定义动画变量 const currTranslateY = useRef(new Animated.Value(-80)).current; const currScale = useRef(new Animated.Value(0.5)).current; const prevTranslateY = useRef(new Animated.Value(0)).current; const prevScale = useRef(new Animated.Value(1)).current; const styles = StyleSheet.create({ mainContainer: { backgroundColor: colors.CONTRAST_PRIMARY, height: 100, borderRadius: 30, marginHorizontal: '5%', flexDirection: 'column', alignItems: 'flex-start', justifyContent: 'center', paddingHorizontal: 15, marginTop: 25, overflow: 'hidden', position: 'relative', }, orderContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', position: 'absolute', left: 15, }, storeImage: { width: 45, height: 45, backgroundColor: 'yellow', borderRadius: 40, marginRight: 20, }, orderStatusText: { color: '#fff', fontFamily: 'BalooPaaji2-SemiBold', }, }); const runTransitionAnimation = useCallback(() => { // 旧组件退出动画:下移+缩小+淡出 Animated.parallel([ Animated.timing(prevTranslateY, { toValue: 100, duration: 500, useNativeDriver: true, }), Animated.timing(prevScale, { toValue: 0.5, duration: 500, useNativeDriver: true, }), ]).start(); // 新组件进入动画:上移+放大到正常 Animated.sequence([ Animated.delay(200), // 延迟让旧组件先启动退出动画 Animated.parallel([ Animated.timing(currTranslateY, { toValue: 0, duration: 800, useNativeDriver: true, }), Animated.timing(currScale, { toValue: 1, duration: 800, useNativeDriver: true, }), ]), Animated.delay(2500), // 组件停留时长 ]).start(() => { // 动画结束后更新索引,重置动画变量 setPrevIndex(currIndex); setCurrIndex(prev => (prev + 1 >= listData.length ? 0 : prev + 1)); currTranslateY.setValue(-80); currScale.setValue(0.5); prevTranslateY.setValue(0); prevScale.setValue(1); runTransitionAnimation(); // 循环执行过渡 }); }, [currIndex, listData.length, currTranslateY, currScale, prevTranslateY, prevScale]); useEffect(() => { runTransitionAnimation(); return () => { // 组件卸载时清理动画,避免内存泄漏 currTranslateY.stopAnimation(); currScale.stopAnimation(); prevTranslateY.stopAnimation(); prevScale.stopAnimation(); }; }, [runTransitionAnimation]); return ( <View style={styles.mainContainer}> {/* 上一个组件(执行退出动画) */} {prevIndex !== null && ( <Animated.View key={`prev-${listData[prevIndex].id}`} style={[ styles.orderContainer, { transform: [{ translateY: prevTranslateY }, { scale: prevScale }], opacity: prevScale.interpolate({ inputRange: [0.5, 1], outputRange: [0, 1], }), }, ]} > <View style={styles.storeImage} /> <Text style={styles.orderStatusText}>{listData[prevIndex].storeName}</Text> </Animated.View> )} {/* 当前组件(执行进入/停留动画) */} <Animated.View key={`curr-${listData[currIndex].id}`} style={[ styles.orderContainer, { transform: [{ translateY: currTranslateY }, { scale: currScale }], }, ]} > <View style={styles.storeImage} /> <Text style={styles.orderStatusText}>{listData[currIndex].storeName}</Text> </Animated.View> </View> ); }; export default StoreCarousel;
关键说明
- 双组件层叠:通过
position: absolute让新旧组件重叠,实现过渡时的视觉衔接。 - 动画时序控制:用
Animated.sequence和delay控制动画顺序,确保旧组件先退出,新组件随后进入,最后停留指定时长。 - 状态重置:每次动画循环结束后重置动画变量,为下一次过渡做好准备。
- 内存泄漏防护:组件卸载时停止所有动画,避免残留动画占用资源。
内容的提问来源于stack exchange,提问作者Ash Archin
相关产品推荐
相关产品推荐

