从源码角度深入分析iScroll中的scrollToElement方法

问题1:官方解释?

scrollToElement(el, time, offsetX, offsetY, easing)
You're gonna like this. Sit tight.
The only mandatory parameter is el. Pass an element or a selector and iScroll will try to scroll to the top/left of that element.
time is optional and sets the animation duration.
offsetX and offsetY define an offset in pixels, so that you can scroll to that element plus a the specified offset. Not only that. If you set them to true the element will be centered on screen. Refer to the scroll to element example.
easing works the same way as per the scrollTo method.

译如下:

el选项可以是选择器或者元素,如果是选择器就会在id='scroller'元素下选择该元素。iScroll会移动到该元素具有的left/top处
time选项表示transition duration,是可选的
offsetX,offsetY采用像素定义偏移,因此你可以滚动到一个元素的时候加上这个偏移量。如果你设置为true那么就会在视口居中
easing表示动画曲线

问题2:下面的提到元素的说明

<div id="wrapper">
	<div id="scroller">
		<ul>
			<li>Pretty row 1</li>
			<li>Pretty row 2</li>
			<li>Pretty row 3</li>
			<li>Pretty row 4</li>
			<li>Pretty row 5</li>
			<li>Pretty row 6</li>
			<li>Pretty row 7</li>
			<li>Pretty row 8</li>
		</ul>
	</div>
</div>
注意:上面是一个标准的iscroll使用案例,下面我提到的scroller就是id='scroller'的元素,而wrapper就是id='wrapper'的元素。

问题3:在学习这个方法的源码之前我们看看两个简单的方法?

首先就是下面的_translate方法:

_translate: function (x, y) {
		//如果使用transform动画那么我们添加transform就行
		if ( this.options.useTransform ) {
/* REPLACE START: _translate */
			this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ;
/* REPLACE END: _translate */
		} else {
			x = Math.round(x);
			y = Math.round(y);
			//获取结束位置,然后使用left/top来移动元素,同时this.x/this.y保存的就是我们的结束位置!!!!!!
			this.scrollerStyle.left = x + 'px';
			this.scrollerStyle.top = y + 'px';
		}
		this.x = x;
		this.y = y;
     // INSERT POINT: _translate
	}
注意:该方法其实很简单,其做的主要工作就是把scroller元素在水平和垂直方向移动到指定的位置,如果使用了transform选项那么就使用tranform来完成,否则使用left/top。同时通过this.x和this.y记录当前iScroll元素所处的位置(如果往上不断移动,那么其值为负数;如果向下不断移动,那么其值为正数;如果刚好显示,那么其值为0)。
第二个方法就是_animate方法:

//使用animation动画调用方式为	this._animate(x, y, time, easing.fn);
    //这个step函数就是分很多步来计算和移动到最终位置
	_animate: function (destX, destY, duration, easingFn) {
		var that = this,
			startX = this.x,
			startY = this.y,
			//获取当前iScroll的位置
			startTime = utils.getTime(),
			//获取当前时间
			destTime = startTime + duration;
           //结束时间的计算
		function step () {
			var now = utils.getTime(),
				newX, newY,
				easing;
            //比较当前时间和结束时间,如果当前时间比结束时间还大表示动画已经结束了,isAnimating设置为false
			if ( now >= destTime ) {
				that.isAnimating = false;
				that._translate(destX, destY);
				//我们直接移动到最终的位置就可以了,这时候你会看到我们设置持续时间
				if ( !that.resetPosition(that.options.bounceTime) ) {
					that._execEvent('scrollEnd');
					//触发scrollEnd事件
				}

				return;
			}

			now = ( now - startTime ) / duration;
			//当前时间-开始时间/持续时间,得到一个小于1的数字,然后转化为我们的Easing函数的参数传入得到一个值!
			easing = easingFn(now);
		    //上面计算得到的值乘以destX-startX,然后加上startX就是我们当前的新的坐标值
			newX = ( destX - startX ) * easing + startX;
			newY = ( destY - startY ) * easing + startY;
			that._translate(newX, newY);
            //得到新的坐标值后我们继续通过left/top或者transform来完成计算就可以了
			if ( that.isAnimating ) {
				rAF(step);
				//到下一帧我们继续调用step函数
			}
		}
		this.isAnimating = true;
		//isAnimating设置为true表示开始动画,并调用step方法
		step();
	}
其实这个方法有很多值得学习的地方,通过这个方法,可以学习如何计算得到自己的动画曲线。该方法主要的作用和上面的_translate一样,就是把 scroller元素移动到指定的位置。我们再看看其中的scrollTo方法,其调用的就是上面的animate和translate方法:

scrollTo: function (x, y, time, easing) {
		easing = easing || utils.ease.circular;
		//获取circular对象,该对象有style和fn属性
		this.isInTransition = this.options.useTransition && time > 0;
		//是否使用transtion动画,同时time>0
		var transitionType = this.options.useTransition && easing.style;
		//这里的transitionType就是我们的动画的贝塞尔曲线
		if ( !time || transitionType ) {
				if(transitionType) {
					//如果存在贝塞尔曲线
					this._transitionTimingFunction(easing.style);
					//为scroller元素添加transtion-timing-function函数
					this._transitionTime(time);
					//这是transition-duration属性
				}
				//我们直接移动到x,y的坐标
			this._translate(x, y);
		} else {
			//如果没有transitionType,这时候我们使用animate就可以了
			this._animate(x, y, time, easing.fn);
			//这时候我们使用animation动画而不是使用transition动画来完成,其中easing.fn和我们的easing.style是同一个贝塞尔函数的不同表达
		}
	}
这个方法的作用在于统一了上面把scroller元素移动到指定位置的方式。

问题4:我们来看看scrollToElement方法

介绍之前贴上两个工具方法,第一个是计算元素的偏移量,第二个是计算元素在视口中的位置

                  utils.offset = function (el) {
				var left = -el.offsetLeft,
					top = -el.offsetTop;
				// jshint -W084
				while (el = el.offsetParent) {
					left -= el.offsetLeft;
					top -= el.offsetTop;
				}
				// jshint +W084
				return {
					left: left,
					top: top
				};
			};
注意:通过这种方式计算存在一点问题,那就是忽略了父元素的border!

utils.getRect = function(el) {
		if (el instanceof SVGElement) {
		//SVG采用的是getBoundingClientRect,而其他元素采用的是offsetWidth等
			var rect = el.getBoundingClientRect();
			return {
				top : rect.top,
				left : rect.left,
				width : rect.width,
				height : rect.height
			};
		} else {
			return {
				top : el.offsetTop,
				left : el.offsetLeft,
				width : el.offsetWidth,
				height : el.offsetHeight
			};
		}
该方法可以计算出元素的位置和大小。我们看看ScrollToElement方法:

scrollToElement: function (el, time, offsetX, offsetY, easing) {
		el = el.nodeType ? el : this.scroller.querySelector(el);
        //如果参数是Element,那么获取该Element,否则在scroller元素下通过选择器获取该元素
		if ( !el ) {
			return;
		}
        //获取该元素的offset值,返回的包括left/top。计算如下:
		var pos = utils.offset(el);
		//之所以计算offset值,是因为scroller元素已经经过定位了,而offset就是计算到定位父元素的距离
		pos.left -= this.wrapperOffset.left;
		pos.top  -= this.wrapperOffset.top;
		//其中this.wrapperOffset = utils.offset(this.wrapper);
		//获取该元素相对于wrapper元素移动的距离是多少
		// if offsetX/Y are true we center the element to the screen
		var elRect = utils.getRect(el);
		var wrapperRect = utils.getRect(this.wrapper);
		//获取wrapper元素相对于视口或者定位的父元素的距离
		if ( offsetX === true ) {
			//注意:这里将会是负数,表示element元素要在X轴移动的距离
			offsetX = Math.round(elRect.width / 2 - wrapperRect.width / 2);
		}
		//注意:这里将会是负数,表示element元素要在Y轴移动的距离
		if ( offsetY === true ) {
			offsetY = Math.round(elRect.height / 2 - wrapperRect.height / 2);
		}
        //更新元素的left/top的值,这是通过offsetParent来计算出来的
		pos.left -= offsetX || 0;
		pos.top  -= offsetY || 0;
		pos.left = pos.left > 0 ? 0 : pos.left < this.maxScrollX ? this.maxScrollX : pos.left;
		pos.top  = pos.top  > 0 ? 0 : pos.top  < this.maxScrollY ? this.maxScrollY : pos.top;
        //如果元素的left大于0,也就是其相对于wrapper的offsetLeft为0表示元素已经显示出来了,left设置为0就可以了
        //如果left>this.maxScrollX也就是-left<-this.maxScrollX,这时候表示向左还没有滚动到极限,这时候设置为left就可以了
		time = time === undefined || time === null || time === 'auto' ? Math.max(Math.abs(this.x-pos.left), Math.abs(this.y-pos.top)) : time;
        //如果没有设置时间,那么自动计算时间
		this.scrollTo(pos.left, pos.top, time, easing);
	}
我们看看下图的分析:


通过上图应该不难理解了

if ( offsetX === true ) {
			//注意:这里将会是负数,表示element元素要在X轴移动的距离
			offsetX = Math.round(elRect.width / 2 - wrapperRect.width / 2);
		}
		//注意:这里将会是负数,表示element元素要在Y轴移动的距离
		if ( offsetY === true ) {
			offsetY = Math.round(elRect.height / 2 - wrapperRect.height / 2);
		}
  //更新元素的left/top的值,这是通过offsetParent来计算出来的
		pos.left -= offsetX || 0;
		pos.top  -= offsetY || 0;

这时候如果你把offsetX,offsetY设置为true,元素就会在视口中居中。此时offsetX,offsetY为负数所以pos.left和pos.top会变大,所以会向下移动。

pos.left = pos.left > 0 ? 0 : pos.left < this.maxScrollX ? this.maxScrollX : pos.left;
pos.top  = pos.top  > 0 ? 0 : pos.top  < this.maxScrollY ? this.maxScrollY : pos.top;
如果pos.left>0,表示其相对于wrapper的距离为正数,所以已经在视口中。如果小于0,同时小于this.maxScrollX,那么表示左边隐藏太多,是不允许的,超过了临界值,所以直接赋值为this.maxScrollX就可以了。

time = time === undefined || time === null || time === 'auto' ? 
		Math.max(Math.abs(this.x-pos.left), Math.abs(this.y-pos.top)) : time;
如果事件设定为undefined/null/'auto'就会自动计算。this.y表示scroller元素已经移动的距离,pos.top表示要出现在视口还要移动的距离。

总结:

(1)不管前面的_animate还是_translate变化的都是scroller元素的坐标,但是这个坐标也通过this.x/this.y也就是iScroll进行了记录。

(2)当我们给wrapper和scroller都进行了定位的时候,scroller是相对于wrapper来定位的,所以当不断向上滚动而出现scroller有隐藏部分的时候,这时候scroller的top值已经是负数了(如果使用了transform,那么translateY就是负数了),同时this.x也是负数。

(3)iScroll没有讨论在wrapper和scroller元素之间有padding等的情况。如果给wrapper添加padding-top那么也会一起滚动,只要出现在wrapper中

你可能感兴趣的:(从源码角度深入分析iScroll中的scrollToElement方法)