table of contents

1概述

刚开始学习 JavaScript 的时候,真心觉得无缝滚动是一个神奇的功能。为什么明明只有几个方块,就是滚不到头?背后到底是怎么回事?

明白了原理之后,才知道原来是使用了一些障眼法来实现。

2原理

假如需要无缝滚动的元素是一个 ul.items 中的 6 个 li.item。我们将控制 ul.items 在容器 .wrap 中滚动。

INFO

ul.items 表示 className 为 items 的 ul 元素。

html 代码如下

code.ts
1
<div class="wrap">
2
<ul class="items"><!--
3
--><li class="item"><div>1</div></li><!--
4
--><li class="item"><div>2</div></li><!--
5
--><li class="item"><div>3</div></li><!--
6
--><li class="item"><div>4</div></li><!--
7
--><li class="item"><div>5</div></li><!--
8
--><li class="item"><div>6</div></li><!--
9
--></ul>
10
</div>

我们的目标是实现水平方向上的滚动,因此需要 li.item 水平排列。此处,我们使用 display: inline-block 的方式来达到布局目的。但是我们知道,这样的布局,元素之间会存在默认的间隙,因此,使用了 <!-- --> 的方式来消除间隙。

另外,我们还需要控制页面元素的移动。可以通过改变元素的 left, top, translateX, translateY 等方式来做到,布局的选择,同时也会影响到最终的方案。

在布局上,超出容器的部分,需要隐藏,此处的隐藏,是给ul.items的,注意与 float 布局的区别。

code.ts
.items { overflow: hidden; }

ul.items 的内容不能折行,因此

code.ts
.items { white-space: nowrap; }

需要适配到移动端,因此,li.item的宽度就必然会随着设备宽度的变小而变小。

code.ts
1
@media (max-width: 780px) {
2
.item {
3
width: 190px;
4
}
5
}
6
7
@media (max-width: 580px) {
8
.item {
9
width: 160px;
10
}
11
}

最后一个核心需要关注的问题,就是无缝滚动的障眼法,到底是什么呢?本来用图片描述可能会更直观一点,这里我偷个懒,用文字描述一下。

我们有子元素 123456,复制一份,就变成 123456123456。让元素们整体向左移动,如果我们在移动到第二个 1 的时候,将整体的位置瞬间重置为初始位置,此时中间发生的变化我们无法用肉眼识别出来,就会给人一种,一直在向左移动,永远都停不下来的感觉。

这就是障眼法的真谛。

3功能实现

一说到运动,我们常常想到的方法可能是利用 setTimeout/setInterval,不过,html5 中,为我们提供了一个性能更加优秀的方法 requestAnimationFrame

代码声明如下:

code.ts
1
nextFrame = window.requestAnimationFrame ||
2
window.webkitRequestAnimationFrame ||
3
window.mozRequestAnimationFrame ||
4
window.msRequestAnimationFrame ||
5
function (callback) {
6
var currTime = + new Date,
7
delay = Math.max(1000 / 60, 1000 / 60 - (currTime - lastTime));
8
lastTime = currTime + delay;
9
return setTimeout(callback, delay);
10
},
11
cancelFrame = window.cancelAnimationFrame ||
12
window.webkitCancelAnimationFrame ||
13
window.webkitCancelRequestAnimationFrame ||
14
window.mozCancelRequestAnimationFrame ||
15
window.msCancelRequestAnimationFrame ||
16
clearTimeout,

我们需要知道滚动什么位置时,整个过程回到初始状态,这个位置刚好就是复制之前所有子元素加在一起的总长度。但是子元素的宽度会因为设备的改变而改变,因此配合布局,我们需要做如下处理:

code.ts
1
// 此处结合 jQuery 来实现,
2
if ($items.children().eq(0).width() == 190) {
3
itemW = 190;
4
}
5
if ($items.children().eq(0).width() == 160) {
6
itemW = 160;
7
}
8
target = itemW * $items.children().length;
INFO

该例子特地结合 jQuery 来实现,也是为了让大家感知一下语法不同,但原理万变不离其宗的感受

为了实现障眼法,需要复制一份子元素

code.ts
$items.html($items.html() + $items.html());

定义一个运动函数,这里的运动为匀速运动,因此比较简单,只需要一直 +1 即可。如果需要运动快一点,就多加一点

code.ts
1
function adAni() {
2
timer = nextFrame(function () {
3
scrollX += 1;
4
if (scrollX >= target) {
5
scrollX = 0;
6
}
7
$items.scrollLeft(scrollX);
8
adAni();
9
});
10
}

还有一些其他的需求。比如,鼠标 mouseover 时,需要停止滚动,离开之后又要重新启动滚动。因为需求的变化,在移动端还需要能够滑动 items.ul,手指松开之后继续滚动。因此我们需要一个区别 pc 与移动端的函数。通过 UA 的不同来区分。

code.ts
1
// 判断是否在移动端
2
function isMobile() {
3
return 'ontouchstart' in document;
4
}

在移动端,可以左右滑动,滑动时停止自动滚动,松开之后继续自动滚动。移动端的滑动事件,主要通过 touchstart, touchmove, touchend 来实现,与 pc 端的 mousedown, mousemove, mouseup 类似。

code.ts
1
var sX, sL;
2
$items.on('touchstart', function (e) {
3
cancelFrame(timer);
4
sX = e.originalEvent.changedTouches[0].pageX;
5
sL = $items.scrollLeft();
6
}).on('touchmove', function (e) {
7
var dis = e.originalEvent.changedTouches[0].pageX - sX;
8
var nowX = sL - dis;
9
if (nowX > target) {
10
nowX = 0;
11
}
12
$items.scrollLeft(nowX);
13
}).on('touchend', function (e) {
14
scrollX = $items.scrollLeft();
15
if (scrollX >= target) {
16
scrollX = 0;
17
}
18
adAni();
19
})

到这里,功能基本上就已经搞定了,完整代码如下

code.ts
1
;
2
(function ($) {
3
var $items = $('.items'),
4
lastTime = 0,
5
nextFrame = window.requestAnimationFrame ||
6
window.webkitRequestAnimationFrame ||
7
window.mozRequestAnimationFrame ||
8
window.msRequestAnimationFrame ||
9
function (callback) {
10
var currTime = + new Date,
11
delay = Math.max(1000 / 60, 1000 / 60 - (currTime - lastTime));
12
lastTime = currTime + delay;
13
return setTimeout(callback, delay);
14
},
15
cancelFrame = window.cancelAnimationFrame ||
16
window.webkitCancelAnimationFrame ||
17
window.webkitCancelRequestAnimationFrame ||
18
window.mozCancelRequestAnimationFrame ||
19
window.msCancelRequestAnimationFrame ||
20
clearTimeout,
21
scrollX = 0,
22
itemW = 240,
23
target = 0,
24
timer = null;
25
26
if ($items.children().eq(0).width() == 190) {
27
itemW = 190;
28
}
29
if ($items.children().eq(0).width() == 160) {
30
itemW = 160;
31
}
32
target = itemW * $items.children().length;
33
34
$items.html($items.html() + $items.html());
35
36
adAni();
37
38
function adAni() {
39
timer = nextFrame(function () {
40
scrollX += 1;
41
if (scrollX >= target) {
42
scrollX = 0;
43
}
44
$items.scrollLeft(scrollX);
45
adAni();
46
});
47
}
48
if (!isMobile()) {
49
$items.on('mouseover', function () {
50
cancelFrame(timer);
51
}).on('mouseout', function () { adAni(); });
52
}
53
54
var sX, sL;
55
$items.on('touchstart', function (e) {
56
cancelFrame(timer);
57
sX = e.originalEvent.changedTouches[0].pageX;
58
sL = $items.scrollLeft();
59
}).on('touchmove', function (e) {
60
var dis = e.originalEvent.changedTouches[0].pageX - sX;
61
var nowX = sL - dis;
62
if (nowX > target) {
63
nowX = 0;
64
}
65
$items.scrollLeft(nowX);
66
}).on('touchend', function (e) {
67
scrollX = $items.scrollLeft();
68
if (scrollX >= target) {
69
scrollX = 0;
70
}
71
adAni();
72
})
73
74
// 判断是否在移动端
75
function isMobile() {
76
return 'ontouchstart' in document;
77
}
78
})(jQuery);

那么,如何封装成为一个 jQuery 插件,就交给大家自己来完成啦!动手实践一下,应该不难搞定。

专栏首页
到顶
专栏目录