代码改变世界

Animation全接触

2007-09-16 20:03  Jacky_Xu  阅读(683)  评论(0编辑  收藏  举报
关于AjaxControlToolkit中的Animation的话题,一直留在我的“写作队列”中。但是由于最近事情较多,一直没有开始写这个系列的文章。现在忽然意识到,如果不趁这方面的文章还没有大量出现之前推出的话,我们又要落后于那些外国人了(其实外国人也不可能知道我写了什么,可惜我会知道外国人写的东西,所以我是在“自爽”)。

  在这个系列中,我将对AjaxControlToolkit中的Animation相关的设计,开发,使用,解析(例如服务器端写的XML怎么变成Animation对象的)等各方面进行分析。在这个系列中涉及到了包括AjaxControlToolkit中的各种Animation客户端和服务器端模型,AnimationBehavior,AnimationExtender,UpdatePanelAnimationExtender等所有Animation相关组件。

  希望我这个系列的文章能够让大家对于AjaxControlToolkit中的Animation有深入的了解,让Animation不再神秘。
Animation是AjaxControlToolkit中提供的经典模型之一,这个模型下的各种组件经过组合可以出现非常丰富的动画效果。Animation模型也是Microsoft AJAX Library客户端Component模型的经典案例,掌握了Animation几乎也相当于掌握了Component模型——甚至包括了Component模型的派生:Behavior模型和Control模型。在我看来,除了在“概念”上的区别之外,从技术角度分析它们几乎都是同样的东西。

  另外,Animation源代码中的注释甚至超过了代码本身的数量,因此它也是非常优秀的学习资料。

  Animation模型的基础是客户端AjaxControlToolkit.Animation.Animation类。为了编程方便,AjaxControlToolkit为AjaxControlToolkit.Animation命名空间取了一个别名“$AA”,因此我们在使用Animation类时可以通过“$AA.Animation”来访问它。

  下图为Animation的UML表示:

Animation类

  Animation类继承了Sys.Component类,因此它也含有Sys.Component的所有成员和特性,例如raisePropertyChanged方法和propertyChanged事件。它的主要成员如下:

构造函数:

  • Animation(target, duration, fps):用于构造一个Animation对象,初始化一些值,不多说了。

属性:

  • target:RW属性,DOM元素类型。表示Animation作用的DOM元素,例如ColorAnimation中改变颜色的那个对象。
  • duration:RW属性,Number类型,默认值为1。表示这个Animation从头至尾“播放”一遍所需的时间,单位为“秒”。
  • fps:RW属性,Number类型,必须是整数,默认值为25。fps即为frame per second,每秒多少帧。表示一秒钟的动画需要使用多少次变化进行。
  • animationTarget:只写属性(Bad Practice),String类型。通过ID查找target。
  • isActive:只读属性,Boolean类型。查看Animation是否处于活动状态(Play或Pause状态)。
  • isPlaying:只读属性,Boolean类型。查看Animation是否处于播放状态。
  • percentComplete:只读属性,Number类型。查看Animation已经播放的百分比。

方法:

  • dispose:销毁Animation对象,不多说了。
  • getAnimationValue:抽象方法。传入percentage作为参数,返回任意对象表示当前Animation状态。
  • setValue:抽象方法。传入一个表示Animation状态的对象用于更新Animation效果。
  • interpolate:传入start,end和percentage,返回start和end之间处于percentage位置的那个值。
  • onEnd:Animation终止时会调用的方法。
  • onStart:Animation启动时会调用的方法。
  • onStep:Animation运行时每一步会调用得方法,接受当前percentage作为参数。
  • pause:暂停Animation。
  • play:开始Animation。
  • stop:停止Animation。当finish参数为true,或者没有提供finish参数时,Animation将会停留在最终状态。
  • raiseEnded:触发ended事件。
  • raiseStarted:触发started事件。
  • setOwner:设置Animation的Owner

事件:

  • ended:Animation结束时触发的事件。
  • started:Animation开始时触发的事件。

域变量(是这样翻译吗?):

  • DynamicProperties:Object类型。其实是一个字典,存放了Animation在开始前需要进行的操作。

 

  上面的成员描述简单到了似乎没有多大价值的地步,因此我会再针对一些成员进行解释。不过一些涉及到高级话题(比如Animation的父子关系,从XML中解析Animation等等)的成员,暂时就不在这片文章中进行讲解了。另外,这片文章也会跳过简单成员的一些涉及到高级话题的细节。

  当我们调用Animaion对象的play方法时,它会初始化一个Timer对象,并调用onStart方法。在每次Timer的tick事件触发时,Animation则会得到当前的percentage,作为参数执行onStep方法。当我们当调用pause方法时,它只是简单地停止Timer对象。而我们当我们调用stop方法时,则Timer对象会被销毁,并调用onEnd方法。onStart、onStep和onEnd的方法的原始实现都非常简单,例如onStart和onEnd分别调用了raiseStarted和raiseEnded方法,用于触发started事件和ended事件。

  Timer的interval又是如何决定的呢?Animation在播放时,通过Fps就可以确定Animation中相邻两个step的间隔长度,这就是Timer的interval。从理论上讲,fps × step = 1000(毫秒),但是事实上,受到window.setInterval方法的限制,fps越高则误差越大。显然,Fps配合duration就能够算出每个step过后的percentage。

  onStep的默认实现会自动将当前的percentage作为参数传入getAnimationValue方法,用于获得一个表示Animation状态的对象,然后再立即传入setValue方法用于更新Animation的样式。这个状态对象可以是一个简单的数字,也可以是一个表示许多信息的复杂对象。

  因此,如果实现的是一个简单的Animation时,一般只需要正确实现getAnimationValue和setValue两个抽象方法即可。在开发高级的Animiation时,可能我们也会覆盖onStart,onStep和onEnd方法。至于其他的方法,我们一般都直接使用Animation类中定义的实现,仅仅会在非常特殊的情况下才进行覆盖。

  在这里我准备了一个简单的CounterAnimtion类,用于在target元素中显示数字,以下则是它的实现:

CounterAnimation
Type.registerNamespace("Jeffz");

Jeffz.CounterAnimation = function(target, duration, fps)
{
    Jeffz.CounterAnimation.initializeBase(this, arguments);
    
    this._startValue = 0;
    this._endValue = 100;
}
Jeffz.CounterAnimation.prototype = 
{
    get_startValue : function()
    {
        return this._startValue;
    },
    set_startValue : function(value)
    {
        if (this._startValue !== value)
        {
            this._startValue = value;
            this.raisePropertyChanged("startValue");
        }
    },
    
    get_endValue : function()
    {
        return this._endValue;
    },
    set_endValue : function(value)
    {
        if (this._endValue !== value)
        {
            this._endValue = value;
            this.raisePropertyChanged("endValue");
        }
    },
    
    getAnimatedValue : function(percentage)
    {
        return this.interpolate(this._startValue, this._endValue, percentage);
    },
    
    setValue : function(value)
    {
        this.get_target().innerHTML = Math.round(value);
    }
}
Jeffz.CounterAnimation.registerClass("Jeffz.CounterAnimation", $AA.Animation);

 

  CounterAnimation定义了两个额外的属性,startValue和endValue,表示初始值和末尾值。然后我们又实现了getAnimationValue和setValue方法。所有的实现都非常简单。在这里,我们用到了定义在Animation类中的一个辅助方法interpolate。这个方法接受start,end和percentage三个参数,返回start,end两个值之间处于percentage位置处的值。例如:interpolate(100, 300, 50)则返回200。

  这个Animation的使用效果如下:

Duration:
Fps:
Start Value:
End Value:

 

  需要注意的是,这里用的Timer并不是MicrosoftAjaxTimer.js中的Sys._Timer类,而是重新定义的Sys.Timer类。Sys._Timer类是为UpdatePanel服务的,它依赖于PageRequestManager等对象。Sys.Timer是AjaxControlToolkit中定义的组件,封装了window.setInterval方法,是个非常通用的Timer组件。如果需要的话,您也可以单独将它取出并使用。
ParentAnimation,顾名思义,是一组Animation的父亲,是所有需要包含其它Animation的父类,我们熟悉的ParallelAnimation(并行动画)和SequenceAnimation(顺序动画)都是它的子类。从这个概念上来说,这也是个非常经典的Composit模式应用。ParentAnimation相关的UML类图如下:

构造函数:

  • ParentAnimation:比Animaion类的构造函数多一个Animation数组作为参数,作为初始化的子Animation对象。

属性:

  • animations:只读属性,Array类型。返回所有当前的子Animation。

方法:

  • add:添加一个子Animation对象。
  • clear:清除并销毁所有子Animation对象。
  • dispose:销毁当前ParentAnimation对象。
  • initialize:初始化当前ParentAnimation对象。
  • remove:删除并销毁指定的子Animation对象。
  • removeAt:删除并销毁指定下标的子Animaion对象。

  ParentAnimation在Animation基础上添加了一些集合操作的方法。

  在ParentAnimation的构造函数中,会得到一个数组,存放了初始情况下所有的子Animation对象,它们会被依次调用add方法添加到ParentAnimation自己维护的数组中。而在ParentAnimation的initialize方法中,会确定每个子Animation有没有被初始化,如果没有,则调用它的initialize方法。

  值得注意的是,像remove,removeAt和clear这些删除子Animation的方法中(也包括dispose方法,它调用了clear方法),除了从ParentAnimation维护的数组中去除子Animation对象之外,还会将其销毁(调用其dispose方法),因此您再也无法使用这个子Animation对象了。

  在ParentAnimation的add方法有个特别的地方,它是这样的实现的:

ParentAnimation的add方法
add : function(animation) {
    if (this._animations) {
        if (animation) {
            animation._parentAnimation = this;
        }
        Array.add(this._animations, animation);
        this.raisePropertyChanged('animations');
    }
}

 

  可以看到,add方法在传入的animation对象中添加了一个_parentAnimation引用指向自己。那么_parentAnimation的作用是什么呢?这就要细化到我们在上一篇文章中的Animation模型了。

  关键在于Animation类的target属性,这是它的实现:

Animation类的target属性
get_target : function() {
    if (!this._target && this._parentAnimation) {
        return this._parentAnimation.get_target();
    }
    return this._target;
}

 

  在target属性的getter中,如果发现当前Animation没有指定target,它则会设法通过_parentAnimation引用来寻找父Animation,并使用父Animation的target属性指定的对象。因此,我们可以仅仅在父Animation中指定target,它的所有子Animation就能统一对同一个对象进行操作了。

  ParentAnimation是一种比较特别的Animation对象,因为它的作用是“组织和管理”子Animation,因此它使用的是其实是子Animation的功能,而它本身并不提供动画效果。因此它会覆盖Animation类的一些特别的方法,比如onStart、onStep和onEnd,而像SequenceAnimation甚至覆盖play,pause和stop方法。

  作为示例,我们来开发一个RandomExecutionAnimation,它的作用是随机从子Animation中选择一个,并播放它的动画效果。RandomExecutionAnimation的代码如下:

RandomExecutionAnimation
Type.registerNamespace("Jeffz");

Jeffz.RandomExecutionAnimation = function(target, duration, fps, animations)
{
    Jeffz.RandomExecutionAnimation.initializeBase(this, arguments);
    
    this._executingAnimation = null;
}
Jeffz.RandomExecutionAnimation.prototype = 
{
    dispose : function()
    {
        Jeffz.RandomExecutionAnimation.callBaseMethod(this, "dispose");
        
        delete this._executingAnimation;
    },
    
    onStart : function()
    {
        Jeffz.RandomExecutionAnimation.callBaseMethod(this, "onStart");
        
        var animations = this.get_animations();
        var index = Math.floor(animations.length * Math.random());
        this._executingAnimation = animations[index];
        this._executingAnimation.onStart();
    },
    
    onStep : function(percentage)
    {
        this._executingAnimation.onStep(percentage);
    },
    
    onEnd : function(percentage)
    {
        this._executingAnimation.onEnd();
    }
}
Jeffz.RandomExecutionAnimation.registerClass(
    "Jeffz.RandomExecutionAnimation", $AA.ParentAnimation);

 

  在RandomExecutionAnimation中,我们覆盖了onStart、onStep和onEnd方法,将功能完全委托给随机选择的子Animation。因此,在子Animation中设置的duration和fps属性的效果将被忽略,它们将会完全使用RandomExecutionAnimation对象中设置的duration和fps。ParallelAnimation也使用了这种做法,因为它所有的子Animaion需要同时播放,所以ParallelAnimation就提供了一个同步调配的作用。

  RandomExecutionAnimation的使用效果如下:

Hello World
Duration:
Fps:

 

  在这个示例中,我在RandomExecutionAnimation中放置了3个ColorAnimation,分别用于变化元素的边框,背景和前景的颜色。大家可以多次点击Play按钮察看效果。