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

如何在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;

关键优化细节

  1. 滚动性能优化:用requestAnimationFrame包裹状态更新,避免频繁重绘:
const handleScroll = () => {
  requestAnimationFrame(() => {
    if (!scrollContainerRef.current) return;
    const scrollLeft = scrollContainerRef.current.scrollLeft;
    setVisibleStartTime(scrollLeft * zoomLevel);
  });
};
  1. 缩放-滚动联动:缩放时保持可视区域中心不变,避免内容跳变:
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);
};
  1. 虚拟滚动(可选):若时间线内容极多,可只渲染可视区域内的元素,大幅提升渲染性能。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 11:40:55