AlloyTouch动画库学习笔记
alloyTouch可以很方便的用做下拉刷新,抽奖转盘等效果,我一直很好奇他是如何工作的,尤其是它能完美模拟原生的平滑滚动和惯性回弹等效果,而且体积小,速度快。
阅读代码前,我的思考
拖拽的惯性效果实现,看上去这种效果的原理很简单,但是真正实践的时候还是有疑问:
怎么检测到松开鼠标那一刻的速度(初速)呢?
假设我拖拽的中途突然停止,再松开,要怎么处理?
拖拽力度很大的情况如何处理?
如果惯性滚动的移动的距离超出边界,回弹效果怎么做?
拖拽超出边界的橡皮筋效果怎么做?
源代码阅读
带着这些疑问,我开始了代码阅读之旅,下面的笔记没有完完全全的讲解整个框架,只是挑了我觉得(辣鸡如我)
容易疑惑的地方。
初始化
this.isTouchStart = false;
这个变量是为了判断用户是否是从目标DOM触摸开始,有可能是先在wrapper触摸,再移动至目标DOM,如果是这种情况,不触发滚动。
bind(this.element, "touchstart", this._start.bind(this));
bind(this.eventTarget, "touchend", this._end.bind(this));
bind(this.eventTarget, "touchcancel", this._cancel.bind(this));
接下来重点就在于这3个函数了,初始化绑定DOM的事件。
touchstart
对应AlloyTouch.prototype._start
。
_start: function (evt) {
this.isTouchStart = true;
this.touchStart.call(this, evt, this.target[this.property]);
cancelAnimationFrame(this.tickID);
this._calculateIndex();
this.startTime = new Date().getTime(); //起始时间
this.x1 = this.preX = evt.touches[0].pageX;
this.y1 = this.preY = evt.touches[0].pageY;
this.start = this.vertical ? this.preY : this.preX; //startPoint
this._firstTouchMove = true; //这里才是判断是否初次触摸
this._preventMove = false;
},
touchmove
对应AlloyTouch.prototype._move
。
这里有段代码一直让我疑惑很久,为什么touchstart和touchmove间隔大于300ms时,startTime
和start(startPoint)
要重新设置呢?
按我的理解,为了方便检测速度,当此次touchmove事件触发时间比startTime
大于300ms时,重新设定计算速度的startPoint
,这样可以在拖拽轨迹中截取合理的起止长度和时间间隔,计算初速,一般拖拽过程有以下3中情况:
假设拖拽的时长小于300ms,
startPoint
则用touchstart
时设置的,假设拖拽时长大于300ms,
startPoint
用满足条件的新touchmove点。拖拽中途停止,不产生惯性效果(一般情况下,鼠标停止的时间会大于300ms)
这里我有个疑问,为什么不直接用最后一个touchmove点作为startPoint呢?
我的理解是,最后触发的touchmove事件和touchend事件间隔时间很短,虽然间隔时间(dt)可以取得的精度很高,但是,移动的距离差(dv)的单位是px
,假设物体移动了1.999px,最后浏览器还是按1px计算,在dt很小的的情况下,误差就变大了。
对于问题5:橡皮筋效果的实现:拖拽时,如果超出边界,则增加移动的阻力,即用outerFactor
。
touchend
对应AlloyTouch.prototype._end
。
var dt = new Date().getTime() - this.startTime;
if (dt < 300) {...}
对于问题4:判断时间间隔是否小于300ms,如果大于,则判定是拖着不动,再松开,此时没有惯性效果。
对于问题3:惯性滚动的距离destination
超出边界max
且大于最大值maxRegion
(默认600px)时,则惯性滚动的最大距离为max + springMaxRegion(默认60px)
,如下图。
_to
的实现
alloyTouch内部所有的动画执行都交给_to
完成,类似$.fn.animate
,实现如下
/**
* 执行过度效果
* @param value 目标值
* @param time 过渡时间
* @param ease 缓动函数
* @param onChange
* @param onEnd
* @private
*/
_to: function (value, time, ease, onChange, onEnd) {
if (this.fixed) return;
var el = this.target,
property = this.property;
var current = el[property];
var dv = value - current;
var beginTime = new Date();
var self = this;
var toTick = function () {
var dt = new Date() - beginTime;
if (dt >= time) {
el[property] = value;
onChange && onChange.call(self, value);
onEnd && onEnd.call(self, value);
return;
}
el[property] = dv * ease(dt / time) + current;
el[property] = a;
self.tickID = requestAnimationFrame(toTick);
//cancelAnimationFrame必须在 tickID = requestAnimationFrame(toTick);的后面
onChange && onChange.call(self, el[property]);
};
toTick();
},
我们替换一下原有的ease函数,也可以达到同样效果,这里我使用TweenJS
提供的缓动函数
let a = Tween.Quad.easeOut(dt, current, dv, time);
// console.log(a);
// el[property] = dv * ease(dt / time) + current;
el[property] = a;
self.tickID = requestAnimationFrame(toTick);
//cancelAnimationFrame必须在 tickID = requestAnimationFrame(toTick);的后面
onChange && onChange.call(self, el[property]);
总结
初速计算
alloyTouch的大体思路就是在一段拖拽轨迹上,以touchend作为endPoint
,再向前300ms内选取一个startPoint
,由两点计算出初速。
缓动函数相关知识
var Tween = {
Quad: {
/**
*
* @param t 时间(x轴)
* @param b 初始值
* @param c 改变的大小
* @param d 持续时间
* @return {*}
*/
easeOut: function(t,b,c,d){
return -c *(t/=d)*(t-2) + b;
}
}
}
x轴是时间,y轴是当前值,b是y轴的初始值,x轴的初始值是0,t是当前时间。当t(x轴)逐渐增加到达d时,当前值(y轴)会到达目标值(b+c)。
扩展
使用alloyTouch可以很方便的做出类似IOS的select效果
做3D效果就更方便啦
要注意的问题
CSS3中transform:rotateX(30px) translateZ(50px)
和transform: translateZ(50px) rotateX(30px)
的效果是不一样的!!!
前者是先旋转(此时Z轴的方向已经发生改变),再往Z轴偏移50px,后者是先往Z轴偏移50px,并以当前为点基准,再旋转。类似的还有perspective
,属性值的排序会造成影响。
对此,我提了一个issue,大家有兴趣可以去看看
参考文献
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。