设计动画管理类

课后整理 2020-12-14

本节案例将设计一个可折叠的面板,效果如图1所示。折叠面板默认显示为展开效果,当使用鼠标单击标题栏时,则折叠面板以动画形式逐步收起内容框,再次单击标题栏,内容框又会以动画形式缓慢展开。

预览效果

 

展开效果

 

动画演示收起

图1  可折叠面板演示效果

这是一个简单的DOM界面应用效果,如果不考虑代码封装和优化,可以直接为标题栏绑定鼠标单击事件,然后在事件处理函数中通过条件语句设计内容框的显示或隐藏。

本节案例的目的不仅仅要完成这个折叠动画的任务,而是通过这个案例为大家提供一种更具通用的应用模式,解决UI动画设计的基本套路。案例借助闭包,把动画设计与管理封装在一个匿名函数内,并让匿名函数自执行,从而形成一个独立的、封闭的上下文环境,在该函数内声明一个全局类型函数animateManage(),并为该类型函数扩展大量原型方法,以便实现动画的高效管理。

本例模式模仿jQuery结构。定义一个自执行匿名函数,并把全局对象window传入函数,然后在函数体内通过window.animateManage定义一个全局函数,并为该构造函数animateManage()定义原型对象。

(function (window,  document, undefined) {
    window.animateManage = function( optios ) {  }
    animateManage.prototype = {};
})(window, document)

实际上,上述写法完全可以转换为:

var animateManage =  function( optios ) {}
animateManage.prototype  = {};
(function (window, document,  undefined) {})(window, document)

但是,本例写法能够实现在animateManage()构造函数或其原型对象中访问外部匿名函数的私有变量。由于函数animateManage()内部没有引用外部匿名函数的私有变量:

window.animateManage = function( optios ) {
        this.context =  optios;//当前对象 }

且使用new运算符调用它,创建实例化对象:

new animateManage({});

因此,外部匿名函数暂时还不是一个闭包体,调用完毕后自动被销毁。但是,animateManage()的原型对象包含很多方法,这些方法内部与外部匿名函数的私有变量保持联系。作为全局作用域上的对象,这些原型对象就构成了对外部匿名函数的引用,确保外部匿名函数被调用后,依然存在,从而形成一个持久存在的闭包体。

animateManage()是一个动画管理的类型函数,参数optios表示一个参数对象,可以设置如下属性:

animateManage()包含多个原型方法,具体说明和完整代码如下:

 
(function (window, document, undefined) {
    var _aniQueue = [],                                          //动画队列
        _baseUID = 0,                                          //元素的UID基础值
        _aniUpdateTimer = 13,                             //动画更新的时间
        _aniID = -1 ,                                            //检测的进程ID
         isTicking = false;                                     //检测状态
    window.animateManage = function( optios ) {
        this.context =  optios;                               //当前对象
    }
    animateManage.prototype = {
        init : function( ){ this.start(this.context);},   //初始化方法
        stop : function(_e){                                   //停止动画
            clearInterval(_aniID);
            isTicking = false;
        },
        start : function(optios){                              //开始动画
           if(optios) this.pushQueue(optios);          //填充队列属性
           if(isTicking || _aniQueue.length ===  0) return false;
           this.tick();
           return true;
        },
        tick : function(){                                      //动画检测
            var self = this; isTicking = true;
            _aniID = setInterval(function(){
                if(_aniQueue.length === 0)  {self.stop();}
                else{
                    var i = 0, _aniLen =  _aniQueue.length;
                    for(; i < _aniLen ;  i++){
                        _aniQueue[i] &&  self.go(_aniQueue[i], i);
                    }
                }
            }, _aniUpdateTimer)
        },
        go : function(_options, i){                          //执行具体的动画业务
            var n = this.now(),
                st = _options.startTime,
                ting = _options.time,
                e = _options.context,
                t = st + ting,
                name = _options.name,
                tPos = _options.value,
                sPos = _options.startValue,
                effect = _options.effect,
                scale = 1;
            if(n >=  t){//如果当前的时间 > 开始时间+结束时间则停止当前动画
                _aniQueue[i] = null;
                this.delQueue();
            }else{
                tPos = this.aniEffect({
                    e:e, ting :ting , n :n , st  :st , sPos:sPos, tPos:tPos
                },effect)
            }
            e.style[name] = name ==  "zIndex" ? tPos : tPos + "px";
            this.goCallBack(_options.callback,  _options.uid);//是否执行回调函数
        },
        aniEffect : function(_options, effect){                //动画效果,用户可以扩展动画算法
             effect = effect ||  "linear";
             var _effect ={
                  "linear":function(__options){             //线性运动
                     var scale = (__options.n -  __options.st)/__options.ting,
                         tPos = __options.sPos  + (__options.tPos - __options.sPos)*scale;
                     return tPos;
                 }
             }
            return _effect[effect](_options);
        },
        goCallBack : function(callback, u){                    //回调
            var i = 0,_aniLen =  _aniQueue.length,isCallback = true;
            for(; i < _aniLen ; i++){
                if(_aniQueue[i].uid ==  u){isCallback = false;}
            }
            if(isCallback){callback &&  callback();}
        },
        pushQueue : function(options){                  //压入执行动画队列
            var con =  options.context,
                t =  options.time || 1000,
                callback =  options.callback,
                effect =  options.effect,
                starCss =  options.starCss,
                c =  options.css, name = "",
                u = this.setUID(con);
            for(name in c){
                _aniQueue.push({
                    "context" : con,
                    "time" : t,
                    "name" : name,
                     "value":parseInt(c[name], 10),
                     "startValue":parseInt((starCss[name] || 0)),
                    "effect":effect,
                    "uid" : u,
                     "callback":callback,
                    "startTime" :  this.now()
                })
            }
        },
        delQueue : function(){                        //删除动画队列中指定的动画
            var i = 0, l = _aniQueue.length;   //寻找到指定动画队列,将其删除
            for(; i < l; i++ ){
                if(_aniQueue[i] === null)  _aniQueue.splice(i, 1);
            }},
        now : function(){                               //获取现在时间
           return new Date().getTime();},
        getUID : function(_e){                       //获取元素的UID
            return  _e.getAttribute("aniUID");},
        setUID : function(_e, _v){                  //设置元素的UID
            var u = this.getUID(_e);
            if(u) return  u;                           //如果存在UID则直接返回
            u = _v || _baseUID++;                 //生成UID
            _e.setAttribute("aniUID",  u);
            return u;}
    };
})(window, document)

设计好动画管理的工具类型,就可以在具体案例中应用了。在本节示例中,为标题栏标签<dt id="header">绑定鼠标单击事件,在事件处理函数中,实例化animateManage()类型,并传入参数对象,然后调用init()方法,开始执行动画。完整代码可以参考本节实例源代码。

new animateManage({                                      //滑动展开广告
    "context" : dd,                                          //被操作的元素
    "effect":"linear",                                       //定义线性动画
    "time": 1000,                                            //持续时间
    "starCss":{"height":300},                           //元素的起始值偏移量
    "css" :{"height":0}                                    //元素的结束值偏移量
}).init();