为了账号安全,请及时绑定邮箱和手机立即绑定

【小贴士】关于transitionEnd/animate的一个有趣故事

标签:
产品

前言

在很久之前,我们项目有一个动画功能,功能本身很简单,便是典型的右进左出,并且带动画功能

以当时来说,虽然很简单,但是受限于框架本身的难度,就直接使用了CSS3的方式完成了功能

当时主要使用transform与animation实现功能,并且用了一个settimeout执行回调,然后此事便不了了之了

但是出来混总是要还的,这不,最近相似的东西又提了出来,我们当然可以将原来的那套东西拿来用,但是看着那个settimeout总是不是滋味,因为这样捕捉回调的效果以及可能引起的BUG大家都懂,于是就想使用transitionEnd监控动画结束再执行相关回调,于是便有了一个有趣的想法

当时的心声

嗯,不行,这次我要写一个通用的东西,他至少有这些功能:

① 我可以给他一个CSS变化属性

② 我可以给他一个时间长度

③ 我可以给他一个动画曲线参数

有了以上东西我就可以让一个元素触发动画,并且对其注册transitionEnd事件,最后执行我们的回调,于是我基本就陷进去了

但是,我想着想着突然感觉不对,感觉以上东西好像在哪里见过,于是一个叫animate的东西冒了出来

突然一刹那,我有一个不妙的感觉,搞出来一看:

复制代码

animateanimate(properties, [duration, [easing, [function(){ ... }]]])   ⇒ self      animate(properties, { duration: msec, easing: type, complete: fn })   ⇒ self      animate(animationName, { ... })   ⇒ self  对当前Zepto集合对象中元素进行css transition属性平滑过渡。properties: 一个对象,该对象包含了css动画的值,或者css帧动画的名称。duration (默认 400):以毫秒为单位的时间,或者一个字符串。fast (200 ms)slow (600 ms)任何$.fx.speeds自定义属性easing (默认 linear):指定动画的缓动类型,使用以下一个:easelinearease-in / ease-outease-in-outcubic-bezier(...)complete:动画完成时的回调函数

复制代码

于是,我自己的想法就只能呵呵了,这个就是我要的嘛......

而且zepto里面便是监听transitionEnd这个事件触发回调,所以,我们今天就来学习这个animate即可!!!

transitionEnd

transitionEnd是CSS3动画transition唯一的事件,我之前还去找个transitionStart,米有找到......

介绍他之前,我们先来个简单的例子,W3C上面的例子:

复制代码

<!DOCTYPE html><html><head>    <style>        div { width: 100px; height: 100px; background: blue; transition: width 2s; -moz-transition: width 2s; /* Firefox 4 */ -webkit-transition: width 2s; /* Safari and Chrome */ -o-transition: width 2s; /* Opera */ }                div:hover { width: 300px; }    </style></head><body>    <div>    </div>    <p>        请把鼠标指针移动到蓝色的 div 元素上,就可以看到过渡效果。</p>    <p>        <b>注释:</b>本例在 Internet Explorer 中无效。</p></body></html>

复制代码

好了,现在若是我们要在动画结束时候加一个事件该怎么办呢? 

复制代码

<!DOCTYPE html><html><head>    <style>        div { width: 100px; height: 100px; background: blue; transition: width 1s; -moz-transition: width 1s; /* Firefox 4 */ -webkit-transition: width 1s; /* Safari and Chrome */ -o-transition: width 1s; /* Opera */ }                div:hover { width: 300px; }    </style></head><body>    <div id="demo">    </div>    <br />    <span id="msg"></span>    <p>        请把鼠标指针移动到蓝色的 div 元素上,就可以看到过渡效果。</p>    <p>        <b>注释:</b>本例在 Internet Explorer 中无效。</p>    <script type="text/javascript">        var demo = document.getElementById('demo');        var msg = document.getElementById('msg');//        eventType(this.scroller, 'transitionend', this);//        eventType(this.scroller, 'webkitTransitionEnd', this);//        eventType(this.scroller, 'oTransitionEnd', this);//        eventType(this.scroller, 'MSTransitionEnd', this);        demo.addEventListener('webkitTransitionEnd', function () {            msg.innerHTML = '事件回调,当前原始宽度:' + window.getComputedStyle(demo).width;        });            </script></body></html>

复制代码

这个例子虽然简单却很好的说明了一些问题,现在我们就来简单模拟一下animate

简单模拟animate

既然zepto已经很好的实现了该功能,我们这里就简单的模拟下即可,然后看看zepto源码

复制代码

var demo = document.getElementById('demo');var msg = document.getElementById('msg');//简单模拟animate,参数问题就不管他了,暂时只考虑width吧function animate(el, css, time, fn) {        if (!el) return;  var callback = function () {    fn(arguments);    el.removeEventListener('webkitTransitionEnd', callback);  };  el.addEventListener('webkitTransitionEnd', callback);  for (var k in css) {    //这里暂时只考虑webkit内核    el.style['-webkit-transition'] = k + ' ' + time + 's';  }  for (var k in css) {    //这里暂时只考虑webkit内核    el.style[k] = css[k];  }}demo.addEventListener('mouseenter', function () {  animate(demo, { width: '300px' }, 1, fn);});demo.addEventListener('mouseout', function () {  animate(demo, { width: '100px' }, 2, fn);});var fn = function () {  msg.innerHTML = '事件回调,当前原始宽度:' + window.getComputedStyle(demo).width;}

复制代码

这是一个简单的实现,每次执行animate的时候,先会执行一次transitionEnd的事件注册,并且执行一次后就销毁

第二步为其设置transition属性,如果可以的话,这里最好是可以消除

最后一步就是为其设置css属性即可整个逻辑很简单,大概原理就是这样,我接下来来看看zepto高大上的实现!!!

zepto高大上的animate

zepto要实现以上代码的话,这样搞:

复制代码

var demo = $('#demo');var msg = $('#msg');var fn = function () {  msg.html('事件回调,当前原始宽度:' + demo.width());};demo.on('mouseenter', function () {  demo.animate({ 'width': '300px' }, 1000, 'ease-out', fn);});demo.on('mouseout', function () {  demo.animate({ 'width': '100px' }, 2000, 'ease-out', fn);});

复制代码

然后我们现在来看看源码:

;(function($, undefined){  var prefix = '', eventPrefix, endEventName, endAnimationName,    vendors = { Webkit: 'webkit', Moz: '', O: 'o' },    document = window.document, testEl = document.createElement('div'),    supportedTransforms = /^((translate|rotate|scale)(X|Y|Z|3d)?|matrix(3d)?|perspective|skew(X|Y)?)$/i,    transform,    transitionProperty, transitionDuration, transitionTiming, transitionDelay,    animationName, animationDuration, animationTiming, animationDelay,    cssReset = {}  function dasherize(str) { return str.replace(/([a-z])([A-Z])/, '$1-$2').toLowerCase() }  function normalizeEvent(name) { return eventPrefix ? eventPrefix + name : name.toLowerCase() }  $.each(vendors, function(vendor, event){    if (testEl.style[vendor + 'TransitionProperty'] !== undefined) {      prefix = '-' + vendor.toLowerCase() + '-'      eventPrefix = event      return false    }  })  transform = prefix + 'transform'  cssReset[transitionProperty = prefix + 'transition-property'] =  cssReset[transitionDuration = prefix + 'transition-duration'] =  cssReset[transitionDelay    = prefix + 'transition-delay'] =  cssReset[transitionTiming   = prefix + 'transition-timing-function'] =  cssReset[animationName      = prefix + 'animation-name'] =  cssReset[animationDuration  = prefix + 'animation-duration'] =  cssReset[animationDelay     = prefix + 'animation-delay'] =  cssReset[animationTiming    = prefix + 'animation-timing-function'] = ''  $.fx = {    off: (eventPrefix === undefined && testEl.style.transitionProperty === undefined),    speeds: { _default: 400, fast: 200, slow: 600 },    cssPrefix: prefix,    transitionEnd: normalizeEvent('TransitionEnd'),    animationEnd: normalizeEvent('AnimationEnd')  }  $.fn.animate = function(properties, duration, ease, callback, delay){    if ($.isFunction(duration))      callback = duration, ease = undefined, duration = undefined    if ($.isFunction(ease))      callback = ease, ease = undefined    if ($.isPlainObject(duration))      ease = duration.easing, callback = duration.complete, delay = duration.delay, duration = duration.duration    if (duration) duration = (typeof duration == 'number' ? duration :                    ($.fx.speeds[duration] || $.fx.speeds._default)) / 1000    if (delay) delay = parseFloat(delay) / 1000    return this.anim(properties, duration, ease, callback, delay)  }  $.fn.anim = function(properties, duration, ease, callback, delay){    var key, cssValues = {}, cssProperties, transforms = '',        that = this, wrappedCallback, endEvent = $.fx.transitionEnd,        fired = false    if (duration === undefined) duration = $.fx.speeds._default / 1000    if (delay === undefined) delay = 0    if ($.fx.off) duration = 0    if (typeof properties == 'string') {      // keyframe animation      cssValues[animationName] = properties      cssValues[animationDuration] = duration + 's'      cssValues[animationDelay] = delay + 's'      cssValues[animationTiming] = (ease || 'linear')      endEvent = $.fx.animationEnd    } else {      cssProperties = []      // CSS transitions      for (key in properties)        if (supportedTransforms.test(key)) transforms += key + '(' + properties[key] + ') '        else cssValues[key] = properties[key], cssProperties.push(dasherize(key))      if (transforms) cssValues[transform] = transforms, cssProperties.push(transform)      if (duration > 0 && typeof properties === 'object') {        cssValues[transitionProperty] = cssProperties.join(', ')        cssValues[transitionDuration] = duration + 's'        cssValues[transitionDelay] = delay + 's'        cssValues[transitionTiming] = (ease || 'linear')      }    }    wrappedCallback = function(event){      if (typeof event !== 'undefined') {        if (event.target !== event.currentTarget) return // makes sure the event didn't bubble from "below"        $(event.target).unbind(endEvent, wrappedCallback)      } else        $(this).unbind(endEvent, wrappedCallback) // triggered by setTimeout      fired = true      $(this).css(cssReset)      callback && callback.call(this)    }    if (duration > 0){      this.bind(endEvent, wrappedCallback)      // transitionEnd is not always firing on older Android phones      // so make sure it gets fired      setTimeout(function(){        if (fired) return        wrappedCallback.call(that)      }, (duration * 1000) + 25)    }    // trigger page reflow so new elements can animate    this.size() && this.get(0).clientLeft    this.css(cssValues)    if (duration <= 0) setTimeout(function() {      that.each(function(){ wrappedCallback.call(this) })    }, 0)    return this  }  testEl = null})(Zepto)

View Code

看代码首先还是看入口,我们这里的入口就是animate

demo.animate({ 'width': '300px' }, 1000, 'ease-out', fn);

复制代码

 1 $.fn.animate = function(properties, duration, ease, callback, delay){ 2   if ($.isFunction(duration)) 3     callback = duration, ease = undefined, duration = undefined 4   if ($.isFunction(ease)) 5     callback = ease, ease = undefined 6   if ($.isPlainObject(duration)) 7     ease = duration.easing, callback = duration.complete, delay = duration.delay, duration = duration.duration 8   if (duration) duration = (typeof duration == 'number' ? duration : 9                   ($.fx.speeds[duration] || $.fx.speeds._default)) / 100010   if (delay) delay = parseFloat(delay) / 100011   return this.anim(properties, duration, ease, callback, delay)12 }

复制代码

他首先这里做了一些默认处理,因为我们传递的参数是不定的,所以第二个参数极有可能是回调

所以他第一句就是做一个简单的判断,第二句也不例外

其实他整个animate都是做一些属性处理,并未做实际的事情,具体的实现还是在anim中

复制代码

 1 $.fn.anim = function(properties, duration, ease, callback, delay){ 2   var key, cssValues = {}, cssProperties, transforms = '', 3       that = this, wrappedCallback, endEvent = $.fx.transitionEnd, 4       fired = false 5  6   if (duration === undefined) duration = $.fx.speeds._default / 1000 7   if (delay === undefined) delay = 0 8   if ($.fx.off) duration = 0 9 10   if (typeof properties == 'string') {11     // keyframe animation12     cssValues[animationName] = properties13     cssValues[animationDuration] = duration + 's'14     cssValues[animationDelay] = delay + 's'15     cssValues[animationTiming] = (ease || 'linear')16     endEvent = $.fx.animationEnd17   } else {18     cssProperties = []19     // CSS transitions20     for (key in properties)21       if (supportedTransforms.test(key)) transforms += key + '(' + properties[key] + ') '22       else cssValues[key] = properties[key], cssProperties.push(dasherize(key))23 24     if (transforms) cssValues[transform] = transforms, cssProperties.push(transform)25     if (duration > 0 && typeof properties === 'object') {26       cssValues[transitionProperty] = cssProperties.join(', ')27       cssValues[transitionDuration] = duration + 's'28       cssValues[transitionDelay] = delay + 's'29       cssValues[transitionTiming] = (ease || 'linear')30     }31   }32 33   wrappedCallback = function(event){34     if (typeof event !== 'undefined') {35       if (event.target !== event.currentTarget) return // makes sure the event didn't bubble from "below"36       $(event.target).unbind(endEvent, wrappedCallback)37     } else38       $(this).unbind(endEvent, wrappedCallback) // triggered by setTimeout39 40     fired = true41     $(this).css(cssReset)42     callback && callback.call(this)43   }44   if (duration > 0){45     this.bind(endEvent, wrappedCallback)46     // transitionEnd is not always firing on older Android phones47     // so make sure it gets fired48     setTimeout(function(){49       if (fired) return50       wrappedCallback.call(that)51     }, (duration * 1000) + 25)52   }53 54   // trigger page reflow so new elements can animate55   this.size() && this.get(0).clientLeft56 57   this.css(cssValues)58 59   if (duration <= 0) setTimeout(function() {60     that.each(function(){ wrappedCallback.call(this) })61   }, 0)62 63   return this64 }

复制代码

传入anim的参数真的就没有什么问题了

第一个是css属性

第二个是动画运行时间

第三个是动画曲线,这个很神奇,没事不要去搞他

第四个是回调函数

第五个是什么就暂时不知道是什么了

进入后,10行之前还是在做容错性处理,这里我们最主要关注点放在endEvent上面

这个东西由前面的fx对象获取:

复制代码

$.fx = {  off: (eventPrefix === undefined && testEl.style.transitionProperty === undefined),  speeds: { _default: 400, fast: 200, slow: 600 },  cssPrefix: prefix,  transitionEnd: normalizeEvent('TransitionEnd'),  animationEnd: normalizeEvent('AnimationEnd')}

复制代码

而我们要做的chrome、firefox等兼容全部被normalizeEvent做了,这里

复制代码

vendors = { Webkit: 'webkit', Moz: '', O: 'o' }
testEl = document.createElement('div')$.each(vendors, function(vendor, event){  if (testEl.style[vendor + 'TransitionProperty'] !== undefined) {    prefix = '-' + vendor.toLowerCase() + '-'    eventPrefix = event    return false  }})

复制代码

这里根据这种方式得出了兼容事件的前缀,webkit的话会返回webkit前缀:

$.fx.transitionEnd => "webkitTransitionEnd"

然后下一步简单仍然是先设置transition相关的属性,并且指定事件结束事件回调:

cssValues[animationName] = propertiescssValues[animationDuration] = duration + 's'cssValues[animationDelay] = delay + 's'cssValues[animationTiming] = (ease || 'linear')endEvent = $.fx.animationEnd

当然,如果我们传入的CSS不止一个的话,下面的处理会相对复杂点

复制代码

cssProperties = []// CSS transitionsfor (key in properties)  if (supportedTransforms.test(key)) transforms += key + '(' + properties[key] + ') '  else cssValues[key] = properties[key], cssProperties.push(dasherize(key))if (transforms) cssValues[transform] = transforms, cssProperties.push(transform)if (duration > 0 && typeof properties === 'object') {  cssValues[transitionProperty] = cssProperties.join(', ')  cssValues[transitionDuration] = duration + 's'  cssValues[transitionDelay] = delay + 's'  cssValues[transitionTiming] = (ease || 'linear')}

复制代码

这里先放下处理Transform等新属性之外,与上面的操作无他

然后关键步骤又来了,

复制代码

 1 wrappedCallback = function(event){ 2   if (typeof event !== 'undefined') { 3     if (event.target !== event.currentTarget) return // makes sure the event didn't bubble from "below" 4     $(event.target).unbind(endEvent, wrappedCallback) 5   } else 6     $(this).unbind(endEvent, wrappedCallback) // triggered by setTimeout 7  8   fired = true 9   $(this).css(cssReset)10   callback && callback.call(this)11 }12 if (duration > 0){13   this.bind(endEvent, wrappedCallback)14   // transitionEnd is not always firing on older Android phones15   // so make sure it gets fired16   setTimeout(function(){17     if (fired) return18     wrappedCallback.call(that)19   }, (duration * 1000) + 25)20 }

复制代码

他这里首先声明了回调函数wrappedCallback,这个函数首先干的事情是注销事件

然后执行传入的回调,这里将this指向了调用者,也就是绑定的标签

后面便是真实的事件绑定操作,里面仍然有一个延时函数执行

其中有一个状态机fired,来记录该事件是否触发

然后就为css复制了,这个时候动画执行结束便会触发transitionEnd事件了

最后,代码结束.......

结语

今天,我们简单的说了下zepto的animate方法,希望对各位有帮助,若是文中有任何问题请提出

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
移动开发工程师
手记
粉丝
18
获赞与收藏
134

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消