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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 12:43:28