完成长图的自动滚动

460 阅读3分钟

前言

一段时间没写文,感觉差点什么。正好接到一个需求需要写一个组件,就记一下思路吧

需求如下

组件能自动循环播放一个长图,播放速度平滑一致,并且在鼠标移动移入移出时停止播放。需求很简单,实际代码量也不是很多,花了大概1个半小时完成

最终效果

tutieshi_480x348_4s.gif

实现

1、requestAnimationFrame

首先我们需要创建平滑的视觉动画效果,为此我们需要使用requestAnimationFrame 函数

requestAnimationFrame 是一个浏览器提供的API,用于在下一次重绘之前执行指定的动画帧。它确保了代码在显示器刷新新内容时执行,能创建基于视觉的非常流畅的动画。

相对于setTiemout的优点也很多

  1. 调用时机requestAnimationFrame 在浏览器准备重新渲染页面之前调用提供的回调函数,这通常发生在浏览器的刷新周期内。
  2. 优化性能:与使用 setTimeout 或 setInterval 不同,requestAnimationFrame 会自动调整帧率以适应浏览器和设备的能力,比如在节能模式或移动设备上降低帧率,从而节省资源。
  3. 回调函数:传递给 requestAnimationFrame 的函数会在浏览器的渲染队列中添加一个动画帧。这个回调函数接收一个参数,通常表示从上一次动画帧到当前动画帧的时间戳。
  4. 链式调用:为了持续动画,你需要在回调函数内部再次调用 requestAnimationFrame。这样,每次动画帧完成后,浏览器都会重复该过程,直到你取消它。
  5. 取消动画:要停止动画,可以使用 cancelAnimationFrame 函数并传入 requestAnimationFrame 返回的唯一标识符(一个长整数)。

function step(timestamp) { 
// 动画逻辑,如更新元素的位置 
// ... 
// 如果还需要继续动画,再次调用 
   requestAnimationFrame(step);
} 
// 启动动画 
requestAnimationFrame(step); 

对上面的用法进行补充,然后来创建自己的平滑移动动画


  //...
  
  /**
   * 线性匀速运动函数
   * @param t 当前时间戳差值
   * @param b 开始进度
   * @param c 结束进度
   * @param d 时长
   *  */
  function linear(t: number, b: number, c: number, d: number): number {
    return (c * t) / d + b;
  }

  /**
   * 平滑移动
   * @param targetY 目标元素
   * @param duration 动画市场
   * @param scrollW 移动距离
   */
  function smoothScrollTo(
    targetY: HTMLDivElement,
    duration: number,
    scrollW: number,
  ) {
    const start = performance.now();
    const left = targetY.scrollLeft;
    function step(timestamp: number) {
      if (timestamp < start + duration) {
        const progress = timestamp - start;
        const linearProgress = linear(progress, 0, 1, duration);

        targetY.scrollTo({
          left: left + scrollW * linearProgress,
        });
        requestAnimationFrame(step);
      } else {
        targetY.scrollTo({ left: left + scrollW });
      }
    }

    requestAnimationFrame(step);
  }

  //...

2、setInterval

循环执行每一段距离的移动,并且在移动到头的时候重置播放进度

使用playingStatus状态值来开启或结束动画

  
  const [playingStatus, setPlayingStatus] = useState(false);

  useEffect(() => {
    let intervalId!: NodeJS.Timeout;
    if (!ref.current) {
      return;
    }
    const viewBoxW = ref.current?.clientWidth;
    const maxW = (ref.current?.firstChild as HTMLImageElement)?.clientWidth;

    // 进度条速度
    const speed = 200;
    if (playingStatus) {
      // 开始计时任务
      intervalId = setInterval(() => {
        if ((ref.current as HTMLDivElement).scrollLeft + viewBoxW === maxW) {
          ref.current?.scrollTo({ left: 0, behavior: 'instant' });
          return;
        }
        smoothScrollTo(ref.current as HTMLDivElement, 500, speed);
      }, 500);
    } else {
      // 清除定时器
      clearInterval(intervalId);
    }
    // 在组件卸载时清除定时器
    return () => {
      clearInterval(intervalId);
    };
  }, [playingStatus]);

3、添加移入移出控制

  const ref = useRef<HTMLDivElement>(null);
  const [playingStatus, setPlayingStatus] = useState(false);
  //...
        <div
          className={styles.imgBox}
          ref={ref}
          onMouseMove={() => {
            setPlayingStatus(false);
          }}
          onMouseLeave={() => {
            setPlayingStatus(true);
          }}
        >
          <img
            src={config.imgUrl ?? demoImg}
            alt=""
            onLoad={() => {
              setPlayingStatus(true);
            }}
          ></img>
        </div>
        //...

4、感谢阅读

完整代码:

const LongImagePreview:React.FC = () => {
  const ref = useRef<HTMLDivElement>(null);
  const demoImg = require('./demo.jpg');
  const [playingStatus, setPlayingStatus] = useState(false);
  
  useEffect(() => {
    setPlayingStatus(false);
    return () => {
      ref.current?.scrollTo(0, 0);
    };
  }, []);
  
    useEffect(() => {
    let intervalId!: NodeJS.Timeout;
    if (!ref.current) {
      return;
    }
    const viewBoxW = ref.current?.clientWidth;
    const maxW = (ref.current?.firstChild as HTMLImageElement)?.clientWidth;

    // 进度条速度
    const speed = 200;
    if (playingStatus) {
      // 开始计时任务
      intervalId = setInterval(() => {
        if ((ref.current as HTMLDivElement).scrollLeft + viewBoxW === maxW) {
          ref.current?.scrollTo({ left: 0, behavior: 'instant' });
          return;
        }
        smoothScrollTo(ref.current as HTMLDivElement, 500, speed);
      }, 500);
    } else {
      // 清除定时器
      clearInterval(intervalId);
    }
    // 在组件卸载时清除定时器
    return () => {
      clearInterval(intervalId);
    };
  }, [playingStatus]);
  
   /**
   * 线性匀速运动函数
   * @param t 当前时间戳差值
   * @param b 开始进度
   * @param c 结束进度
   * @param d 时长
   *  */
  function linear(t: number, b: number, c: number, d: number): number {
    return (c * t) / d + b;
  }

  /**
   * 平滑移动
   * @param targetY 目标元素
   * @param duration 动画市场
   * @param scrollW 移动距离
   */
  function smoothScrollTo(
    targetY: HTMLDivElement,
    duration: number,
    scrollW: number,
  ) {
    const start = performance.now();
    const left = targetY.scrollLeft;
    function step(timestamp: number) {
      if (timestamp < start + duration) {
        const progress = timestamp - start;
        const linearProgress = linear(progress, 0, 1, duration);

        targetY.scrollTo({
          left: left + scrollW * linearProgress,
        });
        requestAnimationFrame(step);
      } else {
        targetY.scrollTo({ left: left + scrollW });
      }
    }

    requestAnimationFrame(step);
  }

  return (
    <div className={styles.container}>
      <div className={styles.imgContainer}>
        <div
          className={styles.imgBox}
          ref={ref}
          onMouseMove={() => {
            setPlayingStatus(false);
          }}
          onMouseLeave={() => {
            setPlayingStatus(true);
          }}
        >
          <img
            src={demoImg}
            alt=""
            onLoad={() => {
              setPlayingStatus(true);
            }}
          ></img>
        </div>
      </div>
    </div>
  );
};