数据可视化大屏列表循环滚动动画Vue指令封装(requestAnimationFrame)

3,119 阅读3分钟

背景及需求

背景: 在大屏项目中,经常会存在列表循环滚动的动画。 当存在很多个列表都需要动画的时候, 重复代码既不够优雅,也不便于维护(当甲方觉得速度快慢调整或其他调整时候,需要重复修改)。

需求:

  1. 列表上下滚动动画循环播放
  2. 当鼠标点击列表时候,滚动暂停
  3. 当鼠标移出列表元素时候,重新触发滚动
  4. 可调整列表滚动的速度(甲方屁事多的情况下)

代码效果展示

详解

  1. 通过requestAnimationFrame进行列表动画滚动动画,并通过回调接口的timeStamp时间戳来控制动画进行的速度。
  2. 通过vue指令进行封装,并且在指令mounted生命周期定义变量isUsing来辨别当前用户是否正在与元素交互中(点击,鼠标滚动, 移入等,根据业务需求), 为元素增添监听鼠标点击、移入移出事件来控制交互期间的动画状态.

一些思考

为什么使用requestAnimationFrame而不是使用setTimeoutsetInterval实现? 在数据可视化需要定制动画的情况下,requestAnimationFrame的稳定性、交互性、流畅度和易用性都是优优于其他二者的,是与浏览器刷新都挂钩的

在当前浏览器的支持度也十分良好

image.png

当然在极限的情况下,也需要额外适配,在下面代码中我也有预留当浏览器不支持时候可以使用setTimeoutsetInterval实现。

核心代码

vue 指令实现

{
    arr: {},
    mounted(el, binding = { value: 200 }, vnode, prevVnode) {
        const gap = 1000 / 45;  // 帧数差距, 浏览器大概1s 60帧, 数字 > 60 效果应该一致,数字越小越慢
    
        if (window.requestAnimationFrame) {
            let isUsing = false;    // 是否介入操作;介入操作时暂停动画;

            // 鼠标移出时,继续动画
            el.addEventListener('mouseleave', () => {
                isUsing && scrollAnimation(); // 如果动画已暂停,重置动画
                isUsing = false;
            }, { passive: true } )

            // 鼠标滚动时,继续动画
            el.addEventListener('wheel', () => {
                isUsing = true;
            }, { passive: true } )

            // 鼠标点击时, 暂停动画
            el.addEventListener('click', () => {
                isUsing = true;
            }, { passive: true } )

            // 动画方法
            const scrollAnimation = () => {
                if (window.requestAnimationFrame) {
                    
 
                    /**
                     * @Author: Damon Liu
                     * @Date: 2024-04-29 10:28:37
                     * @LastEditors: Damon Liu
                     * @LastEditTime: 
                     * @Description: 
                     * @param {*} timeStamp 当前时间戳
                     * @param {*} preTimeStamp 上一帧时间戳
                     * @param {*} diff  累计的时间差
                     */
                    let animationFun = (timeStamp, preTimeStamp = 0, diff = 0) => {
                        if (isUsing) {
                            return
                        }
                        let currentDiff = preTimeStamp === 0 ? 0 : timeStamp - preTimeStamp;    // 当前时间差
                        let n_diff = currentDiff + diff;    // 总时间差
                        // 当总时间差比小于帧数差距时,不执行动画,申请下一帧执行
                        if(n_diff < gap) {
                            window.requestAnimationFrame((_timeStamp) => animationFun(_timeStamp, timeStamp, n_diff))
                            return ;
                        }
                        let scrollTop = el.scrollTop;   // 滚动条顶部
                        let clientHeight = el.clientHeight; // 内容高度
                        let scrollHeight = el.scrollHeight; // 滚动内容高度
                        // 当没有滚动至底部时
                        if (scrollTop + clientHeight < scrollHeight) {
                            el.scrollTop = scrollTop + 1;
                            window.requestAnimationFrame((_timeStamp) => animationFun(_timeStamp, timeStamp, 0));
                        }
                        // 滚动至底部重置动画
                        else {
                            el.scrollTop = 0;
                            scrollAnimation();
                        }
                    }
                    // 开始动画
                    window.requestAnimationFrame(animationFun);
                }

            }
            scrollAnimation()
        }
        else {

        }
    },
    unmounted(el, binding, vnode, prevVnode) {
    }
}

额外拓展

存在额外需求, 如需要手动去控制列表是否滚动时候,可通过指令传入额外变量去进行控制。

列表滚动动画也可以更改为其他动画,如颜色的变化以及形状,位移等。总体的思路一致。