匀速运动框架
首先从最基本的运动框架开始讲起
function startMove() { var oDiv=document.getElementById('div1'); setInterval(function (){ oDiv.style.left=oDiv.offsetLeft+10+'px'; }, 30); }
让一个div不断往右移动,每隔30毫秒移动10个像素。
但是这个div没有一个目的地,我们要让它在目的地停止运动,即在offsetLeft达到目标位置(target)的时候关闭定时器。
var timer=null; function startMove() { var oDiv=document.getElementById('div1'); timer=setInterval(function (){ if(oDiv.offsetLeft==300) { clearInterval(timer); } oDiv.style.left=oDiv.offsetLeft+10+'px'; }, 30); }
我还想要控制这个div移动的速度,我们可以自己来规定速度。
但是自己规定速度会出现一个问题,如果我们把速度设置成7,而目的地设为300,那么div的offsetLeft永远不会等于300,所以条件要变成大于等于300。
var timer=null; function startMove() { var oDiv=document.getElementById('div1'); timer=setInterval(function (){ var speed=7; if(oDiv.offsetLeft>=300) { clearInterval(timer); } oDiv.style.left=oDiv.offsetLeft+speed+'px'; }, 30); }
另外我们还要把移动的语句写在else分支里,避免让button被点击执行startMove()方法的时候,哪怕div的offsetLeft已经大于等于target都会执行一次。
这是一个很重要的原则,即,把运动和停止隔开(if/else)。
var timer=null; function startMove() { var oDiv=document.getElementById('div1'); timer=setInterval(function (){ var speed=10; if(oDiv.offsetLeft>=300) { clearInterval(timer); } else { oDiv.style.left=oDiv.offsetLeft+speed+'px'; } }, 30); }
另外如果我们不断点击button让startMove()被执行,这个时候会有多个定时器被开,导致div的运动不断加速。
所以,我们要保证只能开启一个定时器,所以我们要在开始运动时关闭已有定时器。
var timer=null; function startMove() { var oDiv=document.getElementById('div1'); clearInterval(timer); timer=setInterval(function (){ var speed=1; if(oDiv.offsetLeft>=300) { clearInterval(timer); } else { oDiv.style.left=oDiv.offsetLeft+speed+'px'; } }, 30); }
我们为了方便在应用运动框架的时候调用更简单,就要把运动方法抽象的更彻底一些。
我们通过给speed单独赋值来控制速度,现在我们把目的地target设置成调用时候的参数,这样运动方法的灵活性就会更强。
var timer=null; function startMove(iTarget) { var oDiv=document.getElementById('div1'); clearInterval(timer); timer=setInterval(function (){ var speed=0; if(oDiv.offsetLeft>iTarget) { speed=-10; } else { speed=10; } if(oDiv.offsetLeft==iTarget) { clearInterval(timer); } else { oDiv.style.left=oDiv.offsetLeft+speed+'px'; } }, 30); }
但是问题又来了,如果我把speed改成7或者-7,target为100或者300,这个时候offsetLeft总是会大于或者小于target,出现的结果就是div在target附近左右晃动,这就涉及到匀速运动的停止条件。
停止条件就是我们把div的offsetLeft强制性地设置成target。
var timer=null; function startMove(iTarget) { var oDiv=document.getElementById('div1'); clearInterval(timer); timer=setInterval(function (){ var speed=0; if(oDiv.offsetLeft<iTarget) { speed=7; } else { speed=-7; } if(Math.abs(iTarget-oDiv.offsetLeft)<=7) { clearInterval(timer); oDiv.style.left=iTarget+'px'; } else { oDiv.style.left=oDiv.offsetLeft+speed+'px'; } }, 30); }
匀速运动框架之透明度运动
我们可以改变div的width和height或者其他种种,但是div有一个属性比较特殊,那就是透明度opacity。
更烦的一点是,opacity存在浏览器兼容问题:
在IE下,opacity的css要写成
{filter:alpha(opacity:30);}
在chrome下,opacity的css要写成
{opacity:0.3;}
IE下是百分比形式,chrome下是浮点数形式,数值都会有差异。
最关键的一点是,javascript并不存在获取透明度的原生写法,所以我们只能用变量提前存储好我们设置好的opacity来进行代替,然后在运动的时候设置div的opacity。
var alpha=30; var timer=null; function startMove(iTarget) { var oDiv=document.getElementById('div1'); clearInterval(timer); timer=setInterval(function (){ var speed=0; if(alpha<iTarget) { speed=10; } else { speed=-10; } if(alpha==iTarget) { clearInterval(timer); } else { alpha+=speed; oDiv.style.filter='alpha(opacity:'+alpha+')'; oDiv.style.opacity=alpha/100; } }, 30); }
缓冲运动框架
缓冲运动就是div离目标越近速度就越慢,速度由距离决定。
function startMove() { var oDiv=document.getElementById('div1'); setInterval(function (){ var speed=(300-oDiv.offsetLeft)/10; oDiv.style.left=oDiv.offsetLeft+speed+'px'; }, 30); }
但是这时候存在一个问题,当div的位置为291px,speed为浮点数比如0.9的时候,浏览器会把0.9读成0,这时候我们就要使用Math.ceil()来对速度进行向上取整。
或者,当div的位置为309px,speed为-0.9的时候,如果仍旧使用Math.ceil()取整,那么-0.9则为0,所以这个时候我们又要使用Math.floor()来对速度进行向下取整。
function startMove() { var oDiv=document.getElementById('div1'); setInterval(function (){ var speed=(300-oDiv.offsetLeft)/10; speed=speed>0?Math.ceil(speed):Math.floor(speed); oDiv.style.left=oDiv.offsetLeft+speed+'px'; document.title=oDiv.offsetLeft+','+speed; }, 30); }
多物体运动框架
在一个页面里经常会有多个物体同时运动,如果再按照我们之前的框架来解决这个问题,那就要写好多个startMove()或者在startMove()里对每一个物体都写一个定时器,这样太麻烦了。
常用的做法是把物体也抽象出来,抽象成object,把object当作参数传入进去,这样就可以解决多个物体同时运动的问题了。
这个框架的写法还是按照我们之前写的那个基础的匀速运动框架来写,唯一的区别就是object参数的传入。
var timer=null; function startMove(obj, iTarget) { clearInterval(timer); timer=setInterval(function (){ var speed=(iTarget-obj.offsetWidth)/6; speed=speed>0?Math.ceil(speed):Math.floor(speed); if(obj.offsetWidth==iTarget) { clearInterval(timer); } else { obj.style.width=obj.offsetWidth+speed+'px'; } }, 30); }
但是这样的方法并不对,因为timer只有一个,虽然我们把obj抽象出来并作为参数传入到方法里面,但是多个obj还是在共用一个timer。
obj是有属性的,所以我们把每个timer作为每个obj的属性去处理就解决了这个问题。
以前的timer=null这句话我们就不用了。
function startMove(obj, iTarget) { clearInterval(obj.timer); obj.timer=setInterval(function (){ var speed=(iTarget-obj.offsetWidth)/6; speed=speed>0?Math.ceil(speed):Math.floor(speed); if(obj.offsetWidth==iTarget) { clearInterval(obj.timer); } else { obj.style.width=obj.offsetWidth+speed+'px'; } }, 30); }
多物体运动框架之透明度运动
我们开头在讲匀速运动框架时曾经把透明度运动单独拉出来讨论,因为透明度具有它特有的特殊性,那么我们在写多物体运动框架时也要单独把透明度拉出来做特殊讨论。
我们知道,opacity是无法获取只能被设置的,我们之前的做法是用一个alhpa变量去保存我们预先在css里设置好的透明度,但是这样做会产生一个问题:现在有好几个div,它们是不能共用同一个alpha变量的。如果我们针对每一个要做透明度运动的div设置一个透明度变量,那一定很蠢。
我们在解决timer的时候说了,把timer作为obj的属性,同理,我们也可以把alpha作为obj的属性。
function startMove(obj, iTarget) { clearInterval(obj.timer); obj.timer=setInterval(function (){ var speed=(iTarget-obj.alpha)/6; speed=speed>0?Math.ceil(speed):Math.floor(speed); if(obj.alpha==iTarget) { clearInterval(obj.timer); } else { obj.alpha+=speed; obj.style.filter='alpha(opacity:'+obj.alpha+')'; obj.style.opacity=obj.alpha/100; } }, 30); }
现在,我们来总结一下这套多物体运动框架:
定时器作为物体的属性
物体、目标值作为方法的参数进行传递
所有东西都不能公用
属性与运动对象绑定:速度、其他属性值(如透明度等)
任意值运动框架
我们前面讨论了,在一个页面里有多个物体同时运动,但是我们讨论的情况是,多个物体同时变高、变宽、变透明,如果我们这个时候要多个物体里的一些物体变高,一些物体变宽,一些物体变透明,那可怎么办?
我们不可能针对变高写一个startMove1(),针对变宽写一个startMove2(),针对变透明写一个startMove3(),针对其他的属性写n个startMoveN(),这样太蠢。
function startMove(obj, iTarget) { clearInterval(obj.timer); obj.timer=setInterval(function (){ var speed=(iTarget-obj.offsetHeight)/6; speed=speed>0?Math.ceil(speed):Math.floor(speed); if(obj.offsetHeight==iTarget) { clearInterval(obj.timer); } else { obj.style.height=obj.offsetHeight+speed+'px'; } }, 30); } function startMove2(obj, iTarget) { clearInterval(obj.timer); obj.timer=setInterval(function (){ var speed=(iTarget-obj.offsetWidth)/6; speed=speed>0?Math.ceil(speed):Math.floor(speed); if(obj.offsetWidth==iTarget) { clearInterval(obj.timer); } else { obj.style.width=obj.offsetWidth+speed+'px'; } }, 30); }
但是如果我们如果把width和height属性作为参数传入,又会遇到一个问题,那就是offsetWidth和offsetHeight的Bug问题。
setInterval(function (){ var oDiv=document.getElementById('div1'); oDiv.style.width=oDiv.offsetWidth-1+'px'; }, 30);
我们写下上面的代码,并且给这个div1加上一些border和padding的样式
{width:200px; height:200px; background:red; border:1px solid black;}
你会发现,div会变得越来越大,原因是,offsetWidth的计算方式是把width和border和padding都算入进去,即
offsetWidth=width+border+padding
offsetHeight=width+border+padding
也就是说,在第一个30秒的时候,width=202-1+'px'='201px'
然后依此类推offsetWidth永远比width大1px,所以会导致它不断的变大。
解决的方法就是,我们不用offsetWidth和offsetHeight了。
我们需要直接取到width和height样式。
但是这又会遇到一个问题,如果是行间样式,我们可以很方便的取到
setInterval(function (){ var oDiv=document.getElementById('div1'); oDiv.style.width=parseInt(oDiv.style.width)-1+'px'; }, 30);
然而我们不可能一直用行间样式去写页面,我们终归还是要用css去控制页面,所以我们得想一个办法去获取css里的width和height。
function getStyle(obj, name) { if(obj.currentStyle) { return obj.currentStyle[name]; } else { return getComputedStyle(obj, false)[name]; } }
看到代码便可以知道,currentStyle属性和getComputedStyle()方法分别支持IE浏览器和Chrome浏览器。
现在我们可以在脚本里头这么写了
setInterval(function (){ var oDiv=document.getElementById('div1'); oDiv.style.width=parseInt(getStyle(oDiv, 'width'))-1+'px'; }, 30);
这是一个小插曲,主要目的是为了做好写任意值运动框架的准备。
既然我们发现了offsetWidth和offsetHeight的Bug,那我们就可以修改一下之前写的多物体运动框架,然而这并没有什么太大意义。我们不可能这样去把多物体运动框架重新描述一遍。
function startMove(obj, iTarget) { clearInterval(obj.timer); obj.timer=setInterval(function (){ var speed=(iTarget-getStyle(obj,'width'))/6; speed=speed>0?Math.ceil(speed):Math.floor(speed); if(getStyle(obj,'width')==iTarget) { clearInterval(obj.timer); } else { obj.style.width=getStyle(obj,'width')+speed+'px'; } }, 30); }
因为,既然我们引入了我们自己封装的getStyle()方法,就说明我们可以自由定制物体的属性了,就可以把物体的属性作为参数来进行传递了。
而且,任意值运动框架本来就比多物体运动框架要高级,多物体运动框架是任意值运动框架的更具体的子集。
我们要一直完善这个框架,直到最后它变成一款完美的运动框架。
现在我们去掉offset,把getStyle和任意值运动框架融合起来。
function getStyle(obj, name) { if(obj.currentStyle) { return obj.currentStyle[name]; } else { return getComputedStyle(obj, false)[name]; } } function startMove(obj, attr, iTarget) { clearInterval(obj.timer); obj.timer=setInterval(function (){ var cur=parseInt(getStyle(obj, attr)); var speed=(iTarget-cur)/6; speed=speed>0?Math.ceil(speed):Math.floor(speed); if(cur==iTarget) { clearInterval(obj.timer); } else { obj.style[attr]=cur+speed+'px'; } }, 30); }
现在我们的这套任意值可以在任何我们想得到的和想不到的属性上做运动!!!
除了透明度。。。
任意值运动框架之兼容透明度
你会发现毫无反映,因为我们通过getStyle()方法get出来的opacity是小数,比如说0.3,而实际上我们在设置opacity的时候使用的是百分制的数字,如90。
所以我们要对opacity进行乘以100的操作,并且不能再使用parseInt()方法,而应该使用parseFloat()方法来进行计算。
于是,如果我们设置的attr是opacity,那么我们就要给保存的变量cur乘以100并且parseFloat()来进行转化,而且opacity的单位从来都没有px,我们也不必拼接'px'。
但是我们在把opacity进行parseFloat()转换并且乘以100后存在一个Bug,从理论上来说,透明度无法精确地达到我们的目标值,因为浏览器会把0.3计算成0.3000000并且在做乘法以后计算出0.30000001这样的数字。所以,这样的计算是存在一定的误差的,我们要用Math.round()方法(四舍五入)来消除这种误差。
function startMove(obj, attr, iTarget) { clearInterval(obj.timer); obj.timer=setInterval(function (){ var cur=0; if(attr=='opacity') { cur=Math.round(parseFloat(getStyle(obj, attr))*100); } else { cur=parseInt(getStyle(obj, attr)); } var speed=(iTarget-cur)/6; speed=speed>0?Math.ceil(speed):Math.floor(speed); if(cur==iTarget) { clearInterval(obj.timer); } else { if(attr=='opacity') { obj.style.filter='alpha(opacity:'+(cur+speed)+')'; obj.style.opacity=(cur+speed)/100; } else { obj.style[attr]=cur+speed+'px'; } } }, 30); }
链式运动框架
链式运动是指,物体连续的在不同的时间段内基于不同的属性进行运动。
我们现在对刚才写过的多物体任意值运动框架进行一个小小的修改,给它增加一个回调函数,使得startMove()方法执行完毕以后会运行这个回调函数。
这个回调函数是一个可选的参数,如果没有也不会有什么影响,有则会执行。
function getStyle(obj, name) { if(obj.currentStyle) { return obj.currentStyle[name]; } else { return getComputedStyle(obj, false)[name]; } } function startMove(obj, attr, iTarget, fnEnd) { clearInterval(obj.timer); obj.timer=setInterval(function (){ var cur=0; if(attr=='opacity') { cur=Math.round(parseFloat(getStyle(obj, attr))*100); } else { cur=parseInt(getStyle(obj, attr)); } var speed=(iTarget-cur)/6; speed=speed>0?Math.ceil(speed):Math.floor(speed); if(cur==iTarget) { clearInterval(obj.timer); if(fnEnd)fnEnd(); } else { if(attr=='opacity') { obj.style.filter='alpha(opacity:'+(cur+speed)+')'; obj.style.opacity=(cur+speed)/100; } else { obj.style[attr]=cur+speed+'px'; } } }, 30); }
现在我们可以来调用这套链式运动框架,调用方式如下:
window.onload=function () { var oDiv=document.getElementById('div1'); oDiv.onmouseover=function () { startMove(oDiv, 'width', 300, function (){ startMove(oDiv, 'height', 300, function (){ startMove(oDiv, 'opacity', 100); }); }); }; oDiv.onmouseout=function () { startMove(oDiv, 'opacity', 30, function (){ startMove(oDiv, 'height', 100, function (){ startMove(oDiv, 'width', 100); }); }); }; };
支持多物体、任意值、链式运动及同时任意值的JSON格式参数的完美运动框架
我们以上写的运动框架距离最终的完美版本还差了一个功能,那就是不支持同时对两个及两个以上的属性进行运动操作。
要实现这个效果,我们必须使用JSON,即,把属性和目标值做成键值对的JSON对象作为参数传入startMove()方法,
然后在方法内使用for-in循环来获得要运动的属性和目标值并且进行设置。
而且我们还要考虑到,多个属性同时进行运动会有一些运动的目标值比较小,先停止了,而另外一些运动的目标值比较大,运动的时间更长,而目标值比较小的运动在停止的时候就会把定时器给关闭。所以我们要让所有的运动都结束以后再关闭定时器。
我们解决的办法就是,检测运动停止,设置标志变量。
function getStyle(obj, name) { if(obj.currentStyle) { return obj.currentStyle[name]; } else { return getComputedStyle(obj, false)[name]; } } function startMove(obj, json, fnEnd) { clearInterval(obj.timer); obj.timer=setInterval(function (){ var bStop=true; //假设:所有值都已经到了 for(var attr in json) { var cur=0; if(attr=='opacity') { cur=Math.round(parseFloat(getStyle(obj, attr))*100); } else { cur=parseInt(getStyle(obj, attr)); } var speed=(json[attr]-cur)/6; speed=speed>0?Math.ceil(speed):Math.floor(speed); if(cur!=json[attr]) bStop=false; if(attr=='opacity') { obj.style.filter='alpha(opacity:'+(cur+speed)+')'; obj.style.opacity=(cur+speed)/100; } else { obj.style[attr]=cur+speed+'px'; } } if(bStop) { clearInterval(obj.timer); if(fnEnd)fnEnd(); } }, 30); }
然后,我们无论进行什么样的运动操作,都可以正常运行了。
window.onload=function () { var oBtn=document.getElementById('btn1'); var oDiv=document.getElementById('div1'); oBtn.onclick=function () { startMove(oDiv, { 101, height: 300, opacity: 100}, function (){ alert('a'); }); }; };