apicloud如何实现优雅的下拉刷新与加载更多(Appcan也可类似实现)
apicloud中提供下拉刷新监听事件api,也提供滚动到底部事件的监听,能够实现下拉刷新和滚动到底部加载更多功能,但是我们真的就满足实现功能了吗?将两个代码拼凑起来运行看看发现了什么?是的,在滚动到底部加载更多的时候底部会弹动,有人可能会说触发加载更多的时候直接放一个遮罩view,也就是progress,用来禁止用户继续对当前view产生触摸事件就行,但是如果你很快滑动到底部呢,弹动现象仍然不能禁止。我曾向技术多次提过在下拉刷新api中提供一个参数用来控制是否禁用底部弹动的,但是前几次技术都是不了了之,最近又问了一次,直接回应“目前市面上有APP是这种效果吗?底部弹动,主要是拟物,跟手,你不喜欢这种效果,不代表每个人都不喜欢。”,我只想说,我又没叫你去掉弹动,只是加一个属性控制是否弹动,这和别人是否喜欢有什么关系,而且尽然还说现在市场的app有这种效果吗?可能我已经脱离了市场轨道。好了,发发牢骚而已,这里不谈技术人员的态度问题,下面分享下我如何实现优雅控制的:
对于这个问题我想过很多种办法,原本想是否改写官方底层来实现,但是尝试了一下放弃了(好吧,我承认是我太水,没改的了),因为就算我把Android改了,那IOS呢,apicloud引擎又没有开源,IOS同样无法实现,那又有什么意义,我想应该有很多人都纠结这个问题,那么到底怎么解决呢?
首先apicloud提供了设置页面bounces的方法,能否从这方面下手,动态改变其bounces状态,在具有下拉意图的时候开启bounces,在具有上拉加载更多意图的时候禁用bounces。但是如何判断意图呢,最初的设想是监听scrolltobottom方法来控制,放滚动到距离底部还有50dp的时候来禁用bounces,在距离顶部50dp(通过计算成距离底部距离)的时候开启bounces,但是发现scrolltobottom有些问题:1、只能监听一个,不能任性的监听,后面的会覆盖前面的。2、滚动到距离底部50dp的时候触发事件,但是滚动到距底部0dp的时候再向下滚任然会触发该事件,这应该是一个bug。原生api使用不了,那js是否提供方法呢,都知道我们的开始使用的是webview,虽然Java可以直接调用webview的原生方法,但是js也可以操作webkit的事件,其中就有onscroll事件,这个是网页滚动的事件。能否通过这个方法来改变bounces呢,试验了一下,还真可以,总体方法就是在使用原生下拉刷新与scrolltobottom监听滚动到底部的同时,使用js监听页面滚动事件,在滚动条距离顶部30像素位置上下分别改变bounces状态。
具体代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="target-densitydpi=device-dpi,maximum-scale=1.0,minimum-scale=1.0,user-scalable=0,width=device-width,initial-scale=1.0"/> <meta name="format-detection" content="telephone=no,email=no,date=no,address=no"> <title>内容页</title> <link rel="stylesheet" type="text/css" href="css/ui-base.css" /> <link rel="stylesheet" type="text/css" href="css/ui-box.css" /> <link rel="stylesheet" type="text/css" href="css/ui-color.css" /> <link rel="stylesheet" type="text/css" href="css/ui.control.css" /> <link rel="stylesheet" type="text/css" href="css/load_more.css" /> <style> html,body{ width:100%; height:100%; } </style> </head> <body class="um-vp"> <div id="page_0" class="up ub ub-ver"> <div id="content" class="ub ub-ver" style="min-height: 300px;"> </div> <div id="loading" class="ub ub-ver" style="background:rgba(0,0,0,0.2);margin-top:5px"></div> </div> </body> <script type="text/javascript" src="./script/zepto.min.js"></script> <script src="script/api.control.js"></script> <script src="script/templete/template.js"></script> <script src="script/ui.widget/ui.listview.js"></script> <script src="script/zepto.widget/zepto.nodata.js"></script> <script> ;(function($){ $(function () { var con={ listview: $.listview({ selector: "#content", type: "thickLine", hasAngle: false, hasIcon: false, hasSmallIcon:false, multiLine: 1, }), pageindex:0, items:20, hasmore:false, times:0, isloading:false, getData:function(){ var self=this; $("#content").empty(); $("#loading").empty(); var data=[]; switch (self.times){ case 0://无数据 break; case 1://5条数据,不足一个屏幕 for(var i=0;i<5;i++){ data.push({ title:"测试数据1" }); } break; case 2://15条数据,超过一个屏幕,但是不足20条,没有加载更多 for(var i=0;i<15;i++){ data.push({ title:"测试数据2" }); } break; case 3://20条,表示可能有更多数据 for(var i=0;i<20;i++){ data.push({ title:"测试数据3" }); } break; } if(data.length>0){ self.listview.add(data,1); } self.logic(data); if(self.times>=3){ self.times=0; }else{ self.times++; } }, logic: function(data) { var self = this; var boxdom=$("#content"); if (boxdom.children().length == 0&&data.length==0) { //从无数据开始 boxdom.nodata(); }else{ if (data.length == 0) { self["hasmore"]= false; if($("#content").find(".ui-nodata")){ //如果已经显示无数据则不进行append操作 }else{ $("#loading").empty().append('<div class="loading_more pause"><div id="loadingglobe" class="sk-spinner sk-spinner-wordpress uhide"><span class="sk-inner-circle"></span></div></div>'); } return; } else { if (boxdom.offset().height > document.body.clientHeight) { if(data.length<self.items){ self["hasmore"]= false; $("#loading").empty().append('<div class="loading_more pause"><div id="loadingglobe" class="sk-spinner sk-spinner-wordpress uhide"><span class="sk-inner-circle"></span></div></div>'); }else{ self["hasmore"]= true; $("#loading").empty().append('<div class="loading_more"><div id="loadingglobe" class="sk-spinner sk-spinner-wordpress uhide"><span class="sk-inner-circle"></span></div></div>'); } } } } }, loadData: function () { var self=this; var data=[{ title:"测试数据4", },{ title:"测试数据4", }]; self.listview.add(data,1); self.logic(data); self.isloading=false; }, init: function () { var bounce=true,self=this; $.apiready(function () { api.setRefreshHeaderInfo({ visible: true, bgColor: '#ccc', textColor: '#fff', textDown: '下拉刷新...', textUp: '松开刷新...', showTime: true }, function(ret, err){ setTimeout(function () { self.getData(); api.refreshHeaderLoadDone(); },2000) }); api.addEventListener({ name:'scrolltobottom', extra:{ threshold:0 } },function(ret,err){ if(self.hasmore&&!self.isloading){ $("#loadingglobe").removeClass("uhide"); $("#loading .loading_more").addClass("active"); self.isloading=true; setTimeout(function () { self.loadData(); },3000) } }); $.showProgress(); setTimeout(function () { self.getData(); $.closeProgress(); },3000) }); window.addEventListener("scroll",function(){ var t = document.documentElement.scrollTop || document.body.scrollTop; if(t<=30){ if(!bounce){ api.setFrameAttr({ name: 'con', bounces: true, }); bounce=true; } }else{ if(bounce){ api.setFrameAttr({ name: 'con', bounces: false, }); bounce=false; } } }); } }; con.init(); }) })(Zepto) </script> </html>
具体体验效果可以下载使用:
在Appcan论坛中也有人曾经问过如何实现的,希望能够参考一下实现,或许有人说使用js监听滚动事件是不是会影响webiew效率,在这里加入了onscroll稀释条件,对于Android的效率影响不是很大,可以放心使用。
有人说在Android中可以这样用,但是IOS中因为IOS引擎在滚动的时候阻塞其内部所有js的执行,只有当滚动停止后才会执行onscroll内的js,这样不就行不通了吗。。那只有Android能实现又有什么用?是的,IOS这样是实现不了的,但是我们都知道IOS的触摸事件响应效率是非常高的,我们是否可以用触摸来代替scroll事件,使用touch事件来模拟scroll事件呢,也就是识别用户滑动的手势来动态判断是上还是下,如果是向上,那么就禁止回弹,如果是向下就开启回弹,这种想法很好,可能有很多人会想到,但是这种会有一个问题,例如用户先向上再突然向下,或者先向下再突然向上这种那就不能在touchend中判断手势了,需要在touchmove中动态判断手势,这种方式也不错,也很容易实现,代码如下:
var sY=0; var flag=0; var cY=0; var sTop=0; var tY=0; var down=false; //控制在100处执行检查,实现稀释 $(window).on("touchstart", function (e) { //记录y坐标 sY=e.touches[0].clientY; cY=sY; sTop=document.getElementById('content').getBoundingClientRect().top; tY=sY; }) $(window).on("touchmove", function (e) { var Y=e.touches[0].clientY; if(Y-tY>=-sTop){//处于下拉中 down=true; }else{ down=false; } //记录y坐标 if(Y-cY>0){//flag: 1 if(flag==-1){ sY=Y; flag=1; return; }else if(flag==0){ flag=1; } }else{ if(flag==1){ sY=Y; flag=-1; return; }else if(flag==0){ flag=-1; } } if(Y-sY>=10&&flag==1){//上滑 if(!bounce){ api.setFrameAttr({ name: 'con', bounces: true, }); bounce=true; } } if(sY-Y>=10&&flag==-1){//下滑 if(bounce&&!down){ api.setFrameAttr({ name: 'con', bounces: false, }); bounce=false; } } cY=Y; }) $(window).on("touchend", function (e) { down=false; sY=0; flag=0; cY=0; sTop=0; tY=0; }) $(window).on("touchcancel", function (e) { down=false; sY=0; flag=0; cY=0; sTop=0; tY=0; })
但是测试会发现一个问题,在底部的时候向下滑动一段距离再向上快速滑动,到底部无法实现锁定,仍然会弹动,这是因为事件响应次序造成的问题,这个问题通过这种方法就不可避免,后来又想是否同Android一样,将手势也固定在一定范围内,例如在content距离webview边框-100~0范围内监听手势来改变,但是最后发现貌似不行,因为当处于最低端快速向上滑动的时候会存在惯性滚动,如果在这段范围内完全由惯性滚动完成,不存在touch事件,那么就无法判断。后来想想完全是自己想的太复杂了,直接监听触摸事件,在触摸事件中监听不就行了,上代码:
//for IOS
;(function () {
;['touchstart', 'touchmove', 'touchend', 'touchcancel',"scroll"].forEach(function(eventName){
$(window).on(eventName, function (e) {
//记录y坐标
var sTop=document.getElementById('content').getBoundingClientRect().top;
if(sTop>=-100){
if(!bounce){
api.setFrameAttr({
name: 'con',
bounces: true,
});
bounce=true;
}
}else{
if(bounce){
api.setFrameAttr({
name: 'con',
bounces: false,
});
bounce=false;
}
}
})
})
})()
这样基本就能解决一下问题了,但是如果暴力测试的话还是有可能有问题的,因为这里的100是要求内容部分总高度大于frame高度100px才行,可以适当调整此大小
IOS版