5

AlloyTouch动画库学习笔记

alloyTouch可以很方便的用做下拉刷新,抽奖转盘等效果,我一直很好奇他是如何工作的,尤其是它能完美模拟原生的平滑滚动和惯性回弹等效果,而且体积小,速度快。

阅读代码前,我的思考

拖拽的惯性效果实现,看上去这种效果的原理很简单,但是真正实践的时候还是有疑问:

  1. 怎么检测到松开鼠标那一刻的速度(初速)呢?

  2. 假设我拖拽的中途突然停止,再松开,要怎么处理?

  3. 拖拽力度很大的情况如何处理?

  4. 如果惯性滚动的移动的距离超出边界,回弹效果怎么做?

  5. 拖拽超出边界的橡皮筋效果怎么做?

源代码阅读

带着这些疑问,我开始了代码阅读之旅,下面的笔记没有完完全全的讲解整个框架,只是挑了我觉得(辣鸡如我)容易疑惑的地方。

初始化

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

clipboard.png

这里有段代码一直让我疑惑很久,为什么touchstart和touchmove间隔大于300ms时,startTimestart(startPoint)要重新设置呢?
按我的理解,为了方便检测速度,当此次touchmove事件触发时间比startTime大于300ms时,重新设定计算速度的startPoint,这样可以在拖拽轨迹中截取合理的起止长度和时间间隔,计算初速,一般拖拽过程有以下3中情况:

  1. 假设拖拽的时长小于300ms,startPoint则用touchstart时设置的,

  2. 假设拖拽时长大于300ms,startPoint用满足条件的touchmove点。

  3. 拖拽中途停止,不产生惯性效果(一般情况下,鼠标停止的时间会大于300ms

这里我有个疑问,为什么不直接用最后一个touchmove点作为startPoint呢?
我的理解是,最后触发的touchmove事件和touchend事件间隔时间很短,虽然间隔时间(dt)可以取得的精度很高,但是,移动的距离差(dv)的单位是px,假设物体移动了1.999px,最后浏览器还是按1px计算,在dt很小的的情况下,误差就变大了。

clipboard.png

对于问题5:橡皮筋效果的实现:拖拽时,如果超出边界,则增加移动的阻力,即用outerFactor

touchend

对应AlloyTouch.prototype._end

  var dt = new Date().getTime() - this.startTime;
  if (dt < 300) {...}

对于问题4:判断时间间隔是否小于300ms,如果大于,则判定是拖着不动,再松开,此时没有惯性效果。

clipboard.png

对于问题3:惯性滚动的距离destination超出边界max且大于最大值maxRegion(默认600px)时,则惯性滚动的最大距离为max + springMaxRegion(默认60px),如下图。

clipboard.png

_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效果

GIF.gif

做3D效果就更方便啦

clipboard.png

要注意的问题

CSS3中transform:rotateX(30px) translateZ(50px)transform: translateZ(50px) rotateX(30px)的效果是不一样的!!!
前者是先旋转(此时Z轴的方向已经发生改变),再往Z轴偏移50px,后者是先往Z轴偏移50px,并以当前为点基准,再旋转。类似的还有perspective,属性值的排序会造成影响。
对此,我提了一个issue,大家有兴趣可以去看看

参考文献

  1. JavaScript Tween算法及缓动效果

  2. 缓动函数速查表

  3. alloyTouch


Larry_
704 声望186 粉丝

FE