您的位置:首页 > 其它

【iScroll源码学习02】分解iScroll三个核心事件点

2014-01-01 15:50 337 查看

前言

最近两天看到很多的总结性发言,我想想今年好像我的变化挺大的,是不是该晚上来水一发呢?嗯,决定了,晚上来水一发!

上周六,我们简单模拟了下iScroll的实现,周日我们开始了学习iScroll的源码,今天我们接着上次的记录学习,因为最近事情稍微有点多了

学习进度可能要放慢,而且iScroll这个库实际意义很大,不能囫囵吞枣的学习,要学到精华,并且要用于项目中的,所以初步规划是最近两周主要围绕iScroll展开

而后两个选择:① 分离iScroll代码用于项目;② 抄袭iScroll精华部分用于项目。无论如何都要用于项目......

iScroll源码学习01准备阶段

SPA移动站点APP化研究之上中下页面的iScroll化

几个事件点

iScroll的整体逻辑由三大事件点组成:

① touchStart(mousedown)

② touchMove(mousemove)

③ touchEnd(mouseUp)

也就是iScroll整体的功能逻辑其实是由这几个事件串起来的,其中

touchStart会保留一些初始化操作,或者停止正在进行的动画

touchMove会带动dom一起移动

而touchEnd最为复杂,在touchend阶段可能需要处理很多东西

① 一般性拖动结束事件
② 超出边界还原后触发的事件(此时可以滚动加载数据)
③ 如果此次为一次按钮点击,需要触发按钮事件那么还有对preventDefault进行处理(preventDefault可能导致事件不触发)
④ 如果此次为一次点击事件,并且对象为文本框或者select(其它会获得焦点的事件),那么应该让其获得焦点,并且弹出键盘
⑤ ......


以上为主观臆测下的猜想,我们来看看iScroll实际干了些什么,下面再细细的分析各个阶段

start

_end: function (e) {
if (!this.enabled || utils.eventType[e.type] !== this.initiated) {
return;
}

if (this.options.preventDefault && !utils.preventDefaultException(e.target, this.options.preventDefaultException)) {
e.preventDefault();
}

var point = e.changedTouches ? e.changedTouches[0] : e,
momentumX,
momentumY,
duration = utils.getTime() - this.startTime,
newX = Math.round(this.x),
newY = Math.round(this.y),
distanceX = Math.abs(newX - this.startX),
distanceY = Math.abs(newY - this.startY),
time = 0,
easing = '';

this.isInTransition = 0;
this.initiated = 0;
this.endTime = utils.getTime();

// reset if we are outside of the boundaries
if (this.resetPosition(this.options.bounceTime)) {
return;
}

this.scrollTo(newX, newY); // ensures that the last position is rounded

// we scrolled less than 10 pixels
if (!this.moved) {
if (this.options.tap) {
utils.tap(e, this.options.tap);
}

if (this.options.click) {
utils.click(e);
}

this._execEvent('scrollCancel');
return;
}

if (this._events.flick && duration < 200 && distanceX < 100 && distanceY < 100) {
this._execEvent('flick');
return;
}

// start momentum animation if needed
if (this.options.momentum && duration < 300) {
momentumX = this.hasHorizontalScroll ? utils.momentum(this.x, this.startX, duration, this.maxScrollX, this.options.bounce ? this.wrapperWidth : 0) : { destination: newX, duration: 0 };
momentumY = this.hasVerticalScroll ? utils.momentum(this.y, this.startY, duration, this.maxScrollY, this.options.bounce ? this.wrapperHeight : 0) : { destination: newY, duration: 0 };
newX = momentumX.destination;
newY = momentumY.destination;
time = Math.max(momentumX.duration, momentumY.duration);
this.isInTransition = 1;
}

if (this.options.snap) {
var snap = this._nearestSnap(newX, newY);
this.currentPage = snap;
time = this.options.snapSpeed || Math.max(
Math.max(
Math.min(Math.abs(newX - snap.x), 1000),
Math.min(Math.abs(newY - snap.y), 1000)
), 300);
newX = snap.x;
newY = snap.y;

this.directionX = 0;
this.directionY = 0;
easing = this.options.bounceEasing;
}

// INSERT POINT: _end

if (newX != this.x || newY != this.y) {
// change easing function when scroller goes out of the boundaries
if (newX > 0 || newX < this.maxScrollX || newY > 0 || newY < this.maxScrollY) {
easing = utils.ease.quadratic;
}

this.scrollTo(newX, newY, time, easing);
return;
}

this._execEvent('scrollEnd');
},


View Code
开始我们就说过,touchend为这个控件一个关键点与难点,现在我们就来啃一啃,这个看完了,iScroll核心部分也就结束了,后面就只需要拆解分析即可

首先仍然是一点初始化操作

if (!this.enabled || utils.eventType[e.type] !== this.initiated) {
return;
}

if (this.options.preventDefault && !utils.preventDefaultException(e.target, this.options.preventDefaultException)) {
e.preventDefault();
}


而后逐步好戏上场了,在手指离开前做了状态保存

var point = e.changedTouches ? e.changedTouches[0] : e,
momentumX,
momentumY,
duration = utils.getTime() - this.startTime,
newX = Math.round(this.x),
newY = Math.round(this.y),
distanceX = Math.abs(newX - this.startX),
distanceY = Math.abs(newY - this.startY),
time = 0,
easing = '';

this.isInTransition = 0;
this.initiated = 0;
this.endTime = utils.getTime();


duration是当前拖动的事件,这里可不是手指触屏到离开哦,因为move时候每300ms变了一次

PS:这里想象一下如果我们想要快速的滑动是不是触屏屏幕很快呢,而我们一直拖动DOM在最后也是有可能想让他快速移动的

记录当前x,y位置记录当前移动位置distanceY,然后重置结束时间,这里有一个resetPosition方法:

resetPosition: function (time) {
var x = this.x,
y = this.y;

time = time || 0;

if ( !this.hasHorizontalScroll || this.x > 0 ) {
x = 0;
} else if ( this.x < this.maxScrollX ) {
x = this.maxScrollX;
}

if ( !this.hasVerticalScroll || this.y > 0 ) {
y = 0;
} else if ( this.y < this.maxScrollY ) {
y = this.maxScrollY;
}

if ( x == this.x && y == this.y ) {
return false;
}

this.scrollTo(x, y, time, this.options.bounceEasing);

return true;
},


他是记录我们是不是已经离开了边界了,如果离开边界了就不会执行后面逻辑,而直接重置DOM位置,这里还用到了我们的scrollTo方法,该方法尤其关键

scrollTo

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);
}
},


这个方法是此处一个重要的方法,传入距离与时间后,他就会高高兴兴的移动到对应位置

如果启用了CSS3的动画,便会使用CSS3动画方式进行动画(这个动画我们下期再说),否则使用_animate方法(js实现方案)

_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 ) {
rAF(step);
}
}

this.isAnimating = true;
step();
},


这里用到了前文说描述的settimeout实现动画方案,这里有一点需要我们回到start部分重新思考,为什么CSS停止了动画?

原因是因为transitionend事件

transitionend 事件会在 CSS transition 结束后触发. 当transition完成前移除transition时,比如移除css的transition-property 属性,事件将不会被触发.


_transitionEnd: function (e) {
if ( e.target != this.scroller || !this.isInTransition ) {
return;
}

this._transitionTime();
if ( !this.resetPosition(this.options.bounceTime) ) {
this.isInTransition = false;
this._execEvent('scrollEnd');
}
},


所以,我们第二次touchstart时候,便高高兴兴停止了动画(之一_transitionTime未传time时候会重置时间),所以先取消动画再移动位置

于是继续回到我们的end事件,

this.scrollTo(newX, newY);


如果没有超出边界便滑动到应该去的位置(这里有动画哦)

点击情况

当然,我们手指可能当前只不过想点击而已,这个时候就要触发相关的点击事件了,如果需要获取焦点,便获取焦点

PS:他这里还模拟的fastclick想提升响应速度,但是他这样会引起大量BUG

if (!this.moved) {
if (this.options.tap) {
utils.tap(e, this.options.tap);
}

if (this.options.click) {
utils.click(e);
}

this._execEvent('scrollCancel');
return;
}

if (this._events.flick && duration < 200 && distanceX < 100 && distanceY < 100) {
this._execEvent('flick');
return;
}


运动参数

第一步的scrollTo其实可以放到move里面去,后面就用到了我们上文所说,根据动力加速度计算出来的动画参数:

if (this.options.momentum && duration < 300) {
momentumX = this.hasHorizontalScroll ? utils.momentum(this.x, this.startX, duration, this.maxScrollX, this.options.bounce ? this.wrapperWidth : 0) : { destination: newX, duration: 0 };
momentumY = this.hasVerticalScroll ? utils.momentum(this.y, this.startY, duration, this.maxScrollY, this.options.bounce ? this.wrapperHeight : 0) : { destination: newY, duration: 0 };
newX = momentumX.destination;
newY = momentumY.destination;
time = Math.max(momentumX.duration, momentumY.duration);
this.isInTransition = 1;
}


那个snap不必关注,直接看下面,在此使用

this.scrollTo(newX, newY, time, easing);


开始运动,最后触发scrollend事件,这里如果超出边界会执行resetPosition方法还原的,不必关心

由此,我们几大核心事件点便学习结束了,轻松愉快哈

结语

今天学习了iScroll的几个核心点,我们下次来说下他的滚动条以及事件机制相关,整个iScroll就七七八八了
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: