如何在React.js中为已实现缩放的时间线添加滚动功能?
在React中实现类似Premiere Pro的时间线滚动功能
核心实现思路
- 用横向滚动容器包裹时间线内容,当时间线放大后内容宽度超出容器,自然触发滚动行为
- 监听容器的
scroll事件,结合当前缩放比例,将滚动偏移量转换为时间偏移,更新可视起始时间状态 - 基于可视时间范围动态渲染时间线刻度、剪辑等内容,确保内容与滚动位置同步
完整代码示例
假设你已完成缩放功能(持有zoomLevel:毫秒/像素,数值越小放大程度越高;totalDuration:时间线总时长),以下是滚动功能的集成实现:
import { useState, useRef, useEffect } from 'react'; const Timeline = () => { // 现有状态:缩放比例、时间线总时长 const [zoomLevel, setZoomLevel] = useState(100); // 初始100ms/像素 const [totalDuration, setTotalDuration] = useState(3600000); // 1小时 // 新增滚动相关状态:可视起始时间 const [visibleStartTime, setVisibleStartTime] = useState(0); // 滚动容器Ref const scrollContainerRef = useRef(null); // 监听滚动事件,更新可视起始时间 useEffect(() => { const handleScroll = () => { if (!scrollContainerRef.current) return; const scrollLeft = scrollContainerRef.current.scrollLeft; // 滚动偏移量转时间偏移 const newVisibleStartTime = scrollLeft * zoomLevel; setVisibleStartTime(newVisibleStartTime); }; const container = scrollContainerRef.current; container?.addEventListener('scroll', handleScroll); return () => container?.removeEventListener('scroll', handleScroll); }, [zoomLevel]); // 计算时间线内容总宽度 const timelineContentWidth = totalDuration / zoomLevel; // 计算可视结束时间 const visibleEndTime = visibleStartTime + (scrollContainerRef.current?.clientWidth || 0) * zoomLevel; // 生成可视区域内的时间刻度 const generateTimeMarkers = () => { const markers = []; const step = 10000; // 每10秒一个刻度 let currentTime = Math.ceil(visibleStartTime / step) * step; while (currentTime < visibleEndTime) { const xPos = currentTime / zoomLevel; markers.push( <div key={currentTime} style={{ position: 'absolute', left: `${xPos}px`, top: 0, width: '1px', height: '100%', backgroundColor: '#ccc' }} > <span style={{ position: 'absolute', top: 0, left: '5px', fontSize: '12px' }}> {new Date(currentTime).toISOString().substring(11, 19)} </span> </div> ); currentTime += step; } return markers; }; // 模拟剪辑数据 const clips = [ { id: 1, startTime: 30000, duration: 60000, name: '片段1' }, { id: 2, startTime: 120000, duration: 90000, name: '片段2' }, { id: 3, startTime: 250000, duration: 120000, name: '片段3' } ]; // 渲染可视区域内的剪辑 const renderClips = () => { return clips .filter(clip => { const clipEnd = clip.startTime + clip.duration; return clip.startTime < visibleEndTime && clipEnd > visibleStartTime; }) .map(clip => { const clipX = clip.startTime / zoomLevel; const clipWidth = clip.duration / zoomLevel; return ( <div key={clip.id} style={{ position: 'absolute', left: `${clipX}px`, top: '30px', width: `${clipWidth}px`, height: '40px', backgroundColor: '#2196F3', borderRadius: '4px', padding: '8px', color: 'white', fontSize: '14px' }} > {clip.name} </div> ); }); }; return ( <div style={{ width: '100%', height: '100px', border: '1px solid #eee', overflowX: 'auto' }} ref={scrollContainerRef} > <div style={{ position: 'relative', width: `${timelineContentWidth}px`, height: '100%' }} > {generateTimeMarkers()} {renderClips()} </div> </div> ); }; export default Timeline;
关键优化细节
- 滚动性能优化:用
requestAnimationFrame包裹状态更新,避免频繁重绘:
const handleScroll = () => { requestAnimationFrame(() => { if (!scrollContainerRef.current) return; const scrollLeft = scrollContainerRef.current.scrollLeft; setVisibleStartTime(scrollLeft * zoomLevel); }); };
- 缩放-滚动联动:缩放时保持可视区域中心不变,避免内容跳变:
const handleZoom = (newZoom) => { if (!scrollContainerRef.current) return; const containerWidth = scrollContainerRef.current.clientWidth; // 当前可视中心时间 const centerTime = visibleStartTime + (containerWidth / 2) * zoomLevel; setZoomLevel(newZoom); // 计算新的滚动位置,保持中心时间一致 const newScrollLeft = (centerTime / newZoom) - (containerWidth / 2); scrollContainerRef.current.scrollLeft = newScrollLeft; setVisibleStartTime(newScrollLeft * newZoom); };
- 虚拟滚动(可选):若时间线内容极多,可只渲染可视区域内的元素,大幅提升渲染性能。
内容的提问来源于stack exchange,提问作者AlgoPro
相关产品推荐
相关产品推荐

