前言
在很久之前,我們專案有一個動畫功能,功能本身很簡單,便是典型的右進左出,並且帶動畫功能
以當時來說,雖然很簡單,但是受限於框架本身的難度,就直接使用了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, { 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)
看程式碼首先還是看入口,我們這裡的入口就是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方法,希望對各位有幫助,若是文中有任何問題請提出