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


    前言

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

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

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

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

    当时的心声

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

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

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

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

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

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

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

    animate
    animate(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):指定动画的缓动类型,使用以下一个:
    ease
    linear
    ease-in / ease-out
    ease-in-out
    cubic-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, {  '300px' }, 1, fn);
    });
    
    demo.addEventListener('mouseout', function () {
      animate(demo, {  '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)) / 1000
    10   if (delay) delay = parseFloat(delay) / 1000
    11   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 animation
    12     cssValues[animationName] = properties
    13     cssValues[animationDuration] = duration + 's'
    14     cssValues[animationDelay] = delay + 's'
    15     cssValues[animationTiming] = (ease || 'linear')
    16     endEvent = $.fx.animationEnd
    17   } else {
    18     cssProperties = []
    19     // CSS transitions
    20     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     } else
    38       $(this).unbind(endEvent, wrappedCallback) // triggered by setTimeout
    39 
    40     fired = true
    41     $(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 phones
    47     // so make sure it gets fired
    48     setTimeout(function(){
    49       if (fired) return
    50       wrappedCallback.call(that)
    51     }, (duration * 1000) + 25)
    52   }
    53 
    54   // trigger page reflow so new elements can animate
    55   this.size() && this.get(0).clientLeft
    56 
    57   this.css(cssValues)
    58 
    59   if (duration <= 0) setTimeout(function() {
    60     that.each(function(){ wrappedCallback.call(this) })
    61   }, 0)
    62 
    63   return this
    64 }

    传入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] = properties
    cssValues[animationDuration] = duration + 's'
    cssValues[animationDelay] = delay + 's'
    cssValues[animationTiming] = (ease || 'linear')
    endEvent = $.fx.animationEnd

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

    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')
    }

    这里先放下处理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 phones
    15   // so make sure it gets fired
    16   setTimeout(function(){
    17     if (fired) return
    18     wrappedCallback.call(that)
    19   }, (duration * 1000) + 25)
    20 }

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

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

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

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

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

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

    结语

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

  • 相关阅读:
    堆(优先队列)模板
    线段树(递归)模板
    快速幂(含二阶方阵类)模板
    ACM/CF赛制getstart模板
    [Andrew Stankevich's Contest#21] Lempel-Ziv Compression
    [NOIP2001]Car的旅行路线
    [NOIP2007] 矩阵取数游戏
    [NOIP2010] 关押罪犯
    [NOIP1999] 拦截导弹
    设计模式之第1章-工厂方法模式(Java实现)
  • 原文地址:https://www.cnblogs.com/yexiaochai/p/3602303.html
Copyright © 2020-2023  润新知