Dev .

一切伟大的思想和行动都有一个微不足道的开始

jQuery实现弹幕效果、文字水平滚动/垂直滚动插件

在写这个插件之前,已经在很多专题页面需要用到弹幕,或者是文字滚动等效果,原理大致相同(也有细微区别),但一直没有封装成插件。也是在这些工作中尝试使用了多种方法,在这里也将一一列举其中的优缺点。

一、使用CSS Animation的解决方案

使用CSS Animation这个解决方案并不好,但这也是我最开始的一个思路。我个人认为这是一个最差的解决方案,在实际使用过程中发现了许多意外的问题。它的兼容性倒是其次,更大的问题是弹幕元素的不可控制,以及窗口失去焦点后,动画暂停,但是JS并没有暂停。这就导致了重新恢复窗口焦点后,弹幕一堆的出现。是一个非常不好的效果。

思路:

使用@keyframes定义一个移动的动画,通过JS动态的加载弹幕元素到显示区域边缘,并指定相应的类。那么添加的元素就会执行我们定义的这个移动动画。那么这个弹幕的效果就实现了。当然这些元素都是绝对定位元素。

这个解决方案的思路大致就是这样,但其本身的局限性导致我不去使用它,就不拆开细说。

二、使用相对定位偏移的解决方案

使用这种方法较上一个方法效果好了很多,这个方法我之前都是用于列表滚动。并没有考虑用在弹幕这种效果上,那么该如何去实现呢?

思路:

这里的所说的相对定位并不是说设置元素的position值为relative。当然这样设置之后使用left/right/top/bottom来做偏移也是能实现这个效果的,但是在这里我是使用的margin来做偏移。效果与设置position值为relative一样。

效果:

因为当时无法提供json数据,所以数据都是渲染到页面上的。

JS:

var barrage = function (val) {
            this.box = $("#j-head-list .ui-repeater");
            this.item = $("#j-head-list .ui-repeater .ui-repeater-item");
            //保存第一个列表盒子 
            this.listBox1 = $("#j-head-list .list_01");
            this.listBox2 = $("#j-head-list .list_02");
            // 保存数据 
            this.data = [];
            // 需要暂停的元素
            this.stopItem = null;
            // 首次渲染长度 
            this.firstLen = 10;
            this.init();
        };
        barrage.prototype = {
            init: function () {
                this.getData();
                //this.news(); 
            },
            //获取并整理数据 
            getData: function () {
                var self = this;
                var item = $.makeArray(this.item);
                for (var i in item) {
                    this.data.push(item[i].innerHTML.trim());
                }
                this.data.forEach(function (item, index, array) {
                    self.data[index] = item.replace(/\[tgc/g, "[");
                });
                this.box.html("");
                this.renderer();
            },
            //新闻列表整理 
            news: function () {
                var item = $(".news_list .ui-repeater").html().toString();
                item = item.replace(/\[tgc/g, "[");
                $(".news_list .ui-repeater").html(item);
            },
            //悬浮暂停 
            hoverStop: function () {
                var self = this;
                this.box.hover(function () {
                    var $this = $(this);
                    if ($this.hasClass("list_01")) {
                        self.stopItem = 1;
                    } else {
                        self.stopItem = 2;
                    }
                }, function () {
                    self.stopItem = null;
                })
            }, //渲染数据
            renderer: function () {
                var self = this, start, dataLen = this.data.length;
                //第一次渲染数据 
                var index = 0;
                for (start = 0; start < this.firstLen; start++) {
                    var _html = '<li class="ui-repeater-item" style="display: inline-block">' + this.data[index] + '</li>';
                    if (start % 2 == 0) {
                        this.box.eq(0).append(_html);
                    } else {
                        this.box.eq(1).append(_html);
                    }
                    index++;
                    if (dataLen < this.firstLen) {
                        if (index == dataLen) {
                            index = 0
                        }
                    }
                }
                this.hoverStop();
                //添加数据 
                var left1 = 0, left2 = 0, width1, width2;
                dataMonitor();
                getWidth();
                var time = setInterval(function () {
                    //保存第一次宽度 
                    if (left1 < width1 && self.stopItem != 1) {
                        self.listBox1.css("margin-left", -(++left1) + 'px')
                    } else if (left1 >= width1 && self.stopItem != 1) {
                        var _html = '<li class="ui-repeater-item" style="display: inline-block">' + self.data[++start] + '</li>';
                        self.listBox1.children().eq(0).remove();
                        self.listBox1.css("margin-left", '-1px');
                        self.listBox1.append(_html);
                        left1 = 1;
                        dataMonitor();
                        getWidth();
                    }
                    if (left2 < width2 && self.stopItem != 2) {
                        self.listBox2.css("margin-left", -(++left2) + 'px')
                    } else if (left2 >= width2 && self.stopItem != 2) {
                        var _html = '<li class="ui-repeater-item" style="display: inline-block">' + self.data[++start] + '</li>';
                        self.listBox2.children().eq(0).remove();
                        self.listBox2.css("margin-left", '-1px');
                        self.listBox2.append(_html);
                        left2 = 1;
                        dataMonitor();
                        getWidth();
                    }
                }, 30);

                function getWidth() {
                    width1 = self.box.eq(0).children().eq(0).css("width");
                    width2 = self.box.eq(1).children().eq(0).css("width");
                    width1 = parseInt(width1.substring(0, width1.indexOf('px'))) + 50;
                    width2 = parseInt(width2.substring(0, width2.indexOf('px'))) + 50;
                }

                function dataMonitor() {
                    if (start + 1 >= dataLen) {
                        start = -1;
                    }
                }
            }
        };
        new barrage({});

从上面的代码可以看出,其实这个方法也是有瑕疵的,速度调节我们需要依赖setInterval的时间,或者是每执行一次时间循环元素的偏移量增大一点。但是无论怎么写,都会有滚动的顿挫感,不够平滑。那有什么方法来解决这些问题呢?

三、基于requestAnimationFrame的解决方案

使用requestAnimationFrame应该是最好的一个解决方案了(就我的眼界来看),并结合translate3d() 做偏移,简直是完美。那么要使用requestAnimationFrame,我们就要知道什么事requestAnimationFrame,那么久简单的介绍一下:

requestAnimationFrame:

window.requestAnimationFrame()这个方法是用来在页面重绘之前,通知浏览器调用一个指定的函数,以满足开发者操作动画的需求。这个方法接受一个函数为参,该函数会在重绘前调用。

如果你想做逐帧动画的时候,你应该用这个方法。这就要求你的动画函数执行会先于浏览器重绘动作。通常来说,被调用的频率是每秒60次,但是一般会遵循W3C标准规定的频率。如果是后台标签页面,重绘频率则会大大降低。

回调函数只会被传入一个DOMHighResTimeStamp参数,这个参数指示当前被 requestAnimationFrame 序列化的函数队列被触发的时间。因为很多个函数在这一帧被执行,所以每个函数都将被传入一个相同的时间戳,尽管经过了之前很多的计算工作。这个数值是一个小数,单位毫秒,精确度在 10 µs。

需要注意的是:如果想得到连贯的逐帧动画,函数中必须重新调用 requestAnimationFrame()。

语法:

*requestID* = window.requestAnimationFrame(*callback*); // Firefox 23 / IE10 / Chrome / Safari 7 (incl. iOS) 
requestID = window.mozRequestAnimationFrame(*`callback`*); // Firefox < 23 
requestID = window.webkitRequestAnimationFrame(callback); // Older versions Chrome/Webkit

我们可以看到上面有兼容性写法,是的,其本身兼容性不太好,但是支持大部分的现代浏览器。但这不符合我们工作的需要,这我们不用担心,Opera浏览器的技术师Erik Möller 把这个函数进行了封装,使得它能更好的兼容各种浏览器。

(function () {
        var lastTime = 0;
        var vendors = ['webkit', 'moz'];
        for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
            window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
            window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame'];
        }
        if (!window.requestAnimationFrame) window.requestAnimationFrame = function (callback, element) {
            var currTime = new Date().getTime();
            var timeToCall = Math.max(0, 16 - (currTime - lastTime));
            var id = window.setTimeout(function () {
                callback(currTime + timeToCall);
            }, timeToCall);
            lastTime = currTime + timeToCall;
            return id;
        };
        if (!window.cancelAnimationFrame) window.cancelAnimationFrame = function (id) {
            clearTimeout(id);
        };
    }());

这样,我们就可以放心的使用这个方法了。但仅限于现代浏览器。IE9+是没问题的。至于它的其他信息可以点击这里查询

效果:

requestAnimationFrame

需要源码的同学可以点击这里下载