Javascript动画效果的实现(三)
上两篇文章我们主要讲了如何使用Javascript实现动画的基本原理,以及如何获得效果较为自然的的运动公式。前两篇文章的重点在于讨论原理,可能急于动手的你已经按捺不住想要写一些自己的动画效果了。那么这篇文章我们就来实现一个自己的通用动画类。它包含一些常用的动画效果,例如显示隐藏,展开关闭等等,同样它也包含一个通用的动画函数,让你可以自己定义个性化的动画效果。
现在让我们来明确一下我们的目的:
1,一个提供计算动画位置的公式的类,我们称它为缓动类;
2,一个通用的动画对象类,可以方便的调用相关的方法进行动画;
3,动画对象类还应该提供一个通用的动画方法,可以让使用者自定义变化的样式和属性;
4,动画对象类应该提供一些快捷的动画方法,例如快速的显示隐藏,展开关闭。
现在我们的目标都明确了,下面要做的就是将我们的想法实现为Javascript代码。你可以点击这里查看Demo效果。
按照我们目的地顺序,我们第一步来实现我们的缓动类,根据我们上两篇文章中的内容,我们能够轻松的得到我们缓动类:
var Tween={ oLinear: function(t,b,c,d){ return c*t/d + b; }, oQuad: { oeaseIn: function(t,b,c,d){ oreturn c*(t/=d)*t + b; o}, oeaseOut: function(t,b,c,d){ oreturn -c *(t/=d)*(t-2) + b; o}, oeaseInOut: function(t,b,c,d){ oif ((t/=d/2) < 1) return c/2*t*t + b; oreturn -c/2 * ((--t)*(t-2) - 1) + b; o} o} }
可以看出我们的缓动类中现在包含的方法还不多,只有两个,分别提供了线性运动和二次缓动,当然你要是愿意也可以添加更多的公式。下面我们将创建我们的动画功能类,我把它命名为Motion,这个类包含的最基本的方法有两个,一个就是run()另外一个是animate()。相关代码:
var Motion = function(target){ o//重置相关参数 othis.resetStatus=function(){} o//设置延时时间 othis.setDelayTime=function(_delayTime){} o//设置执行的样式 othis.setStyles=function(_styles){} o//设置回调函数 othis.setCallBackFunc=function(_callBackFunc){} o//将函数推入执行队列 othis.pushFuncToQueue = function(funcString){} o//获取动画执行状态 othis.getAnimateStatus=function(){} o//set quick methord target object styles othis.setQuickStyle=function(_quickStyles,quickType){} o//get target object's width and height othis.getTargetStyle=function(){} o//calculate the target object's styles change value and the original value ovar calculatChange=function(){} o//设置目标DOM对象样式 ovar setTargetStyles=function(_styles){} ovar beforeRun = function(){} othis.afterRun = function(){} othis.run(){ odo something... o} o} Motion.prototype = { o//接受一个json形式的样式参数,以及一个方法 oanimate:function(styles,callBackFunc){ odo something... othis.run(); o} }
现在简单的解释一下,Motion对象包含的最基础的一个方法是run()方法,这个方法是内部方法,它的作用是,根据提供的变化参数运行动画效果。另外还有一个方法就是animate()方法,这个方法是提供给用户使用的方法,用户使用的时候调用animate()方法就可以实现动画效果。当然系统同样需要很多的在执行动画时需要用到的参数,例如当前时间t,运行时间d,setTimeout()函数的延时时间delayTime等等。下面是所有用到的属性:
var t = 0;//当前时间 var d = 150;//持续时间 var delayTime = 10;//延时时长,ms var styles=null;//变化的样式 var stylesChange={};//样式的改变值 var stylesBegin={};//样式的初始值 var callBackFunc=null;//回调函数 var timer=null;//setTimeout函数的句柄 var quickStylesBefore=null;//快捷方法的开始样式 var quickStylesAfter=null;//快捷方法的结束样式 var animateStatus=false;//当前动画的状态,false为动画未执行或执行完毕 var funcQueue=[];//动画运行队列 if(typeof(target)=='string')//判断如果传入的target参数为字符串则将其读取为DOM对象 target=document.getElementById(target);
下面我们从用户开始调用animate这个方法来开始分析。在我们的页面中添加一个DIV:要使用我们的动画对象就必须开始的时候新建一个动画对象(这样可以让页面同时运行多个动画,我没有想出更好的办法,静态方法来实现可能会更易用),并将要添加动画效果的元素作为参数传入或者将元素的ID传入。
divM = new Motion('myDiv');
用户可能想让DIV向左移动到500px处,向下移动到200px处,那么他应该这么调用相关方法:
divM.animate({left:'500px',top:'200px'}); 我们看一下animate()这个函数中到底有哪些方法: if(this.getAnimateStatus()) oreturn this.pushFuncToQueue([this.animate,arguments]); this.resetStatus(); this.setStyles(styles) this.setCallBackFunc(callBackFunc); this.run();首先我们判断当前动画的状态,如果当前动画正在运行中则将本次动画推入动画队列中等待,如果当前不再动画状态那么继续执行下面的代码,首先是重置本次动画的基本状态,然后设置动画的最终样式,再次设置动画执行完毕后的回调函数,准备工作都做好了,最后执行动画。
前三个方法比较容易理解,采用闭包的手段将相应的属性赋给当前动画实例的私有变量。下面我们分析一下run()函数的主要内容:
(t == 0) && beforeRun(); for(var styleName in styles){ target.style[styleName]=Tween.Quad.easeInOut(t,stylesBegin[styleName],stylesChange[styleName],d) + 'px'; } t++; if(t<=d) return timer = setTimeout(dk.bind(this,this.run),delayTime); this.afterRun(); if(callBackFunc) return callBackFunc();
看过上两篇的文章应该对这部分的内容不陌生,这个函数主要就是根据缓动公式计算出当前的值然后赋给对象,通过时间标识t的不断递增从而形成动画效果。可以看出这里比以前的示例主要是增加了beforeRun()和afterRun()这两个函数。这两个函数主要是在动画运行之前设置相关状态以及参数,和在动画运行完毕后检查动画运行队列是否为空如果不为空则继续运行相应动画,以及设置最终的状态。看到这不知道您对整个动画对象有了一定的了解,至于其他的快捷方法你可以根据相同的思路去考虑,这里就不再赘述。
下面是完整的代码:
var Tween = { Linear: function (t, b, c, d) { return c * t / d + b; }, Quad: { easeIn: function (t, b, c, d) { return c * (t /= d) * t + b; }, easeOut: function (t, b, c, d) { return -c * (t /= d) * (t - 2) + b; }, easeInOut: function (t, b, c, d) { if ((t /= d / 2) < 1) return c / 2 * t * t + b; return -c / 2 * ((--t) * (t - 2) - 1) + b; } }, Cubic: { easeIn: function (t, b, c, d) { return c * (t /= d) * t * t + b; }, easeOut: function (t, b, c, d) { return c * ((t = t / d - 1) * t * t + 1) + b; }, easeInOut: function (t, b, c, d) { if ((t /= d / 2) < 1) return c / 2 * t * t * t + b; return c / 2 * ((t -= 2) * t * t + 2) + b; } }, Quart: { easeIn: function (t, b, c, d) { return c * (t /= d) * t * t * t + b; }, easeOut: function (t, b, c, d) { return -c * ((t = t / d - 1) * t * t * t - 1) + b; }, easeInOut: function (t, b, c, d) { if ((t /= d / 2) < 1) return c / 2 * t * t * t * t + b; return -c / 2 * ((t -= 2) * t * t * t - 2) + b; } }, Back: { easeIn: function (t, b, c, d, s) { if (s == undefined) s = 1.70158; return c * (t /= d) * t * ((s + 1) * t - s) + b; }, easeOut: function (t, b, c, d, s) { if (s == undefined) s = 1.70158; return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b; }, easeInOut: function (t, b, c, d, s) { if (s == undefined) s = 1.70158; if ((t /= d / 2) < 1) return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b; return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b; } } } var Motion = function (target) { var t = 0; //当前时间 var d = 150; //持续时间 var delayTime = 10; //延时时长,ms var styles = null; //变化的样式 var stylesChange = {}; //样式的改变值 var stylesBegin = {}; //样式的初始值 var callBackFunc = null; //回调函数 var timer = null; //setTimeout函数的句柄 var quickStylesBefore = null; //快捷方法的开始样式 var quickStylesAfter = null; //快捷方法的结束样式 var animateStatus = false; //当前动画的状态,false为动画未执行或执行完毕 var funcQueue = []; //动画运行队列 if (typeof (target) == 'string') //判断如果传入的target参数为字符串则将其读取为DOM对象 target = document.getElementById(target); this.resetStatus = function () { t = 0; styles = null; stylesChange = {}; stylesBegin = {}; callBackFunc = null; timer = null; quickStylesBefore = null; quickStylesAfter = null; } this.setDelayTime = function (_delayTime) { delayTime = _delayTime; } this.setTarget = function (_target) { target = _target; } this.setStyles = function (_styles) { styles = _styles; } this.setCallBackFunc = function (_callBackFunc) { callBackFunc = _callBackFunc; } this.getTarget = function () { return target; } //将函数推入执行队列 this.pushFuncToQueue = function (funcString) { funcQueue.push(funcString); } //获取动画执行状态 this.getAnimateStatus = function () { return animateStatus; } //set quick methord target object styles this.setQuickStyle = function (_quickStyles, quickType) { if (quickType) quickStylesBefore = _quickStyles; else quickStylesAfter = _quickStyles; } //get target object's width and height this.getTargetStyle = function () { return { width: target.style.width || target.clientWidth, height: target.style.height || target.clientHeigth } } //calculate the target object's styles change value and the original value var calculatChange = function () { for (var styleName in styles) { stylesChange[styleName] = parseInt(styles[styleName]) - parseInt(target.style[styleName] || 0); stylesBegin[styleName] = parseInt(target.style[styleName] || 0); } } var setTargetStyles = function (_styles) { for (var styleName in _styles) { target.style[styleName] = _styles[styleName]; } } var beforeRun = function () { quickStylesBefore && setTargetStyles(quickStylesBefore); calculatChange(); target.style.display = 'block'; animateStatus = true; } this.afterRun = function () { if (target.style.width == '0px' || target.style.height == '0px') target.style.display = 'none'; quickStylesAfter && setTargetStyles(quickStylesAfter); if (funcQueue.length > 0) { animateStatus = false; var currentFuncArray = funcQueue.shift(); return currentFuncArray[0].apply(this, currentFuncArray[1]); } animateStatus = false; //this.slideDown(); } this.run = function () { (t == 0) && beforeRun(); for (var styleName in styles) { target.style[styleName] = Tween.Quad.easeInOut(t, stylesBegin[styleName], stylesChange[styleName], d) + 'px'; } t++; if (t <= d) return timer = setTimeout(dk.bind(this, this.run), delayTime); this.afterRun(); if (callBackFunc) return callBackFunc(); } } motion.prototype = { animate: function (styles, callBackFunc) { if (this.getAnimateStatus()) return this.pushFuncToQueue([this.animate, arguments]); this.resetStatus(); this.setStyles(styles) this.setCallBackFunc(callBackFunc); this.run(); }, slideDown: function (callBackFunc) { if (this.getAnimateStatus()) return this.pushFuncToQueue([this.slideDown, arguments]); this.resetStatus(); var targetStyle = this.getTargetStyle(); this.setQuickStyle({ height: '0px' }, true); this.setStyles({ height: targetStyle.height }); this.setCallBackFunc(callBackFunc); this.run(); }, slideUp: function (callBackFunc) { if (this.getAnimateStatus()) return this.pushFuncToQueue([this.slideUp, arguments]); this.resetStatus(); var targetStyle = this.getTargetStyle(); this.setQuickStyle({ height: targetStyle.height }, false); this.setStyles({ height: '0px' }); this.setCallBackFunc(callBackFunc); this.run(); }, show: function (callBackFunc) { this.resetStatus(); var targetStyle = this.getTargetStyle(); this.setQuickStyle({ width: '0px', height: '0px' }, true); this.setStyles({ width: targetStyle.width, height: targetStyle.height }); this.setCallBackFunc(callBackFunc); this.run(); }, hide: function (callBackFunc) { this.resetStatus(); var targetStyle = this.getTargetStyle(); this.setQuickStyle({ width: targetStyle.width, height: targetStyle.height }, false); this.setStyles({ width: '0px', height: '0px' }); this.setCallBackFunc(callBackFunc); this.run(); } }
写这个对象主要是为了自己实现一个比较通用的动画对象,这样在做项目的时候如果你只是需要一些基本的动画效果的话就不用将一个javascript引入了,只需要引入这一个对象就好。当然这是我根据自己的思路写的,只是实现的一些效果,肯定还有很多地方需要改进,比如透明度的改变就没有加入(其实加入并不难,再次改进的时候会加入)。之所以有这么多好用的框架了还要写这个东西主要是是为了深入了解一下相关方面的知识,毕竟我们不能只是停留在利用工具的层次上不是吗?
欢迎批评指正,共同进步!
转载请注明原文出处《Javascript动画效果的实现(三)》 如无特别声明,所有文章均遵守创作共用 署名-非商业-禁止演绎 3.0协议。
红色对人的视力损伤很大的啊!
[回复]
@kim, 做的时候为了醒目,也就顾不上太多了,关键是红色的代码比较方便#f00,哈哈,毕竟只是功能Demo不是嘛?下次我一定注意用蓝色或者绿色……
感谢支持!
[回复]
发现一个小问题,我点了三次Hide,它就Hide了三次,我觉得连续点的几次Hide,是不是写个判断只执行一次Hide比较好?个人愚见哈。
[回复]
@简明现代魔法, 感谢您的意见!执行动画函数的队列可能需要继续完善一下,如果后一个动画与前一个动画相同那么就取消第二个动作的执行,但是判断起来可能比较困难,我尝试改进一下。
[回复]
非常感谢你的代码。我搜索到,直接用到我的网页了。重量轻,简单实用。
谢谢
[回复]
@段gongzi, 非常高兴能够你有所帮助,另外你提到的slideUp函数,这是一个用于是对象在显示与不显示直接进行切换的快捷方法,它本身并不支持任意高度,如果需要任意高度请使用animite函数,animite({height:’50px’})。
[回复]
@DK, 哦。太好了。非常非常感谢。现在很好用。
[回复]
发现一个问题。如果想要slideup 到人以高度,您的代码是不行的。会出错。
把slideup 里面的setQuickStyle参数改为true以后,就可以任意高度的伸展,缩回了。
othis.setQuickStyle({height:targetStyle.height},true);
完整的代码:
oslideUp:function(callBackFunc){
oif(this.getAnimateStatus())
oreturn this.pushFuncToQueue([this.slideUp,arguments]);
othis.resetStatus();
ovar targetStyle=this.getTargetStyle();
othis.setQuickStyle({height:targetStyle.height},true);
othis.setStyles({height:’80px’});
othis.setCallBackFunc(callBackFunc);
othis.run();
o},
[回复]
@段gongzi, 这个工具主要是实验性质的代码,所以也没有维护用户手册。
这个地方可能有所误解,slideup方法本身是并不支持任意高度的,它是用于快速切换显示状态的方法,如果需要任意高度请使用animate方法。
[回复]