摘要:相对以往的界面编程框架来说,WPF引入了很多激动人心的特性。对动画的抽象就是这些特性之一。但这并不意味着WPF的动画框架就已经很完美了。WPF利用Storyboard表示动画,通过在Storyboard中动态改变依赖属性的值,从而实现相应的动画效果。但是Storyboard有其本身的局限。其局限之一就是难以表示动画序列。本文对这个问题进行了探讨,在讨论了Storyboard局限的同时,给出了一个动画序列框架的初步实现。实验证明,这个框架在原有的动画基础上引入了序列的机制,可以更好地表示动画。在此基础上,完全可以对该框架进行扩展,使得其成为通用性的框架,在WPF/Silverlight中得到广泛的应用。
“飞刀,又见飞刀……” –引自古龙同名小说
“动画,又见动画……” –引自作者的梦话
0. 背景
好吧,我不得不说,虽然接触不久,但我真的迷上WPF了。与以往的界面框架相比,WPF确实改变了很多,从逻辑树到依赖属性,从路由事件到模板……,这些功能极大地丰富了界面开发的方法。但在所有的这些特性中,最令我魂牵梦萦的,还是动画。
是的,就是动画。WPF对动画进行了抽象,将动画与依赖属性绑在了一起。同时提供了Storyboard,这样,我们就可以很容易地动态修改依赖属性值,从而实现动画。动画实现变得容易了,带来的好处就是我们可以花费较少的工作量在程序中引入更丰富的动画,使得程序变得更酷——这是不言而喻的。甚至可以说,没有动画,WPF炫丽的界面就会黯然失色。而善于使用动画也是WPF程序设计者不可或缺的能力之一。
正是基于这样的背景,我在说梦话被老婆打醒后,决定认真研究一下动画。试着实现一些动画效果。
1. 问题描述
在实现了几个基本的动画效果之后,我遇到了一个问题:Storyboard不擅长表示动画序列。所谓动画序列呢,额……就是动画组成序列啦。就是我先播放一个动画,等到前一个动画播放完毕后再播放下一个动画,依此类推。
动画序列很有用。比如在视图转换的过程中,我们希望实现如下的动画:将当前的视图变小,变小到一定程度后从屏幕中移除,从而露出后面的动画。这本质上是一个动画序列,包含了两个动画。第一个动画减小视图尺寸,第二个动画移除减少尺寸后的视图。两个动画依次执行。还可以用动画序列实现很多功能,只要我们的想象力够丰富就可以。
用Storyboard也可以实现动画序列。在Storyboard中定义动画时,可以为一些动画(比如DoubleAnimation)指定开始时间。只要确保后一个动画的开始时间是前一个动画的终止时间,就能实现这个功能了。但这种实现方式有其自身的缺陷——代码复杂,不便于维护。所以我说Storyboard不擅长表示动画序列。为了给大家一个实际的感受,让我们看一个例子:
假设目前有一个按钮,单击按钮时,我们希望按钮首先变宽;变化完成后再变高;之后宽与高统一缩小;最后宽与高统一扩大一点。
这是一个典型的动画序列。为了用Storyboard实现这个功能,我们可以在XAML中引入如下的定义(可以在所附代码的OriginalMethod项目中找到相关的代码):
其中的“_btn”是动画将要实施的按钮。
不管你晕不晕,反正我看到上面的代码是晕了。这段代码非常不好,原因如下:
l 首先,在设置每个动画的BeginTime时,很容易设置错误(事实上,作者在写这段代码时,就出现了设置错误,导致动画效果不符合预期);
l 其次,这段代码没有层次感,一眼看上去,很难看清楚这段代码实际上定义了4段动画;
l 第三,这段代码难以修改:如果我们希望改变某段动画持续的时间,那么它后面所有的动画所持续的时间都需要发生变化;而且,如果我们希望改变动画的顺序,那么相应的每个动画的BeginTime可能都要发生改变。当引入的动画段落比较多时,这种方式很容易引入错误。
正是由于上述原因,我得出了“Storyboard”不擅长表示动画这个结论。
2. 动画序列的表示框架
为了解决上述问题,我设计并初步实现了一个动画序列。调用动画序列的代码所示(可以在本文所附代码中找到完成的示例):
这个代码实现了上述4段的动画。从代码中可以看出,使用动画序列,我们可以:
l 免除BeginTime的设置,减少出错的可能性;
l 代码更加易读:从代码中可以一目了然地看到其中包含的动画段数以及动画的执行顺序;
l 代码便于修改:对每个单独动画的调整,只需在相应的Storyboard中修改即可;对动画顺序的调整,则只需要调整Storyboard的顺序即可,无需其它的操作;
l 新的类可以与XAML之间较好的集成。
3. 动画序列框架的实现
这一部分讨论这个框架的实现方法。
为了实现序列的功能,我首先定义了一个序列类StoryboardChain,并在其中引入了属性:
用于包含Storyboard序列。
这个类同时包含了一个Begin方法,用于调用动画。这个方法的主要代码如下:
代码会遍历现有的Storyboard集合,通过ElementIndexer.SetPos为每一个Storyboard引入一个附加属性,这个附加属性用于表示当前动画执行完毕后,下一个动画的ID。对于集合中的最后一个动画,这个值为-1,表示没有后续需要执行的内容了。在此基础上,为每个Storyboard的Completed事件关联一个句柄OnCurrentFinished。当前动画执行完毕后,系统会调用这个函数,执行下一个动画。这些都设置完毕后,调用第一个Storyboard的Begin方法,开始整个动画序列的执行。
OnCurrentFinished则主要用于调用下一个动画,其主要代码如下所示:
这就将整个动画串起来了。
最后,我们需要为该类添加一个特性声明:
这样,在XAML中声明的Storyboard将被置于Animates中。
OK,大功告成!。现在就可以测试一下这个序列了。测试的代码也包含在附带的源码中。
4. 小结
在发现Storyboard表示动画序列差强人意后,为了能够用更丰富地方法表示动画,我实现了一个“动画序列”的基础框架:实现了基本的动画序列的功能。与仅仅采用Storyboard表示动画序列相比,使用这个框架可以也出更易懂,更便于维护的代码。
之所以说这个框架是一个“基础框架”,因为它还有很多可以改进的地方,比如:
l StoryboardChain中只包含了Begin方法,在实际使用中,可能需要中断动画的方法;
l 从上面的代码中可以看到,StoryboardChain中的每个Storyboard均需要设定TargetName,如果这些名称相同,可以考虑在StoryboardChain中进行设置,而不是针对每个Storyboard设置;
l StoryboardChain目前不能控制动画执行的总体时间:动画执行的总体时间是由每个Storyboard执行的时间求和得到的,可以考虑引入类似Grid的机制,使用“1*”等方式在Storyboard中设置一个相对执行时间,然后在StoryboardChain中引入总体时间控制;
l StoryboardChain中动画的执行顺序就是其中Storyboard声明的顺序,可以仿照Panel中的方式,设置每个Storyboard的ZIndex,用ZIndex调整动画的执行顺序
l ……
限于篇幅,这里就不对这些改进进行一一的讨论了。所要说明的是,这些改进相对比较简单。完全可以在现有的框架基础上实现。
WPF博大精深,作者水平有限,在发现Storyboard不擅长表示动画序列后也查阅了一些资料。由于没有找到好的方式,才自己实现了这个类。如果大家有更好的解决方案,还请不吝赐教!
TransTest
by LiWei
本文转自百度技术51CTO博客,原文链接:http://blog.51cto.com/baidutech/1033616,如需转载请自行联系原作者