文章出處

前面的話

  除了拖拽以外,運動也是javascript動畫的一個基本操作。通過CSS屬性transitionanimation可以實現運動。但是,要進行更精細地操作,javascript運動是必不可少的。本文將詳細介紹javascript運動

 

簡單運動

  讓一個元素在頁面中運動起來很簡單,設置定時器,改變定位元素的left或top值即可

<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>

定時器管理

  上面的代碼中沒有進行定時器管理。當元素在運動的過程中,多次按下按鈕,會開啟多個定時器,從而使元素運動速度加快

  有兩種定時器管理方式

【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語句

<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>

分享效果

  現在要做一個類似于“分享到”側邊欄的效果

<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>

移入移出

  如果把鼠標移入和鼠標移出都增加運動效果,則需要使用運動函數

  但是,有一個很重要的問題需要注意的是,鼠標移入移出的順序問題

  如果把移入移出事件都加在父元素的身上,則需要做如下處理

  由于鼠標從子元素移動到父元素上時,會觸發子元素的移出事件,通過冒泡也會觸發父元素移出事件。此時,有兩種方法解決該問題。一種是在子元素移出事件中阻止冒泡,另一種是在父元素移出事件設置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更為合適,定時器每運行一次,該元素前進一步

 

Interval函數

  以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>

 

Frame函數

  使用setInterval()的問題在于,定時器代碼可能在代碼再次被添加到隊列之前還沒有完成執行,結果導致定時器代碼連續運行好幾次,而之間沒有任何停頓。而JS引擎對這個問題的解決是:當使用setInterval()時,僅當沒有該定時器的任何其他代碼實例時,才將定時器代碼添加到隊列中。這確保了定時器代碼加入到隊列中的最小時間間隔為指定間隔

  但是,這樣會導致兩個問題:1、某些間隔被跳過;2、多個定時器的代碼執行之間的間隔可能比預期的小

  為了避免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的形式  

  但是,當出現多個定時器時,此問題仍然不好解決。更好的辦法是使用時間版運動

 


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()