除了拖拽之外,運動也是javascript動畫的一個基本操做。經過CSS屬性transition和animation能夠實現運動。可是,要進行更精細地操做,javascript運動是必不可少的。本文將詳細介紹javascript運動javascript
讓一個元素在頁面中運動起來很簡單,設置定時器,改變定位元素的left或top值便可html
<button id="btn">開始運動</button> <button id="reset">還原</button> <div id="test" style="height: 100px;width: 100px;background-color: pink;position:absolute;left:0;"></div> <script> var timer; reset.onclick = function(){history.go();} btn.onclick = function(){ timer = setInterval(function(){ if(test.offsetLeft < 500){ test.style.left = test.offsetLeft + 10 + 'px'; }else{ test.style.left = '500px'; clearInterval(timer); } },30); } </script>
上面的代碼中沒有進行定時器管理。當元素在運動的過程當中,屢次按下按鈕,會開啓多個定時器,從而使元素運動速度加快java
有兩種定時器管理方式瀏覽器
【1】開啓新定時器前,消除舊定時器框架
[注意]即便沒有定時器的狀況下,消除定時器也不會報錯,只是靜默失敗函數
<button id="btn">開始運動</button> <button id="reset">還原</button> <div id="test" style="height: 100px;width: 100px;background-color: pink;position:absolute;left:0;"></div> <script> var timer; reset.onclick = function(){history.go();} btn.onclick = function(){ clearInterval(timer); timer = setInterval(function(){ if(test.offsetLeft < 500){ test.style.left = test.offsetLeft + 10 + 'px'; }else{ test.style.left = '500px'; clearInterval(timer); } },30); } </script>
【2】當定時器未中止時,不容許開啓新定時器動畫
[注意]因爲定時器開啓時,其返回值是一個不爲0的整數,因此能夠經過判斷其返回值,來肯定是否使用return語句this
<button id="btn">開始運動</button> <button id="reset">還原</button> <div id="test" style="height: 100px;width: 100px;background-color: pink;position:absolute;left:0;"></div> <script> var timer; reset.onclick = function(){history.go();} btn.onclick = function(){ if(timer) return; timer = setInterval(function(){ if(test.offsetLeft < 500){ test.style.left = test.offsetLeft + 10 + 'px'; }else{ test.style.left = '500px'; clearInterval(timer); } },30); } </script>
如今要作一個相似於「分享到」側邊欄的效果spa
<style> #test{ width: 100px; height: 100px; background-color: lightblue; text-align:center; position:absolute; top: 0; left: -100px; } #test-in{ width: 30px; height: 60px; background-color: orange; margin-left: 100px; position:relative; top: 20px; } </style> <div id="test"> <div id="test-in">分享到</div> </div> <script> test.onmouseover = function(){test.style.left = '0px';} test.onmouseout = function(){test.style.left = '-100px';} </script>
若是把鼠標移入和鼠標移出都增長運動效果,則須要使用運動函數code
可是,有一個很重要的問題須要注意的是,鼠標移入移出的順序問題
若是把移入移出事件都加在父元素的身上,則須要作以下處理
因爲鼠標從子元素移動到父元素上時,會觸發子元素的移出事件,經過冒泡也會觸發父元素移出事件。此時,有兩種方法解決該問題。一種是在子元素移出事件中阻止冒泡,另外一種是在父元素移出事件設置target判斷條件。當target爲父元素自己時才執行
鼠標從父元素移動到子元素的過程當中,會按照順序觸發父元素的移出事件、子元素的移入事件以及父元素的移入事件
爲了不觸發移入事件。此時,使用開關變量對移入事件的代碼進行限制。移出事件代碼完成以前不執行移入事件代碼
<script> var testIn = document.getElementById('test-in'); var timer1,timer2; var onOff = false; test.onmouseover = function(){ if(!onOff){ clearInterval(timer1); timer1 = setInterval(function(){ if(!onOff){ if(test.offsetLeft < 0){ test.style.left = test.offsetLeft + 10 + 'px'; }else{ test.style.left = '0'; clearInterval(timer1); timer1 = 0; } }else{ clearInterval(timer1); } },30); } } test.onmouseout = function(e){ e = e || event; var target = e.target || e.srcElement; if(target === test){ //當觸發父元素移出事件時,開啓開關 onOff = true; clearInterval(timer2); timer2 = setInterval(function(){ if(test.offsetLeft > -100){ test.style.left = test.offsetLeft - 10 + 'px'; }else{ test.style.left = '-100px'; clearInterval(timer2); timer2 = 0; //當運動結束後,關閉開關 onOff = false; } },30); } } </script>
從上面的代碼中,能夠看出運動部分的重複代碼較多,把運動封裝爲帶參數的函數更合適
<style> #test{width: 100px;height: 100px;background-color:lightblue;text-align:center;position:absolute;top: 0;left: -100px;} #test-in{width: 30px;height: 60px;background-color: orange;margin-left: 100px;position:relative;top: 20px;} </style> <div id="test"> <div id="test-in">分享到</div> </div> <script> var testIn = document.getElementById('test-in'); var timer; test.onmouseover = function(){move(test,0,10);} test.onmouseout = function(){move(test,-100,-10)} function move(obj,target,speed){ clearInterval(timer); timer = setInterval(function(){ if((obj.offsetLeft - target)*speed < 0){ obj.style.left = obj.offsetLeft + speed + 'px'; }else{ obj.style.left = target + 'px'; clearInterval(timer); timer = 0; } },16); } </script>
因爲不只僅是left值能夠作運動,其餘屬性(如width)也能夠。因此,屬性attr也應該做爲參數提取出來
這時就沒法使用offset類屬性,而應該使用計算樣式的兼容函數getCSS()
function getCSS(obj,style){ if(window.getComputedStyle){ return getComputedStyle(obj)[style]; } return obj.currentStyle[style]; } function move(obj,attr,target,speed){ clearInterval(timer); timer = setInterval(function(){ var cur = parseInt(getCSS(obj,attr)); if((cur - target)*speed < 0){ obj.style.left = cur + speed + 'px'; }else{ obj.style.left = target + 'px'; clearInterval(timer); timer = 0; } },30); }
透明度是一個比較特殊的樣式,由於IE8-瀏覽器不支持opacity,只能經過濾鏡的方式寫成filter:alpha(opacity=透明值)
可是,因爲IE瀏覽器獲取計算樣式時,能夠得到自定義樣式,因此雖然opacity屬性在IE8-瀏覽器沒法生效,可是能夠得到它的值
若是透明度作運動的話,則須要對運動函數進行從新封裝
[注意]因爲透明度涉及小數計算,如0.07*100=> 7.000000000000001,因此須要用Math.round()去掉尾巴
<style> #test{width: 100px;height: 100px;background-color:lightblue;text-align:center;position:absolute;top: 0;left: 0;} #test-in{width: 30px;height: 60px;background-color: orange;margin-left: 100px;position:relative;top: 20px;} </style> <div id="test"> <div id="test-in">分享到</div> </div> <script> var testIn = document.getElementById('test-in'); var timer; test.onmouseover = function(){move(test,'opacity',0.1,-0.05);} test.onmouseout = function(){move(test,'opacity',1,0.05)} function getCSS(obj,style){ if(window.getComputedStyle){ return getComputedStyle(obj)[style]; } return obj.currentStyle[style]; } function move(obj,attr,target,speed){ clearInterval(timer); var cur; timer = setInterval(function(){ if(attr == 'opacity'){ cur = Math.round(getCSS(obj,attr)*100); if((cur - target*100)*speed < 0){ obj.style.opacity = (cur + speed*100)/100; obj.style.filter = 'alpha(opacity=' + (cur + speed*100) + ')'; }else{ obj.style.opacity = target; obj.filter = 'alpha(opacity=' + target + ')'; clearInterval(timer); timer = 0; } }else{ cur = parseInt(getCSS(obj,attr)); if((cur - target)*speed < 0){ obj.style[attr] = cur + speed + 'px'; }else{ obj.style[attr] = target + 'px'; clearInterval(timer); timer = 0; } } },30); } </script>
若是一個元素有多個值同時運動時,像下面這樣直接調用move()函數是有問題的
move(test,'opacity',0.1,-0.05);
move(test,'left',-100,-1);
由於函數裏面定時器的變量timer是一個公共變量,當一個運動中止時,會清除定時器。這時另外一個運動即便沒有完成,定時器已經中止了,就沒法繼續運動了
因此,合適的作法是在參數對象obj下面設置一個自定義屬性timers,timers爲一個空對象,而後將定時器返回值儲存在timers對象下的attr屬性中,此時兩個定時器不會相互干擾
<style> #test{width: 100px;height: 100px;background-color: lightblue;text-align:center;position:absolute;top: 0;left: -100px;opacity:1;} #test-in{width: 30px;height: 60px;background-color: orange;margin-left: 100px;position:relative;top: 20px;} </style> <div id="test"> <div id="test-in">分享到</div> </div> <script> test.onmouseover = function(){ move(test,'opacity',0.1,-0.05); move(test,'left',0,10); } test.onmouseout = function(){ move(test,'opacity',1,0.05); move(test,'left',-100,-10); } function getCSS(obj,style){ if(window.getComputedStyle){ return getComputedStyle(obj)[style]; } return obj.currentStyle[style]; } function move(obj,attr,target,speed){ if(!obj.timers){ obj.timers = {}; } clearInterval(obj.timers[attr]); var cur; obj.timers[attr] = setInterval(function(){ if(attr == 'opacity'){ cur = Math.round(getCSS(obj,attr)*100); if((cur - target*100)*speed < 0){ obj.style.opacity = (cur + speed*100)/100; obj.style.filter = 'alpha(opacity=' + (cur + speed*100) + ')'; }else{ obj.style.opacity = target; obj.filter = 'alpha(opacity=' + target + ')'; clearInterval(obj.timers[attr]); obj.timers[attr] = 0; } }else{ cur = parseInt(getCSS(obj,attr)); if((cur - target)*speed < 0){ obj.style[attr] = cur + speed + 'px'; }else{ obj.style[attr] = target + 'px'; clearInterval(obj.timers[attr]); obj.timers[attr] = 0; } } },30); } </script>
若是在頁面中有多個元素利用運動函數進行運動。因爲定時器返回值在不一樣元素不一樣屬性中都不會受影響。因此,上面的運動函數能夠直接使用
<style> div{height: 100px;width: 100px;position: absolute;left: 0;} #test1{background-color: pink;top: 40px;} #test2{background-color: lightblue;top: 150px;} </style> <div id="test1">元素一</div> <div id="test2">元素二</div> <button id="btn">開始運動</button> <button id="reset">還原</button> <script> reset.onclick = function(){history.go();} btn.onclick = function(){ move(test1,'width',300,10); move(test1,'left',100,10); move(test2,'width',500,20); move(test2,'left',200,10); } function getCSS(obj,style){ if(window.getComputedStyle){ return getComputedStyle(obj)[style]; } return obj.currentStyle[style]; } function move(obj,attr,target,speed){ if(!obj.timers){ obj.timers = {}; } clearInterval(obj.timers[attr]); var cur; obj.timers[attr] = setInterval(function(){ if(attr == 'opacity'){ cur = Math.round(getCSS(obj,attr)*100); if((cur - target*100)*speed < 0){ obj.style.opacity = (cur + speed*100)/100; obj.style.filter = 'alpha(opacity=' + (cur + speed*100) + ')'; }else{ obj.style.opacity = target; obj.filter = 'alpha(opacity=' + target + ')'; clearInterval(obj.timers[attr]); obj.timers[attr] = 0; } }else{ cur = parseInt(getCSS(obj,attr)); if((cur - target)*speed < 0){ obj.style[attr] = cur + speed + 'px'; }else{ obj.style[attr] = target + 'px'; clearInterval(obj.timers[attr]); obj.timers[attr] = 0; } } },30); } </script>
物體的多個屬性可能不是同時運動,多是一個屬性運動完成以後,另外一個屬性再運動。若是要完成這種需求,就須要用到回調函數
在運動函數中,定時器中止時,再調用運動函數,就能夠接續運動效果
<style> div{height: 100px;width: 100px;position: absolute;left: 0;} #test{background-color: pink;top: 40px;} </style> <div id="test">元素</div> <button id="btn">開始運動</button> <button id="reset">還原</button> <script> reset.onclick = function(){history.go();} btn.onclick = function(){ move(test,'left',100,20,function(){ move(test,'width',300,10) }); } function getCSS(obj,style){ if(window.getComputedStyle){ return getComputedStyle(obj)[style]; } return obj.currentStyle[style]; } function move(obj,attr,target,speed,fn){ if(!obj.timers){obj.timers = {};} clearInterval(obj.timers[attr]); var cur; obj.timers[attr] = setInterval(function(){ if(attr == 'opacity'){ cur = Math.round(getCSS(obj,attr)*100); if((cur - target*100)*speed < 0){ obj.style.opacity = (cur + speed*100)/100; obj.style.filter = 'alpha(opacity=' + (cur + speed*100) + ')'; }else{ obj.style.opacity = target; obj.filter = 'alpha(opacity=' + target + ')'; clearInterval(obj.timers[attr]); obj.timers[attr] = 0; fn && fn.call(obj); } }else{ cur = parseInt(getCSS(obj,attr)); if((cur - target)*speed < 0){ obj.style[attr] = cur + speed + 'px'; }else{ obj.style[attr] = target + 'px'; clearInterval(obj.timers[attr]); obj.timers[attr] = 0; fn && fn.call(obj); } } },30); } </script>
【速度參數】
上面封裝的函數中,傳遞速度參數時,須要在速度參數前添加正負號做爲方向標識。實際上,這步能夠寫在函數的程序內,而只傳遞正的速度參數便可
speed = parseInt(getCSS(obj,attr)) < target ? speed : -speed;
【拉回操做】
還有一個能夠升級的地方,就是拉回操做。經過判斷元素是否到達目標點,若是超過目標點後,將元素拉回到目標點位置
cur = parseInt(getCSS(obj,attr)); if((cur - target)*speed < 0){ obj.style[attr] = cur + speed + 'px'; }else{ obj.style[attr] = target + 'px'; clearInterval(obj.timers[attr]); obj.timers[attr] = 0; fn && fn.call(obj); }
更合理的操做,應該是元素確定不能超過目標點
因此應該把判斷條件用來處理speed,當speed是一個合適的值時,再賦值給obj.style[attr],可更改以下
cur = parseInt(getCSS(obj,attr)); //若速度設置值使得元素超過目標點時,將速度設置值更改成目標點值 - 當前值 if((cur +speed - target)*speed > 0){ speed = target - cur; } //將合適的speed值賦值給元素的樣式 obj.style[attr] = cur + speed + 'px'; //當元素到達目標點後,中止定時器 if(speed == target - cur){ clearInterval(obj.timers[attr]); obj.timers[attr] = 0; fn && fn.call(obj); }
【使用步長】
其實,把元素的位移變化命名爲速度並不合適,只是由於約定俗成的關係才如此起名,將其命名爲步長step更爲合適,定時器每運行一次,該元素前進一步
以move.js的名字對該運動函數進行保存,在線地址
function getCSS(obj,style){ if(window.getComputedStyle){ return getComputedStyle(obj)[style]; } return obj.currentStyle[style]; } function move(obj,attr,target,step,fn){ //若是沒有創建定時器對象,則在obj下創建定時器對象 if(!obj.timers){obj.timers = {};} //清除定時器 clearInterval(obj.timers[attr]); //聲明當前值變量cur var cur; //判斷步長step的正負值 step = parseInt(getCSS(obj,attr)) < target ? step : -step; //開啓定時器 obj.timers[attr] = setInterval(function(){ //若是樣式是透明度 if(attr == 'opacity'){ //對當前值的取值進行四捨五入,去除因爲javascript小數計數中的bug存在的小尾巴 cur = Math.round(getCSS(obj,attr)*100); if((cur - target*100)*step < 0){ //設置透明度 obj.style.opacity = (cur + step*100)/100; //IE兼容 obj.style.filter = 'alpha(opacity=' + (cur + step*100) + ')'; //透明度到達指定目標時 }else{ obj.style.opacity = target; obj.filter = 'alpha(opacity=' + target + ')'; //清除定時器 clearInterval(obj.timers[attr]); obj.timers[attr] = 0; //設置回調函數 fn && fn.call(obj); } //當樣式不是透明度時 }else{ //獲取樣式當前值並賦值給cur cur = parseFloat(getCSS(obj,attr)); ////若步長設置值使得元素超過目標點時,將步長設置值更改成目標點值 - 當前值 if((cur + step - target)*step > 0){ step = target - cur; } //將合適的步長值賦值給元素的樣式 obj.style[attr] = cur + step + 'px'; //當元素到達目標點後,中止定時器 if(step == target - cur){ clearInterval(obj.timers[attr]); obj.timers[attr] = 0; fn && fn.call(obj); } } },30); }
【實例】
下面以一個實例來講明move函數的應用,點擊document便可查看效果
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <style> div{ width: 50px; height: 50px; position: absolute; top: 0; background-color:lightblue; } div:nth-child(odd){ background-color:pink; } </style> </head> <body> <script src="http://files.cnblogs.com/files/xiaohuochai/move.js"></script> <script> var str = ''; var len = 10; var timer; var num = 0; for(var i = 0; i < len; i++){ str+= '<div style="left:'+60*i+'px;"></div>'; } document.body.innerHTML = str; document.onclick = function(){ var aDiv = document.getElementsByTagName('div'); if(timer) return; timer = setInterval(function(){ move(aDiv[num++],'top', 200,10,function(){ var _this = this; setTimeout(function(){ move(_this,'top', 0,10); },1000) }); if(num == len){ clearInterval(timer); num = 0; setTimeout(function(){ timer = 0; },2000); } },100); } </script> </body> </html>
使用setInterval()的問題在於,定時器代碼可能在代碼再次被添加到隊列以前尚未完成執行,結果致使定時器代碼連續運行好幾回,而之間沒有任何停頓。而JS引擎對這個問題的解決是:當使用setInterval()時,僅當沒有該定時器的任何其餘代碼實例時,纔將定時器代碼添加到隊列中。這確保了定時器代碼加入到隊列中的最小時間間隔爲指定間隔
可是,這樣會致使兩個問題:一、某些間隔被跳過;二、多個定時器的代碼執行之間的間隔可能比預期的小
爲了不setInterval()定時器的問題,可使用鏈式requestAnimationFrame()調用,IE9-瀏覽器可使用setTimeout()兼容
以frameMove.js的名稱保存該js文件
if (!window.requestAnimationFrame) { requestAnimationFrame = function(fn) { setTimeout(fn, 17); }; } if (!window.cancelAnimationFrame) { window.cancelAnimationFrame = function(id) { clearTimeout(id); }; } function getCSS(obj,style){ if(window.getComputedStyle){ return getComputedStyle(obj)[style]; } return obj.currentStyle[style]; } function move(obj,attr,target,step,fn){ //若是沒有創建定時器對象,則在obj下創建定時器對象 if(!obj.timers){ obj.timers = {}; } //清除定時器 cancelAnimationFrame(obj.timers[attr]); //聲明當前值變量cur var cur; //判斷步長step的正負值 step = parseInt(getCSS(obj,attr)) < target ? step : -step; //開啓定時器 obj.timers[attr] = requestAnimationFrame(function func(){ //若是樣式是透明度 if(attr == 'opacity'){ //對當前值的取值進行四捨五入,去除因爲javascript小數計數中的bug存在的小尾巴 cur = Math.round(getCSS(obj,attr)*100); if((cur - target*100)*step < 0){ //設置透明度 obj.style.opacity = (cur + step*100)/100; //IE兼容 obj.style.filter = 'alpha(opacity=' + (cur + step*100) + ')'; //遞歸調用定時器 obj.timers[attr] = requestAnimationFrame(func); //透明度到達指定目標時 }else{ obj.style.opacity = target; obj.filter = 'alpha(opacity=' + target + ')'; //清除定時器 cancelAnimationFrame(obj.timers[attr]); obj.timers[attr] = 0; //設置回調函數 fn && fn.call(obj); } //當樣式不是透明度時 }else{ //獲取樣式當前值並賦值給cur cur = parseInt(getCSS(obj,attr)); //若步長設置值使得元素超過目標點時,將步長設置值更改成目標點值 - 當前值 if((cur + step - target)*step > 0){ step = target - cur; } //將合適的步長值賦值給元素的樣式 obj.style[attr] = cur + step + 'px'; //遞歸調用定時器 obj.timers[attr] = requestAnimationFrame(func); //當元素到達目標點後,中止定時器 if(step == target - cur){ cancelAnimationFrame(obj.timers[attr]); obj.timers[attr] = 0; fn && fn.call(obj); } } }); }
不管是Interval版本的運動函數,仍是requestAnimationFrame版本的運動函數,語法都沒有問題。但瀏覽器卻有問題。爲了節電,對於那些不處於當前窗口的頁面,瀏覽器會將時間間隔擴大到1000毫秒。另外,若是筆記本電腦處於電池供電狀態,Chrome和IE10+瀏覽器,會將時間間隔切換到系統定時器,大約是16.6毫秒
定時器時間間隔的變化,獲得運動不能按照預期進行,不少時間會出現預想不到的bug
好比,仍是上面的例子,它是以iframe內聯框架的形式引入頁面的。若是元素在運動過程當中,拖動滾動條,使可視區域展現其餘內容。過幾秒鐘後,再移回來時,發現運動的元素已經出現了bug
關於以上狀況的解決辦法是,只要頁面不處於活動狀態,定時器就中止運行,回到活動狀態時,再恢復運行。可使用window的onblur和onfocus事件來解決
window.onblur = function(){ //清除定時器 cancelAnimationFrame(timer); } window.onfocus = function(){ //開啓定時器 timer = requestAnimationFrame(func) }
[注意]只能使用window.onblur的形式,而不能使用window.addEventListener的形式
可是,當出現多個定時器時,此問題仍然很差解決。更好的辦法是使用時間版運動