项目背景
最近刚刚用vue完成了一个商城项目,ui给的页面里有个如下图的经典滚动组件,带有下拉刷新和上拉加载的功能。我不禁虎躯一震,有点意思。
鉴于老板一直比较讨厌微信下拉露黑底的问题(其实我一直觉得这不是问题,京东也这样)。还是照例使用IScroll-lite作为滚动组件。
iscroll-lite作为iscroll专为移动端所做的精简版,代码控制在1000行以内。
首先肯定考虑到从touchstart,touchmove,touchend事件回调入手
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 327: this._init(); 352: _init: function () { this._initEvents(); }, 895: _initEvents: function(){ var eventType = remove ? utils.removeEvent : utils.addEvent, target = this.options.bindToWrapper ? this.wrapper : window; ....... if ( utils.hasTouch && !this.options.disableTouch ) { eventType(this.wrapper, 'touchstart', this); eventType(target, 'touchmove', this); eventType(target, 'touchcancel', this); eventType(target, 'touchend', this); } } 50: me.addEvent = function (el, type, fn, capture) { el.addEventListener(type, fn, !!capture); }; me.removeEvent = function (el, type, fn, capture) { el.removeEventListener(type, fn, !!capture); };
|
我们可以清楚的看到在IScroll构造函数中调用prototype中的_init方法,完成事件的初始化,这里将this也就是当前对象this传递给addEventListener的第二个参数,那么比较下下面两种事件的注册方式:
1 2 3 4 5 6 7 8 9 10 11 12 13
| function clickHandler() { console.log('clickHandler:', this); } document.addEventListener('click', clickHandler, false); var handleObj = { handleEvent: function() { console.log("handleEvent obj:", this); } } document.addEventListener('click', handleObj, false);
|
可以看出使用function handler,this指向的是绑定事件的元素本身;使用object handler,this指向的是该对象。而IScroll使用的就是object handler,它将为IScroll.prototype添加了handleEvent方法,使得其更容易调用原型上的其他方法。
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
| handleEvent: function (e) { switch ( e.type ) { case 'touchstart': case 'pointerdown': case 'MSPointerDown': case 'mousedown': this.options.touchstartCall(); this._start(e); break; case 'touchmove': case 'pointermove': case 'MSPointerMove': case 'mousemove': this._move(e); break; case 'touchend': case 'pointerup': case 'MSPointerUp': case 'mouseup': case 'touchcancel': case 'pointercancel': case 'MSPointerCancel': case 'mousecancel': this.options.touchendCall(); this._end(e); break; ......... } }
|
滑动加速度的实现
之后在_start回调函数中记录了startX,absStartX等等起始值。在_move回调中this._translate(newX, newY);进行页面css3位移,以垂直滚动的newY计算为例:
1 2 3 4 5 6 7 8
| newY = this.y + deltaY; var point = e.touches ? e.touches[0] : e, deltaY = point.pageY - this.pointY, this.pointY = point.pageY;
|
可以看出当你快速滑动的时候,deltaY作为垂直方向的加速度,值也越大,页面也会滑动更迅速。这就是滑动加速的实现。
bounce效果的实现
还是以垂直滚动为例,其实还是newY的计算问题。看_move方法中的这段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| if ( newY > 0 || newY < this.maxScrollY ) { newY = this.options.bounce ? this.y + deltaY / 3 : newY > 0 ? 0 : this.maxScrollY; } if ( this.resetPosition(this.options.bounceTime) ) { return; } resetPosition{ if ( !this.hasVerticalScroll || this.y > 0 ) { y = 0; } else if ( this.y < this.maxScrollY ) { y = this.maxScrollY; } this.scrollTo(x, y, time, this.options.bounceEasing); }
|
newY > 0就是上滑超出边界,newY < this.maxScrollY就是下滑超出边界。这时,this.options.bounce默认值为true,newY的值就以this.y + deltaY / 3减速增长。在_end中又调用resetPosition,resetPostion scrollTo正常的边界,这就是bounce的实现
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
| if ( this.options.probeType == 3 ) { this.options.useTransition = false; } scrollTo: function (x, y, time, easing) { easing = easing || utils.ease.circular; this.isInTransition = this.options.useTransition && time > 0; if ( !time || (this.options.useTransition && easing.style) ) { this._transitionTimingFunction(easing.style); this._transitionTime(time); this._translate(x, y); } else { this._animate(x, y, time, easing.fn); } } _animate: function (destX, destY, duration, easingFn) { var that = this, startX = this.x, startY = this.y, startTime = utils.getTime(), destTime = startTime + duration; function step () { var now = utils.getTime(), newX, newY, easing; if ( now >= destTime ) { that.isAnimating = false; that._translate(destX, destY); if ( !that.resetPosition(that.options.bounceTime) ) { that._execEvent('scrollEnd'); } return; } now = ( now - startTime ) / duration; easing = easingFn(now); newX = ( destX - startX ) * easing + startX; newY = ( destY - startY ) * easing + startY; that._translate(newX, newY); if(that.isAnimating) { tempRfa = rAF(step); } if ( !!that.options.pullupH ) { that._execEvent('scroll'); } } this.isAnimating = true; step(); },
|
由于会下拉刷新和上拉加载更多会需要监听滚动事件,而iscroll-probe中需要监听滚动时将useTransition设置为false,导致滚动调用的一致都是_animate,_animate方法用到了requestAnimationFrame,也就是逐帧动画,每一帧绘制结束之后调用step函数,完成滚动的动画实现。
IScroll的主要代码分析大概就是这样,接下来到了vue刷新组件的实现
vue滚动刷新插件的实现
先贴出项目github地址:vue pullup refresh and pulldown loadmore component
这是可能出现的状态:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <template> <div :id="wrapperId" class="wrapper"> <div :id="scrollId" class="scroll"> <div v-if="needRefresh" class="pullup"> <div v-show="pullupStatus==1">↓下拉刷新</div> <div v-show="pullupStatus==2">↑释放立即刷新</div> <div v-show="pullupStatus==3">加载中...</div> <div v-show="pullupStatus==4">刷新成功</div> <div v-show="pullupStatus==5">刷新失败,请重试</div> </div> <slot></slot> <div v-if="needRefresh && hasMoreData" class="pulldown"> <div v-show="pulldownStatus==2">加载中...</div> <div v-show="pulldownStatus==4">加载失败,请重试</div> </div> <div v-if="!hasMoreData && moreTip" class="noData">没有更多数据了</div> </div> </div> </template>
|
pullup和pulldown高度都限定为40。下拉刷新思路如下:
- 用户下拉,页面要随之下拉。这里有个问题:iscroll小于一屏的时候没有bounce效果。所以这里要加上这段代码;
- 下拉高度>2*pullupH,pulldownStatus从1变成2;
- 用户释放,如果pullupStatus==2,则这是激发刷新操作,pullupStatus赋值为3;反之如果pullupStatus==1,则不做任何操作;这里有个问题:在加载的过程中,应该滚动到y=pullupH的高度,只有接收到refresh-finish的broadcast的时候,才滚动到0。所以这里肯定要修改resetPosition的代码;
刷新相关重点代码
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
| moveOverScroll: function(newY){ if(this.directionLocked == 'h' && this.options.hswipeStopScroll && this.options.scrollY){ return 0; } if(this.options.pullupH > 0 && this.y >= 0){ var tempx = this.pointY-this.startPointY + this.absStartY; var tempplH = this.options.pullupH; if(tempx > 0){ if(tempx > 0 && tempx <= tempplH){ } else if(tempx <= tempplH * 2){ tempx = tempplH + (tempx - tempplH) * 0.5; } else if(tempx > tempplH * 2){ tempx = tempplH + tempplH * 0.5 + (tempx - tempplH * 2) * 0.2 } if ( this.options.probeType == 3 ) { this._execEvent('scroll'); } return tempx; } } return newY; }, newY = this.moveOverScroll(newY); this._translate(newX, newY);
|
在move函数中调用moveOverScroll函数,手动计算滚动y值
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
| teCall () { this.isTouch = false; if(this.pullupStatus == 2){ this.pullupStatus = 3; this.myScroll.setRefresh(true); this.refreshCall(this.scrollId); } } "refresh-finish": function(msg){ let self = this; msg = msg || {}; msg.scrollId = msg.scrollId || 'scroll'; if(self.scrollId == msg.scrollId){ if(msg.errorCode == 0) self.pullupStatus = 4; else self.pullupStatus = 5; self.myScroll.setRefresh(false); if(self.hasMoreData) self.pulldownStatus = 2; } }, setRefresh: function(isRefreshing){ var self = this; if(isRefreshing){ this.isRefreshing = true; } else { this.isRefreshing = false; if(this.isAnimating){ function endCall(){ self.off('scrollEnd', endCall); self.refresh(); } this.on('scrollEnd', endCall); } else { this.resetPosition(this.options.bounceTime); } } }, resetPosition: function(time){ if(this.isRefreshing){ y = this.options.pullupH; } else { y = y; } }, _animate: function (destX, destY, duration, easingFn) { if ( !that.resetPosition(that.options.bounceTime) ) { that._execEvent('scrollEnd'); } }
|
这一段就是刷新的代码,teCall是自定义的touchend回调,在teCall中设置isRefreshing为true。这样在_end中调用resetPosition的时候,y值就会被设置成为pullupH。resetPosition也就回scrollTo(0, pullupH)。如果这个时候刷新结束,在setRefresh函数中会监听_animate的scrollEnd事件;这里不需要再次手动调用_animate,因为在_animate函数中,滚动结束会调用resetPostion,而这时isRefresh已经变成了false,所以又是resetPosition会scrollTo(0, 0); 等到真正滚动到0,animate中也就回_execEvent(‘scrollEnd’); 这时再refresh当前页面,重新计算高度。
加载更多相关重点代码
上拉加载更多其实没什么好说的。无非就是一个滚动到哪里激发事件的问题,这里注意还要考虑加载一次之后可能还是不足一页的情况
1 2 3
| showLoadMore () { return (this.myScroll.y<this.myScroll.maxScrollY+pullupH/2) || (this.myScroll.maxScrollY==0); },
|
最终效果