javascript运动系列第一篇——匀速运动

前面的话

  除了拖拽之外,运动也是javascript动画的一个基本操做。经过CSS属性transitionanimation能够实现运动。可是,要进行更精细地操做,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更为合适,定时器每运行一次,该元素前进一步

 

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()时,仅当没有该定时器的任何其余代码实例时,才将定时器代码添加到队列中。这确保了定时器代码加入到队列中的最小时间间隔为指定间隔

  可是,这样会致使两个问题:一、某些间隔被跳过;二、多个定时器的代码执行之间的间隔可能比预期的小

  为了不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的形式  

  可是,当出现多个定时器时,此问题仍然很差解决。更好的办法是使用时间版运动

相关文章
相关标签/搜索