如何在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组件可以开箱即用地实现列表重排动画,不需要自己计算位置差值。
实现步骤
- 安装依赖:
npm install framer-motion - 替换代码如下:
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
相关产品推荐
相关产品推荐

