likes
comments
collection
share

前端跑马灯Marquee

作者站长头像
站长
· 阅读数 29

为什么叫Marquee

分析

跑马灯在前端是很常见的一种功能。听着高大上,其实就是让一行文字朝一个方向移动。让dom元素移动的方式在css中又无外乎两种:transition & animation

transition

实现一个文字从容器右侧滚动到容器左侧的跑马灯。(这里的例子是未知容器长度未知文字长度时的处理方案。如果已知容器宽度则只使用translateX移动即可)。

他的缺点就在于需要动态为元素p添加和取消控制他的class,因此一般还是采用他的升级版animation

.tra-marquee {
    width: 100%;
    height: 50px;
    border: solid 1px #000;
    line-height: 50px;
    font-size: 16px;
    box-sizing: border-box;
    position: relative;
    overflow: hidden;
    // 起始点:使文字在容器右侧
    > p {
        position: absolute;
        right: 0; // 文字右侧与容器右侧距离为0,使文字右侧与容器右侧对齐
        top: 0;
        transform: translateX(100%); // 使文字向右移动文字长度的距离,达到文字左侧紧贴容器右侧
        white-space: nowrap;
        // 终点:使文字在容器左侧
        &.move {
            right: 100%; // 使文字左侧与容器左侧对齐
            transform: translateX(0%); // 使文字右侧与容器左侧对齐
            transition: all 12s linear;
        }
    }
}

animation

优点:作为animation的升级版可以更好的控制动画的延时、停止后的位置、运动快慢、是否重复等。就不需要再动态控制样式了。

.ani-marquee {
    width: 100%;
    height: 50px;
    border: solid 1px #000;
    line-height: 50px;
    font-size: 16px;
    box-sizing: border-box;
    position: relative;
    overflow: hidden;
    > p {
        position: absolute;
        top: 0;
        white-space: nowrap;
        animation: Marquee 12s linear infinite;
    }
}

@keyframes Marquee {
    0% {
        right: 0;
        transform: translateX(100%);
    }
    100% {
        right: 100%;
        transform: translateX(0%); 
    }
}

特殊场景

上面通过css实现的都是一些简单场景。但有的时候我们可能需要再跑马灯中加入各种逻辑判断,又或者使用类似于跑马灯的场景但又不完全是。

这类场景的一大特点就是单纯的css不能够完全满足需求,需要再javaScript文件中来动态控制跑马灯。而使用javaScript控制跑马灯在原理上与animation相同。也是通过将整个动画拆分为不同的帧动画,然后再将他们拼合起来。

不同的是animation中帧动画是通过keyframes关键字实现的,而javaScript中实现帧动画是通过浏览器的刷新帧来实现的。而在浏览器每一刷新帧所触发的钩子函数,就是浏览器专门为动画所暴露的Api:requestAnimationFrame

具体使用方法可以看上方MDN链接中示例部分的介绍。如有不足也可参考下方我自己写的另一种特殊示例。

抖音直播间标题文字过长超出范围时自动滚动

这个需求看上去是跑马灯的效果,但是有一定的差异。因为跑马灯是整个文字从尾部滚动到头部,而这个是从文字过长被容器隐藏的地方一直滚动到文字末尾,将整个文字都展示出来。然后再循环滚动。

这时我们通过css就无法拿到滚动的距离(文字宽度 - 容器宽度),也无法用css捕捉到动画停止的时机(滚动到文字尾部停止)。这时就需要采用requestAnimationFrame实现。

获取容器宽度和文字长度

首先需要获取到容器宽度文字长度,通过ref配合offsetWidth即可实现。需要注意的是,默认文档流内文字长度是等于容器宽度的。需要将文字脱离文档流才可获取到其长度。 这里我采用的方法是定位。

.request-marquee {
    width: 200px;
    height: 50px;
    border: solid 1px #000;
    line-height: 50px;
    font-size: 16px;
    box-sizing: border-box;
    margin: 50px 0 0 50px;
    position: relative;
    overflow: hidden;
    > p {
        white-space: nowrap;
        position: absolute; // 脱离文档流
    }
}

requestAnimationFrame实现

import React, { FC, useEffect, useRef } from "react";
import "./index.scss";

const RequestAnimationFrame: FC = () => {
	const parentElement = useRef<HTMLDivElement>(null);
	const childElement = useRef<HTMLParagraphElement>(null);
   const timer = useRef<NodeJS.Timeout>(); // 清除定时器
	const start = useRef<number>(); // 动画开始时间
	const delayBeforeAnimate = 3000; // 动画开始时的延迟时间
	const delayAfterNextAnimate = 3000; // 动画结束后距离下一次动画开始的延迟时间

	useEffect(() => {
        const parentWidth = parentElement.current!.offsetWidth;
        const childWidth = childElement.current!.offsetWidth;

        // 如果文字长度大于容器长度就开启滚动
        if (childWidth - parentWidth > 0) {
            timer.current = setTimeout(() => {
                requestAnimationFrame(startAnimate);
            }, delayBeforeAnimate);
        }
	}, []);

	/**
	 * 开始动画
	 * @param timestamp 当前时间
	 */
	function startAnimate(timestamp: number) {
        if (start.current === undefined) {
            start.current = timestamp; // 开始滚动的时间戳
        }
        const parentWidth = parentElement.current!.offsetWidth;
        const childWidth = childElement.current!.offsetWidth;
        const speed = 0.03; // 每一帧滚动的速度
        const elapsed = timestamp - start.current; // 距离开始滚动所过去的时间
        const distance = speed * elapsed; // 速度乘以时间得到对应帧滚动的距离
        childElement.current!.style.transform = `translateX(-${exactDistance}px)`; // 设置文字在当前帧滚动的距离
        const maxDistance = childWidth - parentWidth; // 应该滚动的距离
        const exactDistance = Math.min(distance, maxDistance); // 更精准的对已经滚动的距离和应该滚动的距离作比较

        if (exactDistance === maxDistance) {
            // 表示已经滚动距离大于应该滚动距离,所以下一帧要重置动画
            timer.current = undefined;
            resetAnimate();
        } else {
            // 表示已经滚动距离小于应该滚动距离,所以下一帧继续动画
            requestAnimationFrame(startAnimate);
        }
	}

	/**
	 * 重置动画
	 */
	function resetAnimate() {
        timer.current = undefined;
        timer.current = setTimeout(() => {
            timer.current = undefined;
            // 重置文本滚动样式
            childElement.current!.style.transition = "none";
            childElement.current!.style.transform = "none";
            // 重置滚动开始时间
            start.current = undefined;
            timer.current = setTimeout(() => {
                requestAnimationFrame(startAnimate);
            }, delayBeforeAnimate);
        }, delayAfterNextAnimate);
	}

	return (
        <div className="request-marquee" ref={parentElement}>
                <p ref={childElement}>我是一条测试直播间名字过长的文本~~~</p>
        </div>
	);
};

export default RequestAnimationFrame;

以上就是前端实现跑马灯的全部内容啦!如果看完文章有所收获的话,烦请动动小手点个赞哦~

您的支持是对我最大的鼓励❤️❤️❤️