ReactJS等速无限滚动动画:多元素显示问题解决方案咨询
保持滚动动画速度恒定的实现方案
当前代码的核心问题是CSS动画时长固定,当元素数量增加时,容器总高度变大,相同时间内滚动的距离变长,导致速度变快。要保持速度不变,必须让动画时长和滚动距离成正比——滚动距离越长,动画时长越久。
方案一:动态计算CSS动画时长
思路
原来5个元素时,滚动完所有元素(一遍)用了5秒,相当于每秒滚动1个元素的高度。我们可以基于这个速度,根据当前元素总数动态计算动画时长:
- 获取单个元素的总高度(包含margin)
- 计算滚动总距离(即一遍元素的总高度,因为容器里复制了一遍元素,动画滚动-50%刚好覆盖)
- 时长 = 滚动总距离 / 单元素高度(比如100个元素用100秒,保持每秒滚动1个元素的速度)
修改后的代码
const { useRef, useEffect, useState } = React; function Card({ cardName }) { return ( <div className="bubble"> <div className="card m-2 pt-2"> <div className="py-1"> <div className="fs-5 mt-2">{cardName}</div> </div> </div> </div> ); } // 模拟100个元素的数据 let data = Array.from({length: 100}, (_, i) => ({Name: `${i+1}`})); function App() { const ref = useRef(null); const [containerWidth, setWidth] = useState("100%"); const [animationState, setPlay] = useState("paused"); const [animationDuration, setAnimationDuration] = useState("5s"); useEffect(() => { if (ref.current) { const scrollHeight = ref.current.scrollHeight; setWidth(scrollHeight + "px"); // 获取单个card的总高度(含margin) const firstCard = ref.current.querySelector(".card"); if (firstCard) { const cardStyle = getComputedStyle(firstCard); const cardTotalHeight = firstCard.offsetHeight + parseInt(cardStyle.marginTop) + parseInt(cardStyle.marginBottom); // 滚动总距离是一遍元素的高度,即scrollHeight/2 const totalScrollDistance = scrollHeight / 2; // 按每秒滚动1个元素的速度计算时长 const duration = (totalScrollDistance / cardTotalHeight) + "s"; setAnimationDuration(duration); } setPlay("running"); } }, [data]); // 元素数据变化时重新计算 const renderCards = data.map((el, index) => { return <Card key={index} cardName={el.Name} />; }); return ( <div className="App"> <div className="d-flex" ref={ref} style={{ width: containerWidth, animationPlayState: animationState, animationDuration: animationDuration }} > {renderCards} {renderCards} </div> </div> ); } ReactDOM.createRoot(document.getElementById('app')).render(<App/>);
.App { overflow: hidden; } .card { width: 200px !important; height: 200px; background: #ffefef; box-shadow: 0px 1px 4px 1px rgba(158, 151, 151, 0.25); border-radius: 15px; margin: 12px; padding: 12px; } .d-flex { overflow: visible; animation: animateContainer linear forwards infinite; flex-direction: column; } @keyframes animateContainer { from { transform: translateY(0); } to { transform: translateY(-50%); } }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js" integrity="sha512-8Q6Y9XnTbOE+JNvjBQwJ2H8S+UV4uA6hiRykhdtIyDYZ2TprdNmWOUaKdGzOhyr4dCyk287OejbPvwl7lrfqrQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js" integrity="sha512-MOCpqoRoisCTwJ8vQQiciZv0qcpROCidek3GTFS6KTk2+y7munJIlKCVkFCYY+p3ErYFXCjmFjnfTTRSC1OHWQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <div id="app"></div>
方案二:用requestAnimationFrame实现精准速度控制
如果觉得CSS动画的动态时长不够灵活,可以用JS的requestAnimationFrame直接控制滚动速度,完全脱离CSS动画的时长限制。
思路
- 定义固定的滚动速度(比如224px/秒,对应原来单个元素的总高度)
- 实时记录滚动位置,每一帧根据时间差更新位置
- 当滚动位置超过一遍元素的总高度时,重置位置,实现无缝循环
代码实现
const { useRef, useEffect, useState } = React; function Card({ cardName }) { return ( <div className="bubble"> <div className="card m-2 pt-2"> <div className="py-1"> <div className="fs-5 mt-2">{cardName}</div> </div> </div> </div> ); } let data = Array.from({length: 100}, (_, i) => ({Name: `${i+1}`})); function App() { const containerRef = useRef(null); const [containerHeight, setContainerHeight] = useState("auto"); const scrollRef = useRef({ position: 0, lastTime: Date.now(), speed: 224, // 每秒滚动224px(对应原来单个元素的总高度) animationId: null }); useEffect(() => { if (!containerRef.current) return; const firstCard = containerRef.current.querySelector(".card"); if (!firstCard) return; // 获取单个card总高度,计算一遍元素的总高度 const cardStyle = getComputedStyle(firstCard); const cardTotalHeight = firstCard.offsetHeight + parseInt(cardStyle.marginTop) + parseInt(cardStyle.marginBottom); const singleLoopHeight = cardTotalHeight * data.length; // 设置容器高度为两倍(用于滚动循环) setContainerHeight((singleLoopHeight * 2) + "px"); // 启动滚动动画 const animate = () => { const now = Date.now(); const deltaTime = (now - scrollRef.current.lastTime) / 1000; // 转换为秒 scrollRef.current.position += scrollRef.current.speed * deltaTime; scrollRef.current.lastTime = now; // 当滚动超过一遍元素高度时,重置位置,实现无缝循环 if (scrollRef.current.position >= singleLoopHeight) { scrollRef.current.position -= singleLoopHeight; } containerRef.current.style.transform = `translateY(-${scrollRef.current.position}px)`; scrollRef.current.animationId = requestAnimationFrame(animate); }; scrollRef.current.animationId = requestAnimationFrame(animate); // 组件卸载时停止动画 return () => { cancelAnimationFrame(scrollRef.current.animationId); }; }, [data]); const renderCards = data.map((el, index) => { return <Card key={index} cardName={el.Name} />; }); return ( <div className="App"> <div className="d-flex" ref={containerRef} style={{ height: containerHeight }} > {renderCards} {renderCards} </div> </div> ); } ReactDOM.createRoot(document.getElementById('app')).render(<App/>);
.App { overflow: hidden; } .card { width: 200px !important; height: 200px; background: #ffefef; box-shadow: 0px 1px 4px 1px rgba(158, 151, 151, 0.25); border-radius: 15px; margin: 12px; padding: 12px; } .d-flex { overflow: visible; display: flex; flex-direction: column; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js" integrity="sha512-8Q6Y9XnTbOE+JNvjBQwJ2H8S+UV4uA6hiRykhdtIyDYZ2TprdNmWOUaKdGzOhyr4dCyk287OejbPvwl7lrfqrQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js" integrity="sha512-MOCpqoRoisCTwJ8vQQiciZv0qcpROCidek3GTFS6KTk2+y7munJIlKCVkFCYY+p3ErYFXCjmFjnfTTRSC1OHWQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <div id="app"></div>
两种方案对比
- CSS方案:更简洁,依赖CSS动画,适合简单场景,动态计算时长即可。
- JS方案:更灵活,能精确控制速度,还可随时暂停/调整速度,适合复杂交互场景。
内容的提问来源于stack exchange,提问作者alainber82
相关产品推荐
相关产品推荐

