You need to enable JavaScript to run this app. 如何在React中实现数组排序时圆形元素的移动动画效果
优惠活动
大模型
产品
解决方案
定价
更多

如何在React中实现数组排序时圆形元素的移动动画效果

解决方案

核心问题排查

你的代码没有动画效果的主要原因有三个:

  • 直接将JSX元素存入state,不符合React数据驱动的设计逻辑,不利于后续状态变化的监听
  • 元素key值使用随机生成的数值,不稳定,React会将排序后的元素判定为全新节点,直接销毁重建,不会触发位置过渡
  • 缺少位置变化的过渡动画逻辑

方案1:原生React + FLIP动画实现(无需第三方库)

FLIP是一种低成本实现列表重排动画的思路,核心是提前记录元素位置变化的差值,通过CSS transition触发平滑过渡。

完整实现代码

import { useEffect, useState, useRef } from "react";
import "./App.css";

function App() {
  const [sumCircles, setSumCircles] = useState(0);
  // 存纯数据,不存JSX
  const [circles, setCircles] = useState([]);
  const wrapperRef = useRef(null);

  const handlerSumCircles = (e) => {
    const value = Number(e.target.value);
    if (value >= 0 && value <= 100) {
      setSumCircles(value);
    } else alert("请输入0-100之间的数值");
  };

  // 生成圆形数据
  useEffect(() => {
    const rows = [];
    for (let i = 0; i < sumCircles; i++) {
      const r = Math.floor(Math.random() * 256);
      const g = Math.floor(Math.random() * 256);
      const b = Math.floor(Math.random() * 256);
      rows.push({
        // 固定唯一id,不要用随机数当key
        id: Date.now() + Math.random(),
        r, g, b,
        sortKey: r + g + b
      });
    }
    setCircles(rows);
  }, [sumCircles]);

  const sorting = () => {
    // 第一步:记录排序前每个元素的位置
    const circlesEl = wrapperRef.current.querySelectorAll(".circle");
    const posMap = new Map();
    circlesEl.forEach(el => {
      const rect = el.getBoundingClientRect();
      posMap.set(el.dataset.id, rect);
    });

    // 第二步:更新排序后的数据
    const sorted = [...circles].sort((a, b) => a.sortKey - b.sortKey);
    setCircles(sorted);

    // 第三步:FLIP动画逻辑
    requestAnimationFrame(() => {
      circlesEl.forEach(el => {
        const oldPos = posMap.get(el.dataset.id);
        const newPos = el.getBoundingClientRect();
        const dx = oldPos.left - newPos.left;
        const dy = oldPos.top - newPos.top;
        // 先把元素移回原来的位置
        el.style.transform = `translate(${dx}px, ${dy}px)`;
        el.style.transition = 'none';

        // 下一帧清除transform,触发过渡动画
        requestAnimationFrame(() => {
          el.style.transform = '';
          el.style.transition = 'transform 0.5s ease';
        });
      });
    });
  };

  return (
    <div>
      <input
        value={sumCircles}
        onChange={handlerSumCircles}
        type="number"
        placeholder="输入圆形数量"
      />
      <button onClick={sorting} className={"button"}>
        排序
      </button>

      <div ref={wrapperRef} className={"wrapper"}>
        {circles.map((item) => (
          <div
            key={item.id}
            data-id={item.id}
            className="circle"
            style={{
              width: `50px`,
              height: `50px`,
              borderRadius: '50%',
              backgroundColor: `rgb(${item.r}, ${item.g}, ${item.b})`,
            }}
          />
        ))}
      </div>
    </div>
  );
}

export default App;

补充CSS(App.css)

.wrapper {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 20px;
}

方案2:使用Framer Motion库实现(更简单)

如果不介意引入第三方库,Framer Motion的Reorder组件可以开箱即用地实现列表重排动画,不需要自己计算位置差值。

实现步骤

  1. 安装依赖:npm install framer-motion
  2. 替换代码如下:
import { useEffect, useState } from "react";
import { Reorder } from "framer-motion";
import "./App.css";

function App() {
  const [sumCircles, setSumCircles] = useState(0);
  const [circles, setCircles] = useState([]);

  const handlerSumCircles = (e) => {
    const value = Number(e.target.value);
    if (value >= 0 && value <= 100) {
      setSumCircles(value);
    } else alert("请输入0-100之间的数值");
  };

  useEffect(() => {
    const rows = [];
    for (let i = 0; i < sumCircles; i++) {
      const r = Math.floor(Math.random() * 256);
      const g = Math.floor(Math.random() * 256);
      const b = Math.floor(Math.random() * 256);
      rows.push({
        id: Date.now() + Math.random(),
        r, g, b,
        sortKey: r + g + b
      });
    }
    setCircles(rows);
  }, [sumCircles]);

  const sorting = () => {
    const sorted = [...circles].sort((a, b) => a.sortKey - b.sortKey);
    setCircles(sorted);
  };

  return (
    <div>
      <input
        value={sumCircles}
        onChange={handlerSumCircles}
        type="number"
        placeholder="输入圆形数量"
      />
      <button onClick={sorting} className={"button"}>
        排序
      </button>

      <Reorder.Group
        axis="x"
        values={circles}
        onReorder={setCircles}
        className="wrapper"
      >
        {circles.map((item) => (
          <Reorder.Item
            key={item.id}
            value={item}
            className="circle"
            transition={{ type: "spring", stiffness: 300, damping: 30 }}
            style={{
              width: `50px`,
              height: `50px`,
              borderRadius: '50%',
              backgroundColor: `rgb(${item.r}, ${item.g}, ${item.b})`,
            }}
          />
        ))}
      </Reorder.Group>
    </div>
  );
}

export default App;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.10 02:48:02