移动端滑动框架scroll5

解决痛点

移动设备中原生对大段文章或超长列表滚动显示的支持不太友好:当一屏显示不全内容时,对于有确定高度和宽度的内容是可以滑动的,但是在低版本Android及现在的IPhone设备中,CSS的position:fixed属性是不支持的。这实际上限制了我么实现上方固定标题/工具栏,下方固定页脚,中间内容滚动的展示效果。借助Iscroll,我们可以只对需要滚动的部分使用Iscroll,同时预留出上下位置即可。另一方面当内容过长时,依靠原生的滚动支持会有明显的卡顿情况,Iscroll则会根据设备支持情况自动选用不同滚动实现形式,当然我们也可以手动控制。
下载及官网地址

可能的坑

兼容性

我们的项目之前做过一部分机型的适配检测:Iscroll总体来说兼容性做的还是不错的,在现在主流版本的Android和所有iPhone设备上都运行良好。目前发现在MUI和华为部分机型的4.4版本下会出现页面直接不能滑动的情况,这个问题卡了我好久,看了好几天Android webView的BUG,最后推测应该是这两个平台下的webView控件对触摸事件的响应有问题,js默认的触摸事件覆盖了Iscroll的事件冒泡。
解决方法是优先调用Iscroll的touchmove事件而不是document的:

1
document.addEventListener('touchmove', function (e) { e.preventDefault(); }, false);

当然这样做的话页面上如果有用默认的滚动效果的话也就不起作用了,我们可以进一步限定只在使用Iscroll滚动的元素上禁止touchmove事件:

1
2
var scroller=document.getElementById('scroller');
scroller.addEventListener('touchmove', function (e) { e.preventDefault(); }, false);

动态加载后不能滚动

我在列表ul上使用了Iscroll,同时做了分页加载,我发现当在ul后新添加了元素后页面就卡住不能滚动了,这个其实是我们新增加元素后滚动元素的高度发生了变化,这时候还按照之前高度计算滚动距离等当然就不对了,所以我们应该在每次做改变元素高度的操作的时候手动通知Iscroll刷新,比如在ajax请求数据成功后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$.ajax({
async:true,
cache:false,
type:"POST",
url:xxx
data: {},
dataType: "JSON",
error:function(jqXHR, textStatus, errorThrown){
$.showDialog();
},
success:function(msg){
fillList(msg);
myScroll.refresh();
}
});

不能选取某些元素

有时我们在滚动元素内部会添加输入框、下拉框等元素,如果使用默认Iscroll设置会出现这些元素不能获取焦点的问题,这是因为Iscroll为了让滚动更流畅,屏蔽了一些元素的touch等事件(想象一下我们用手滚动页面时实际上touch了很多元素,如果这些事件都需要处理的话是比较耗资源的)。
在Iscroll4版本里我们可能需要修改源码才能支持对某些元素的选中,源码:

1
2
3
4
5
6
onBeforeScrollStart: function (e) {
var target = e.target;
  while (target.nodeType != 1) target = target.parentNode;
  if (target.tagName != 'SELECT' && target.tagName != 'INPUT' && target.tagName != 'TEXTAREA')
    e.preventDefault();
},

但在Iscroll5版本里已经可以在初始化Iscroll时通过设置来指定过滤掉哪些元素的touch响应:

1
2
3
var myScroll = new IScroll('#wrapper', {
preventDefaultException: { tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT|LI)$/ }
});

我们想使用输入框是需要去掉“INPUT”就行了。

最简实践

最简单的HTML结构为:

1
2
3
4
5
6
<div id="wrapper">
<ul>
<li></li>
...
</ul>
</div>

Iscroll需要在页面加载后手动初始化,对于要滚动元素既可以指定选择器字符串,也可以是引用对象:

1
2
3
4
5
function loaded () {
var myScroll = new IScroll('#wrapper',{});
/*var wrapper = document.getElementById('wrapper');
var myScroll = new IScroll(wrapper,{});*/

}

{}内是Iscroll的一个数组参数,在这里我们可以配置一些常见设置,如是否滑动后回弹、开启或关闭CSS transforms支持等。

分页加载

对于超长列表,我们经常会用到上拉加载和下拉刷新功能以实现分页展示,这在用户体验和性能上的优势不言自明,对Iscroll进行一些改造就能支持这样的效果。在这里我们主要利用了Iscroll的自定义事件:Iscroll提供了一些自定义事件可以用on的方式绑定到滚动元素上,以在一些特殊时刻处理滚动操作,比如scrollStart和scrollEnd分别在滚动开始和结束时触发。
HTML结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="fltList" id="wrapper">
<div id="scroller">
<div id="pullDown">
<span class="pullDownIcon"></span><span class="pullDownLabel">下拉刷新...</span>
</div>
<ul id="testList">
</ul>
<div id="pullUp">
<span class="pullUpIcon"></span><span class="pullUpLabel">加载更多...</span>
</div>
</div>
</div>
<div id="return2top"></div>

数据都添加到testList这个ul中;pullDown和pullUp用于实现根据下拉和上拉的距离更改用户提示;return2top这个元素是用来从任意位置返回列表顶端的(在一个超长列表中让用户自己从最低端再划回去有点太不人道了,最好有一键返回。这里用到了scrollTo接口)。
js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
var myScroll,
scrollLen=5,
pullDownEl = document.getElementById('pullDown'),
pullDownOffset = pullDownEl.offsetHeight,
pullUpEl = document.getElementById('pullUp'),
pullUpOffset = pullUpEl.offsetHeight,
winHeight=window.innerHeight,
pageLoading=document.getElementById('pageLoading'),
isLoading=false;
$('#return2top').click(function(){
myScroll.scrollTo(0,0,500,IScroll.utils.ease.quadratic);
});
myScroll.on("refresh",function(){
if (pullDownEl.className.match('loading')) {
pullDownEl.className = '';
pullDownEl.querySelector('.pullDownLabel').innerHTML = '下拉刷新...';
} else if (pullUpEl.className.match('loading')) {
pullUpEl.className = '';
pullUpEl.querySelector('.pullUpLabel').innerHTML = '加载更多...';
}
});

myScroll.on("scroll",function(){
if (this.y > scrollLen && !pullDownEl.className.match('flip')) {
pullDownEl.className = 'flip';
pullDownEl.querySelector('.pullDownLabel').innerHTML = '释放后刷新...';
this.minScrollY = 0;
} else if (this.y < scrollLen && pullDownEl.className.match('flip')) {
pullDownEl.className = '';
pullDownEl.querySelector('.pullDownLabel').innerHTML = '下拉刷新...';
this.minScrollY = -pullDownOffset;
pageLoading.style='';
} /*else if (this.y < (this.maxScrollY - scrollLen) && !pullUpEl.className.match('flip')) {
pullUpEl.className = 'flip';
pullUpEl.querySelector('.pullUpLabel').innerHTML = '释放后加载...';
this.maxScrollY = this.maxScrollY;
} else if (this.y > (this.maxScrollY + scrollLen) && pullUpEl.className.match('flip')) {
pullUpEl.className = '';
pullUpEl.querySelector('.pullUpLabel').innerHTML = '加载更多...';
this.maxScrollY = pullUpOffset;
}*/

});

myScroll.on("scrollEnd",function(){
if (pullDownEl.className.match('flip')) {
pullDownEl.className = 'loading';
pullDownEl.querySelector('.pullDownLabel').innerHTML = '正在加载...';
} else /*if (pullUpEl.className.match('flip'))*/ {
pullUpEl.className = 'loading';
pullUpEl.querySelector('.pullUpLabel').innerHTML = '正在加载...';
}
if((document.getElementById('fltList').clientHeight-200)>winHeight){
$('#return2top') .show();
}else{
$('#return2top') .hide();
}
});
myScroll.on("slideDown",function(){
if(this.y > scrollLen && !isLoading){
isLoading=true;
freshList();//刷新列表方法
}
});

myScroll.on("slideUp",function(){
if(this.maxScrollY - this.y > scrollLen && !isLoading){
isLoading=true;
getList();//获取列表数据,并添加至列表中
}
});

上述代码主要用来判断滑动距离以更改下拉、刷新提示信息,以及何时执行下拉、刷新动作(当滑动超过列表最上、最下距离时回弹并触发事件,模拟Android和iPhone中的原生加载体验)。代码中注释掉的是判断上拉加载的样式的部分,因为从实际上线后用户反馈来看,上拉一段距离回弹后再加载数据显得有些繁琐,我直接改成滑动到底边时就触发请求数据的方法,可以让用户感觉不到数据有分页,而是一个无限长的完整列表。如果有需要做上拉一段距离再触发加载的,可以恢复注释掉的内容。
代码开头给return2top元素绑定点击事件这里就是做了快速回到顶部的功能,scrollTo的函数定义为scrollTo(x, y, time, easing)。x和y表示要滚动到的位置,(0,0)自然是滚动到最上面的位置了;time为滚动动画持续的时间,即以多块的速度滚动到最上方;easing为指定动画的类型,可选项为quadratic、circular、back、bounce、elastic,可以都自己试一遍,里面有些回到顶端弹动一会儿再渐渐停止的效果还是很有意思的,不过我为了性能及软件整体风格的考量,只使用了最简单的quadratic动画。
还有一点需要指出的是我在触发数据加载的判断中加入了是否正在加载,即判断isLoading。默认其值应为false,当触发了加载时间时应该先置为true以防止用户多次滑动触发请求,这在使用移动网络时是非常必要的,否则将造成多次重复加载同一页数据情况。当然记得在freshList()和getList()自己实现数据异步请求和解析后将标志位isLoading复位为false。

常用API说明

本来写了一大堆,但我在查找某个参数的默认值时发现gitbook上已经有人写了中文版API文档,确实写的很好,后悔之前用的时候没发现。。
Iscroll5中文API

总结

Iscroll是一个非常好用的滚动处理插件,同时也非常轻量级(针对移动设备,不需要那么多功能的可以在下载时选择lite版本),本身已经提供了多种动画只需要简单配置就能使用。更赞的是它提供了相当多的接口,使用这些接口我们可以获取足够多的滚动相关的数据以定制化自己的功能,我还见过用Iscroll实现的PPT放映效果,对于不想使用全套移动js框架的同学来说真心推荐使用。
移动web开发使用的框架除了Iscroll和jQuery外我还用了Mobilebone,模拟原生APP页面切换用的,有需要的同学可以参考下我的上一篇日志。接下来我会整理下最近搞移动混合式开发的一些心得,主要是样式兼容性和js与原生代码交互的坑,敬请期待~