《Programming WPF》翻译 第8章 动画
想象带有一个完全静态外观的应用程序,对鼠标的点击或其他输入,将提供非可视化的反应。有时候这是困难的——通知应用程序是否在执行功能或已经被冻结了。我们依赖于可视化反馈来保证应用程序可以响应我们的输入。添加动作到到你的用户界面可以使之苏醒以及增强应用程序的交互式感觉。控件经常模仿物理性的行为。例如,按钮被点击的时候变得明显被挤入。然而,如果按钮转换的很突然——在它的正常和被挤入的状态之间,它看上去很做作的。使用动画,我们可以制作看上去更现实的转换,以及为应用程序提供一个更自然的感觉。
动画也是有用的——对于处理从一个视图转换到另一个。在真实世界里,我们习惯于将突然无端出现的条目物化,但是计算机程序经常使用这样的生硬的转换。在电影非常早期的时代,
但是计算机程序经常使用相当突然的转换。在电影的早些时代,调整镜头以使得对象或人物突然出现,是一种有效的使观众惊恐的方法,既然这是一种看起来相当不自然的方式。这些天我们已经习惯于不真实的图像,不会被轻易的震惊,但是突然的装换仍然可以震惊的。小心的和便捷的使用动画可以使用户更简单的遵循可视化转换,正如从一个页面到另一个页面的移动或Windows和其它UI样式的出现和消失。
多年以来,Windows已经能够播放视频剪辑,但这只是提供了移动内容的孤岛。普通控件的动画样式是更加困难的。WPF使得添加动画到你的应用程序中是容易的——提供对动画广泛的支持:几乎是任何用户界面元素的可见外表。
8.1动画基础
动画包括在一段时间内改变用户界面的某些可见的特征,如它的大小、位置或颜色。你可以做到这一点,非常困难的通过创建一个timer并在每一个timer_tick句柄中修改用户界面的外观。当然,这是动画在Win32或Windows Forms中典型的做法。幸运的是,WPF照顾到这些低级别的细节。动画,就像WPF中的其他特征,简单的要求我们声明想要做的。系统会为我们照顾它的实现。
所有的WPF动画支持归结为,在一段时间内改变一个或多个属性。这意味着有很多限制在WPF动画系统能为你做些什么上。例如,可视化树全部保持着同样的结构。一个动画不可能为你添加或移除元素(虽然为动画设置属性使元素可见是可能的)。没有办法提供一个“before”和“after”的场景,或者使WPF在这两者间添加新场景。这意味这没有一种自动的方法——做一个动画,从一种外观转换到另一种,其程度足以使某个元素从起始位置滑动到终止位置。
了解什么动画可以或不可以实现的关键是,理解它的聚焦属性的天性。它只是改变了你通知的无论任何属性。当决定任何给一个UI设计动画时,问一下自己你想要确切地看到什么——经由动画的中途,以及计算出如何设置需要的属性——从而可以捕获中途的点。如果你把这应用到动画进程:从一个水平的StackPanel转换为垂直的,这明显会有一个问题。你不能在StackPanel上设置一个属性,使得它在水平布局和垂直布局的中途显示什么。如果你不能这么做,那么动画系统也不能!(如果你想达到这种类型的效果,你可以使用Canvas,它允许在任意位置放置的元素。你可能需要手动的为每个元素设置动画中的位置和大小。)
在我们详细看到动画的任意部分之前,让我们检查一个简单的例子。示例8-1显示了包含一个单独的红色椭圆的窗体标记。这个椭圆元素的Height被设置为100,但是他没有直接声明一个Width属性。替代的,Width属性由一个动画决定。椭圆会在一段时间内改变它的宽度。
示例8-1
动画声明在Window.Storyboards属性中。Storyboard时一个动画的集合,用于调整多个动画。当动画定义在标记中时,它们总是出现在Storyboard中,尽是在简单的例子中——Storyboard中只有一个动画。
这个示例中的动画包含两部分,SetterTimeline和DoubleAnimation。SetterTimeline决定了通过TargetName属性设计了什么样的动画,这就涉及了椭圆的x:Name属性。它的Path属性指出了椭圆的Width是有动画效果的。
#Path属性需要被设置动画的属性和定义了该属性的类的名称。这是因为属性并不总是必须被它们应用到的类定义,你可能想要为附属属性设置动画,如Canvas.Left。为了一致性,你可能需要总是详细的指明类和属性,即使属性是目标对象的一个成员。
内嵌到SetterTimeline的DoubleAnimation决定了被设置动画效果的属性在一段时间内如何改变。“Double”在DoubleAnimation中的意义是,被设置动画效果的属性是Double类型的,而不是Int32、Point、Size或其它类型。并不是所有的类型以相同的方式设置动画效果。例如,Point是一个二维的值,意味着我们可能想要控制它的动画外观——这对于一个一维的类型如Double是没有意义的。Ellipse.Width属性——我们在这里为它设置动画——是Double类型的,因此我们必须使用DoubleAnimation。
示例8-1设置了From属性为10,To属性为300,以及Duration属性为0:0:5。正如你可能猜到的,这意味着Width起始于10,并且逐渐改变到300,在5秒的时间里。RepeatBehavior属性被设置为Forever,指出一旦动画到达了终点,它应该回到起点并且不确定的重复。图8-1显示了这个椭圆在动画中是如何显示各种形状的。
图8-1
#确保你在动画中详细指定了任意Duration的所有3个部分的值。值2被解释为2小时。如果你的意思是2秒,你必须使用0:0:2,意味着0小时0分2秒。
正如我们看到的,这里有很多正确选择属性是如何改变的方式,确保它是如何直接支持曲线运动以及速度上的改变,但正是这些方式,需要WPF在正确时间设置正确的值。
8.1.1可设置动画效果的属性
大多数可以影响元素外观的属性都可以设置动画效果。这里有三种需求,是一个属性能够被设置动画效果:属性必须是一个依赖属性,一个合适的动画类型必须是可利用的,以及目标元素必须派生于FrameworkElement。
动画系统依赖于依赖属性系统——可以自动更新属性值。第9张详细描述了依赖属性。大部分WPF元素属性都是依赖属性。
第二个需求是,属性的类型必须有一个相应的动画类型,涉及到像DoubleAnimation或PointAnimation的类型。WPF为大多数使用属性影响外观的类型提供了动画类型。唯一的例外是枚举类型。例如,StackPanel使用的Orientation类型,就没有相应的动画类型。这是有意义的——当你认为这个枚举只支持两个值,Horizontal或Vertical。这里没有办法表示两种选择之间的中间值,因此动画并不被支持。
#你可以编写自己的动画类型。如果你写一个控件——属性中包含这些自定义的可以被设置动画效果的类型——这通常是有用的。技术上讲,没有什么可以阻止你为不支持动画效果的系统类型写一个动画类型。例如,理论上你可以写一个OrientationAnimation。然而,它在使用上是有限制的,因为在动画期间任意给定的时刻,是不能要求设置属性为这两个被支持的值:Horizontal或Vertical。没有办法在两个值之间设置平滑的动画,因此你能做到的最好是在图画的中途从一种转换到另一种。
上面列出的最后一个需求是,动画的目标元素必须是一个FrameworkElement。这通常不是一个问题,因为WPF的用户界面元素都派生于这个类。然而,这有时是你可能希望设置动画的数量,实际上不是FrameworkElement的属性,而是属性的内嵌属性。例如,示例8-1中的椭圆是红色的,但是我们可能想要为这个颜色设置动画。Fill属性的类型是Brush,xaml编译器解释这个Red值作为SolodColorBrush属性的简写。示例8-2显示了这个标记的完整版本。这是准确等价于在示例8-1中声明的单线条的Ellipse。
示例8-2
这个完整的扩展版本使得在一定时间内改变一个椭圆的颜色更加清晰,我们需要为SolodColorBrush属性设置动画。但这里有个问题。SolodColorBrush不是一个FrameworkElement,因为笔刷并不是用户界面树的一部分。笔刷是非常轻量的描述元素外观的对象,而不是作为凭借自身能力的可见元素。你不能为Brush分配一个x:Name,同时它不能作为一个动画的直接目标。
这可能看起来是一个相当严格的约束。幸运的是,存在一种解决方案。动画可以把内嵌属性作为目标。SetterTimeline的Path属性可以影响到属性内部的子对象,我们可以使用它来为笔刷或其它类似的轻量类型的属性设置动画。
示例8-3显示了任何为椭圆的颜色设置动画。
示例8-3
这个动画需要一个FrameworkElement作为它的目标,因此它的TargetName再一次指向了椭圆。SetterTimeline.Path属性唯一标志了Ellipse.Fill属性,以及指出了它想要深入到这个SolodColorBrush属性,以及设置了内嵌的Color属性。这个ColorAnimation接着详细指定了每7秒颜色会在红色和紫色之间渐变。
#如果你使用了低级别的几何体类型(见第7章)来生成绘图,你就需要使用示例8-3中显示的技术,因为Geometry并不是直接派生于FrameworkElement的。你可以在Path的Data属性中为几何体设置动画——通过为Path指定动画目标和使用SetterTimeline的Path属性来详细指定内嵌在Path中的几何体的属性。同样的技术还用于为3-D基础设置动画。
SetterTimeline和各种动画类型都是timeline的例子。Timeline是动画的基础,因此我们将要详细的看一下这些技术。
8.2 Timeline
Timeline代表了时间的延伸。它通常还描述了一个或多个在这段时间所发生的事情。例如,在前面章节描述的动画类型,都是Timeline。可哦率这样的DoubleAnimation:
<DoubleAnimation From=”10” To=”300” Duration=”0:0:5” />
正如Duration属性指出的,这代表了一个5秒的时间长度。所有类型的Timeline总是有一个开始时间和一个持续时间。如果没有详细指定开始时间,它默认为0:0:0,但是它可以使用BeginTime属性设置。开始时间可以是相对于各种引用帧的,如当一个页面被解析的时候;或者是相对于另一个Timeline,依赖于Timeline在哪里定义的。
#你还可以设置BeginTime为null。(在xaml中,这是通过{x:Null}标记来实现的。)这就指出了Timeline并没有一个固定的开始时间,但是可以被某个事件触发。后面我们将会看到任何触发一个Timeline。
不仅表示一个特定的时间延伸,特定的timeline还表示一段时间内某个值的改变。在timeline的开始,值为10,在结束,值为300。DoubleAnimation是很多内建动画类型的一个。
8.2.1 动画时间线类型
WPF提供了一组动画类——符合相同的基本样式。因此当你必须选择一个动画类型——这个类型匹配被设置了动画的属性类型,,动画类型的行为是相当一致的。
例如,Double类型的属性可以被设置动画——通过使用DoubleAnimation,而为了一个Color属性,你可以使用ColorAnimation。这些类型都允许遵循相同的TypeAnimation命名转换,正如你从表8-1中看到的。
表8-1
所有的内建类提供了To和From属性来设置开始值和结束值。可选择的,很多还提供了By属性,这将允许你修改属性而不用知道它当前的值。如果示例8-4被应用到一个对象的Width,这将使得它增加100px的合理宽度,不管初始的Width值为多少。
示例8-4
你可以设计动画为交叠的——通过开始一个在另一个结束之前。你甚至可以这么做,通过动画为同样的属性设置目标。如果动画使用了To和From,最后一个动画会覆盖其它的。但是如果动画使用了By,它们的效果是累积的。净结果是独立的动画效果的总和。
To和from属性在是示例8-1中所有的动画类型上都是有效的。(By属性不能够不是在所有的类型上都有效,因为有一些,如Color,这将没有任何意义。)当然,这些属性的类型匹配了ColorAnimation目标类型,这些属性将将会是Color类型的——当在DoubleAnimation上它们是Double类型的。在所有情形中,本质行为是一样的。动画简单地在其持续时间内添加了新值,从一个值到另一个值。
默认的,这种添加新值是线性的。这个值以常速在整个动画的持续时间内改变,然而,你可以通过AccelerationRation和DecelerationRation属性来改变这个值。这些属性允许你向动画提供一个“软”的开始和结束。如果你设置了AccelerationRation为0.2,这个动画的改变速度将要从0开始,它会逐渐地加速到全速,在timeline的第一个五分之一的持续时间里。如果你设置DecelerationRation为0.1,动画将减速直到停止,在timeline的最后一个十分之一的持续时间里。
这是相当不寻常的——想只孤立地使用一个动画。你将经常想对多个相关的动画——一起工作以产生所需要的可视化效果——进行分类。为了支持这一点,timeline可以被分组和嵌套。
8.2.2 层次
Timeline经常排列在一个层次。我们已经看到SetterTimeline——作为DoubleAnimation的父一级,但这是普通的使更深层的嵌套,来管理更复杂的动画。我们使用ParallelTimeline来实现,这是一个timeline类型,作为分组其他timeline使用。
子一级Timeline的开始时间是相对于它们的父一级的。因此BeginTime的0:1:0并不一定意味着1分钟。作为子一级Timeline,它意味着1分钟,在它的父一级开始之后。
示例8-5使用ParallelTimeline对一些动画进行分组。
示例8-5
这个动画按顺序修改了每个按钮的高度,放大了按钮,然后收缩到它的元素大小。图8-2显示了这个动画的中途的一个情形。
图8-2
Storyboard的结构并不是像这个简单的序列所建议的那样直接。它有一点人为的结构,为了在层次上显示timeline的效果。每一个按钮都有一个SetterTimeline和DoubleAnimation来为它的高度设置动画。前两个按钮是足够简单的,它们都是ParallelTimeline的子级,而且SetterTimeline.BeginTime属性被各自设置为0:0:0和0:0:1。这意味这第二个按钮伸展和缩短比第一个按钮晚1秒。然而,后两个按钮有点令人惊讶的。它们的BeginTime属性也都分别设置为0:0:0和0:0:1。虽然这样,它们并没有和头两个按钮同时伸展和缩短。图8-2显示了第四个按钮,和第二个按钮具有同样的大小。
这些按钮的动画从左到右一个接着一个执行。即使后两个按钮和前两个按钮有相同的BeginTime值,这仍然是可以工作的,原因是它们嵌入到了另一个ParallelTimeline中,这将轮流嵌入到顶级的ParallelTimeline中。后两个动画的BeginTime属性是关联到这个内嵌的ParallelTimeline,而不是顶级的ParallelTimeline。这种内嵌了ParallelTimeline的动画有一个值为0:0:2的BeginTime,意味着它直到顶级timeline2秒后才开始运行,在前两个按钮被设置动画之后。这依次意味着这些内嵌按钮的动画直到这是才开始运行。
图8-3说明了示例8-5中Storyboard的结构。每一个timeline(包括SetterTimeline和DoubleAnimation)都表示为一个水平线,在开始和结束的位置都有一个圆点。它的水平位置指出了,当timeline按照上面显示的刻度运行时,这个timeline向右显示的越远,,它运行的越晚。(这个刻度相对于应用程序开始的时间)
这种有层次的结构使得改变很容易——当一个动画序列开始时,而不用必须遍及这个序列的任意细节。因为每个BeginTime属性指向到它的父一级,我们可以通过调整这个单独的BeginTime来移动序列。例如,我们可以改变——当后两个按钮通过只改变它们父一级的BeginTime的方式设置动画。一种绘制的方式是想象在图8-3中通过一个被标记为BeginTime的垂直箭头获取这个结构。如果你移动一条线从一边到另一边,任何在这条线下的事物都会跟着移动。
在这个示例中,唯一的BeginTime——相对于流逝的时间,是顶级的不具备父一级的ParallelTimeline。默认的,顶级的ParallelTimeline会使用“全局应用程序时钟”作为它的参考。这个“全局应用程序时钟”开始运行于应用程序首次解析标记或加载xaml,因此任何这样timeline的BeginTime是相对于应用程序首次加载UI的时间。
#“全局应用程序时钟”并没有等第一个窗体的打开。当UI初始化的时候,它才开始运行。这意味着你的动画在显示这个窗体之前开始计时是可能的。极端的例子是,动画可以在窗体出现之前结束。如果你想动画只在窗体出现之前开始,你可以给它们一个空的BeginTime,以及使用在本章后面讨论的代码后置技术。我们希望这个样式的版本可以更容易地设置动画的开始时间——相对于UI的外观。
注意到在图8-3中,图表的右手边,所有的四个激活的timeline都到达了一个终点在一个严格相同的瞬间。这不仅仅是坐标。这甚至不是小心编码的结果,如果你看一下示例8-5,你可以看到,只有带着明确的延续时间的timeline才是DoubleAnimation元素。所有其它的timeline自动获取它们的延续时间。
8.2.3 延续时间
如果你没有提供一个Duration属性,timeline会尝试计算出它的延续时间。这会基于它的子级别的延续时间,设置它自己的延续时间,使之足够长以容纳任何最后一个结束的timeline。
考虑一下示例8-6。
示例8-6
每个DoubleAnimation都有一个显示的Duration,但是两个SetterTimeline元素没有。它们都有一个隐式的延续时间——由它们的子级DoubleAnimation结束的时间决定。在这个例子中,这意味着这两个SetterTimeline元素都有0.2秒的延续时间。
父一级ParallelTimeline是有趣的,因为它包括两个SetterTimeline元素,它们都有一个隐式的0.2秒延续时间。然而,这个timeline的有效延续时间并不是0.2秒;而是1.2秒。还记得一个隐式的延续时间并不简单的是最长的子级timeline的长度,而是由最后一个timeline结束的时间决定。第二个SetterTimeline对象的BeginTime值为0:0:1,也就是在它的父一级ParallelTimeline开始后1秒。由于这个子级的延续时间是0.2秒,它就直到它的父一级开始1.2后才会结束——意味着它的父一级有一个隐式的1.2秒延续时间。
#所有的timeline都提供一个AutoReverse属性。如果被设为true,timeline将会反过来运行——在它到达终点时。这就加倍了它的延续时间。这会产生轻微地困惑,当与一个显示Duration协力工作时。一个带有显示0:0:0.2的Duration以及AutoReverse设置为true,有一个有效的0.4秒延续时间。这就是为什么图8-3中的timeline都比你所希望的长一些。
一般而言,显示延续时间机制工作良好,可以为你节省一些努力。然而,有一些情形会引起惊讶。确实,它会引起一个轻微的小故障在一个早期的示例中。如果你测试了示例8-5,你会注意到这里有一个仅多于0.5秒的间隙在每个按钮伸展和收缩之间,除重复序列以外。在第四个按钮结束收缩和第一个按钮开始伸展之间没有间隙。这个小故障在图8-3中是可见的。
你可以看到每个DoubleAnimation以一个整秒数在序列之间。第一个按钮马上就有了动画效果,第二个在1秒之后,第三个在2秒之后,第四个在3秒之后。但是因为这个动画会在3.4秒后重复,这引起了一个简单的不平衡的感觉。如果在4秒后重复,这将会更好。
有很多种方法来修复这个问题。我们可以仅设置顶级ParallelTimeline的延迟时间为4秒。更巧妙地,我们可以设置第四个SetterTimeline的延迟时间为1秒。这将隐式地扩展它的父一级ParallelTimeline为2秒长——使得顶级ParallelTimeline为4秒。尽管这个方法看上去不太直接,它避免了硬编码顶级timeline的延迟时间,意味着如果你后来添加了更多的子级动画,你不会需要返回来调整顶级的延迟时间。
8.2.4 循环
默认的,一个timeline开始于由它的BeginTime详细指定的偏移,并停止于当它到达延迟时间时。尽管如此,所有的timeline都有一个RepeatBehavior属性,支持它们重复一次或更多次在到达它们的终止点之后。
我们已经在示例8-5中看到这一点,在顶级ParallelTimeline的RepeatBehavior设置为Forever之处。这有一个对顶级元素充分直接的意义:它们会在UI运行的时候重复。对于内嵌的timeline,这并不是非常简单的。当一个内嵌的带有RepeatBehavior设置为Forever的timeline到达延迟时间的终点时,它会回到起始点以及继续重复直到时间的终点,但是只为“the end of time”的小值。
记住任何嵌入timeline的BeginTime都是相对于它的父一级。实际山,它的全部时间视图都由它的父一级决定。因此对于一个内嵌的timeline,“the end of time”意味着它的父一级的延迟时间。示例8-7显示了一个值为Forever的RepeatBehavior可以在一小段时间后被切分。
示例8-7
在这个示例中,按钮的背景被设置了动画效果:在红色和黄色之间渐变。它使用了一个ColorAnimation,带有一个值为Forever的RepeatBehavior。运行这段代码,在2秒内显示了一个红色的按钮,渐变到黄色,并返回来一次,再一次渐变到黄色,然后突然回到红色,并永远保持这样的方式。这2秒的延迟由SetterTimeline.BeginTime为0:0:2导致。这个动画在一个半循环(3秒)后被切割,因为顶级的ParallelTimeline有一个显示的0:0:5延迟时间。一旦达到了这一点,timeline和所有它的子一级都会结束,动画也不再有效了,以及按钮反转到它的原始颜色。
图8-4显示了示例8-7中的timeline结构。正如你看到的,SetterTimeline在2秒后开始,因为它的BeginTime为0:0:2。ColorAnimation.Duration属性被设置为0:0:1,但是这并不是一个有效的延迟时间。首先,AutoReverse属性被设置为true,加倍了有效的长度。此外,因为它的RepeatBehavior值为Forever,它将会执行在它被允许的时候,因此它的有效的延迟时间只是被它的上下文约束。
SetterTimeline容器并没有一个显示的延迟时间,因此它获取不确定的有效的ColorAnimation延迟时间。但是这些都被它的父一级ParallelTimeline切割,带有它的显示5秒延迟时间。
#如果你使用设置为Forever的RepeatBehavior,并没有在父一级进行切割——带有显示的延迟时间,隐式的父一级元素的延迟时间将是不确定的。从示例8-7中的ParallelTimeline移除Duration属性,允许颜色动画不确定地运行。
RepeatBehavior属性还支持有效的重复。你可以指示一个timeline来重复一个特定长度的时间或一个固定数量的迭代。示例8-8显示了这两个技术的例子。
图8-4
示例8-8
示例8-8中的ColorAnimation的RepeatBehaior值为3x。这指出了动画应该重复3次然后停止。有效的动画结束延迟时间为3秒,三倍时间比没有使用重复的情况。DoubleAnimation的RepeatBehavior值为0:0:2。这意味着这个动画将会重复直到2秒过后。
8.2.5 填充
很多动画有有限的延迟时间。这引发了一个问题:当动画完成后,被设置了动画的属性将会发生什么?到目前为止出现的示例都有点鬼鬼祟祟的——我们看到的所有动画或者是永远重复或者是设置属性回到它的原始值在它们结束之前。示例8-9没有使用这些诡计。
示例8-9
示例8-9非常类似于示例8-1。这两个示例都设置了动画,使椭圆的大小在5秒内从10增加到300。这里有5个不同点。示例8-9只运行了动画一次。它忽略了示例8-1中的RepeatBehavior。它还在开始之前等待2秒。
当你运行这个程序时,这个椭圆会初始化为不可见的。2秒后,它会出现,然后逐渐扩展——像之前那样。在5秒动画的终点,这个椭圆保持着它的原始大小。我们可以添加一些代码来更详细地看一下,正如示例8-10所示。
示例8-10
示例8-10创建了一个timer,每秒2次调用我们的OnTimerTrick函数。(DispatcherTimer是一个特殊的WPF的计时器,保证了在能使UI安全工作的上下文中调用我们的计时器。这意味着我们不需要担心是否在安全的线程上。参见附录C获取更多WPF中线程的信息。)在每个计时器的tick中,椭圆的宽度将使用Debug类打印出来。运行这个程序在vs2008中,我们可以在“输出”面板中看到这些消息,如下:
以下省去若干行代码。。。。。。
这就说明了2点。首先,不要依赖DispatcherTimer特别精确于它什么时候回调的,尤其是如果你运行在调试器中。其次,在动画运行前,椭圆报到的准确宽度为NaN。这是Not a Number的简写,同时说明了属性的宽度并没有一个值。
#NaN是由Double浮点类型支持的一个特殊值。对于WPF这不是稀奇的。IEEE标准中的浮点类型为积极的和消极的无限值定义了特殊值,以及这个“not a number”值。NaN经常出现于可疑的操作,如尝试0除0,或者从无限值中减去有限值。
虽然NaN是一个标准值,WPF在这里使用它有一点不寻常。它担当着一种标记值,指出了一个属性没有被设置。
我们不应惊讶于椭圆初始没有宽度,由于我们没有直接在标记中设置椭圆的宽度属性。我们使用动画间接地设置,因此Width属性只有一个意味深长的值,一旦动画开始。我们修复这个问题,通过设置椭圆的宽度。
<Ellipse x:Name=”myEllipse” Height=”100” Fill=”Red” Width=”42” />
做了这样的改动,这个椭圆在动画开始时是可见的。它初始化为42px宽度。(如以前,一旦动画结束,它是300px宽度。)调试器反映了这一点,在动画的开始显示了宽度为42的值,取代以NaN:
以下省去若干行代码。。。。。。
这是动画的默认行为——当它们到达终点时,它们最后的值持续请求——只要它们的父一级timeline持续为活动的。在一些环境中,这可能并不总是你需要的行为,你可能想确保这个属性返回它的原始值。即使当它是你需要的行为的时候,这看起来并不是直接了当的。
当一个动画到达它的延迟时间的终点时,这个动画并没有完全结束。我们看到动画的最后值——应用到上面的示例中,原因是这个动画仍然是活动的,即使它已经到达延迟时间的终点。这个模糊的地带——在动画延迟时间的终点和它最后的钝化之间,被称为“填充周期”。
所有的timeline都有一个FillBehavior属性,详细指出了在timeline到达它的有效延迟时间之后发生了什么。默认值为HoldEnd,意味着这个动画将会继续应用它的最后值直到UI关闭,除非一些事引起它为无效的。可选择的FillBehavior,显示在示例8-11中,为Deactivate。这使得这个动画无效——一旦它到达了延迟时间的终点,意味着相应的属性将会反转它的值在动画开始之前。
示例8-11
注意到,不同于RepeatBehavior,FillBehavior属性对timeline的有效延迟时间没有影响。FillBehavior.HoldEnd只会做一些事——如果父一级timeline运行时间比正被讨论的timeline的延迟时间长。示例8-12显示了这样一个场景。父一级SetterTimeline有10秒的延迟时间,当它的子一级有5秒的延迟时间,剩下一个5秒的“填充周期”。子一级FillBehavior并没有被设置,因此它默认为HoldEnd。
示例8-12
图8-5说明了这样一对timeline。由于父一级timeline的FillBehavior为Deactivate,它在其自然的延迟时间终止点会失效。当父一级失效时,所有的子一级都会失效,因此这会引起子一级的“填充周期”到达终点,意味着相应的属性将全都回复到在动画开始之前的值。
图8-5
如果顶级timeline有一个默认为HoldEnd的FillBehavior,它的“填充周期”将是不确定的。这就一次意味着它的子一级“填充周期”也会是不确定的。示例8-13显示了这样一个有层次的Timeline。(这和示例8-9具有相同的一组Timeline)
示例8-13
这里,DoubleAnimation和SetterTimeline都有一个显示的FillBehavior,因此它们默认为HoldEnd。由于SetterTimeline是顶级的timeline(没有父一级),这意味着它的“填充周期”是有效不确定的。这就依次表明DoubleAnimation也有一个不确定的“填充周期”,正如图8-6中的双向箭头指出的。示例8-13中带有Storyboard的结果是,椭圆的宽度从10增长到300,并保持在300。
图8-6
默认的填充行为意味着动画典型地结束于一个不确定的“填充周期”。这通常导致了渴望的行为:一旦动画的延迟时间结束一个动画的最后值就是在那个适当的的位置保持的值。然而,它有一个令人吃惊的结果——如果你一个接着一个应用多个动画到同样的属性,而且这些属性使用By属性。例如,你可能有一个动画:在几秒内向右移动一个对象,接着另一个动画将会在它的“填充周期”内。这意味着两个动画是同时激活的。如果第二个动画使用了From和To,这将复写第一个属性。但是如果它使用了By属性,这个动画将会累积。动画系统会把第二个动画的效果添加到第一个动画。
幸运的是,在这种情形中,这个行为的终结结果可以是你想要的——当使用By属性时,第二个动画的起始点是第一个动画的最后值。
8.2.6 速度
有时你可能发现你想改变动画运行的某部分的速度。对于一个简单的包含单独元素的动画,你可以只改变延迟时间。对于一个更复杂的动画,有很多timeline组成,这将变得冗长的
——来手动调整每个延迟时间。一个简单的解决方案是包装时间,有效地在任何timeline上使用SpeedRatio属性。
SpeedRatio允许你在动画向后播放处改变速率。它的默认值为1,意味着所有的timeline提前一秒——为实时流逝的每一秒。然而,如果你修改了若干timeline中一个SpeedRatio为2,这个timeline以及它的子一级都会提前2秒——为实时流逝的每一秒。
SpeedRatio是相对于父一级timeline前进的速率,而不是绝对的流逝时间。这变得很重要——如果你在多个地方详细指出速率。示例8-14显示了一个示例8-5动画的修改后的版本,带有一个SpeedRatio属性,添加到某些timeline中。
示例8-14
图8-7显示了这些改动的效果。顶级timeline的速度没有详细指出,因此它默认为1,并按正常的速率前进。它的第一个SetterTimeline子一级也是这样的。第二个SetterTimeline的SpeedRatio为2。这没有影响这个timeline开始的时间。它的BeginTimeline是相对于它的父一级的,因此依赖于它的父一级速度。但是这个timeline的内容,DoubleAnimation,将会运行以正常速度的两倍,因此它就像是这个动画的延迟时间设置为0.1而不是0.2。结果是第二个按钮扩展和收缩在第一个按钮扩展和收缩的一半时间内。
顶级timeline的后两个子一级是一个ParalletTimeline元素,带有SpeedRatio为4的属性。这将是4倍于它工作中的子一级timeline。可是,它的子一级是带有SpeedRatio为0.25属性的SetterTimeline。因此,这个timeline——设置在第三个按钮的动画,将会以正常的速度运行。下一个内嵌的SetterTimeline——控制着第四个按钮,它的BeginTime设置为0:0:1,但是因为它的父一级SpeedRatio为4,它将会开始于这个timeline中的1/4秒,引起它轻微交叠于前一个动画,如图8-7所示。它的速度为0.5,但这是相对于它的父一级速度为4,意味着这个timeline以双倍速度运行。可是,它的子一级是速度为0.125的DoubleAnimation。这里有3个SpeedRatio值在运行中。这里,内嵌的ParalletTimeline、SetterTimeline和DoubleAnimation的速度分别为4、0.5和0.125。联合这些,我们得到了0.25。因此最后的结果是第四个按钮的动画效果为四分之一的正常速度,因此是延迟时间的4倍。
图8-7
到目前为止,本章所有的示例,你可能想知道为什么这些动画都在storyboard中。这是可论证地,对于ColorAnimation更加简单,以间接地嵌入到示例8-2中的SolidColorBrush,取代以被隔离到Window.Storyboard属性中,这里需要一个SetterTimeline元素来指出它应用到哪个元素。对于非常简单的动画,使用storyboard可能有一点麻烦,但是一旦你想要同时为多个属性设置动画,就会保持这些动画异步激活的挑战。Storyboard存在以解决这个问题。
8.3 Storyboard
Storyboard是动画的集合。如果你使用了标记,所有的动画必须要被定义在一个Storyboard中。(在代码中创建隔离的动画对象,这是可能的,参见本章后面部分。)一个动画的结构通常是不同于设置了动画的UI的结构上。例如,你可能想要来两个单独的用户界面元素在同一时间被设置动画。因为Storyboard将动画从有动画效果的对象中隔离出来,Storyboard是自由地反射这样的连接,即使这些元素被设置了对象,可能被定义在完全不同的文件部分中。
示例8-15显示了包含了两个椭圆的用户界面的标记。
示例8-15
这些椭圆是互不邻近的,但是它们的宽度都以异步的方式设置了动画。这种异步被反射在Storyboard的结构中:这两个动画都是内嵌在同样的ParallelTimeline元素中,指出了这些动画都在相同的时间运行。一个从10到300的动画,另一个是从300到10,因此StackPanel中这三项的总宽度保持为恒定的。
Storyboard必须运行以三种位置中的一种。它们可以放置在Style中、ContentTemplate中、或者顶级元素中。顶级元素是Window和Page,或者派生于此的类。
#所有的用户界面元素都有一个Storyboard属性,继承于基类型FrameworkElement。你可能想这意味着你可以添加一个Storyboard到任意一个元素。这于当前是不被支持的。Storyboard只会工作于:Style中、ContentTemplate中、或者顶级元素中。
把动画放入样式的Storyboard中的能力,使你通过样式系统应用动画。这可是很有用的——如果你想使用同样的动画在很多地方。通过把动画放入样式,而不是一个Window或Page,你要避免复制和排除次要矛盾的可能性。示例8-16显示了带有Storyboard的一个样式。
示例8-16
这些动画并没有做什么显著不寻常的事情。它们只是改变了一对按钮的大小和颜色,正如图8-8所示。可是,注意到SetterTimeline元素并没有详细指出TargetName。这是因为使用一个样式Storyboard,这里有一个隐式的目标:该样式应用到的元素(这种情形下是一个Button)。同样,如图8-8所示,因为这是一个样式,它定义的动画应用到所有的按钮。
图8-8
如果你为控件定义了一个模板,它可能包含了——没有直接响应到元素可你又想设置动画的属性——的样式,例如,图8-9显示了两对按钮。在顶行,这些按钮显示为自定义的可视化——带有圆形的静态外观。底部按钮是相似的,但是一个放射性的填充被添加到按钮中,并含有一个内部的光源。我们可能想为这个光源设置动画,使这个按钮逐渐地跳动。
图8-9
这个按钮的类型并没有提供我们用来表示光源颜色的属性,因此为了给这个光源设置动画,这个动画需要为控件模板中详细指定的元素设置目标。在这种情形中,我们可以把动画放入模板的Storyboard中,而不是样式的Storyboard中。如果你设置了x:Name属性在模板中相应的元素上,你可以接着在动画中用TargetName引用它。示例8-17显示了图8-9的标记。
示例8-17
大多数模板是静态的,但是光源被设置了动画。注意到x:Name属性带有一个glow值在相关的形状上。这个动画在模板的Storyboard中,正如你希望的,它将包括一个单独的SetterTimeline,带有一个被设置为glow的TargetName。这个Path有点复杂,简单的因为我们为一个特定的带有笔刷的GradientStop设定动画。记住轻量级对象——如笔刷或者梯度停止,不能被直接设置动画。替代的,我们不得不生成相应的完整的UI元素。动画设置目标同时使用Path属性来向下导航到我们希望改动的属性。
#这个特定的Path引进了一个我们之前没有看到过的新样式:[0]。这个[index]语法用于指出一个项在集合的一个特定的偏移位置。
正如我们在第五章看到的,样式和模板都可以定义触发器以允许属性被设置为自动依照某一个刺激。例如,你可以触发任何Storyboard中的动画——当一个特定的事件发生时。
示例8-18为一个按钮显示了一个样式,带有一个简单的模板——仅绘制了一个矩形在按钮内容的周围。这个模板的Storyboard包含了两个动画。第一个退变颜色到PeachPuff,然会再返回来,另一个则在矩形轮廓的厚度上振荡。注意到,这两个按钮都有一个设置为{x:Null}的BeginTime。这防止了在应用程序开始的时候这些按钮会自动运行动画。
示例8-18
这个动画被模板中的triggers部分中的Eventtrigger元素全部触发。首先会响应到这些按钮的Click事件。(这个事件由按钮的基本类定义,这里是ButtonBase.Click。)无论何时点击这个按钮。这将引起clickTimeline动画的运行,使按钮退变颜色到PeachPuff然后再回到被点击时的样子。
另一个动画有两个Eventtrigger元素,一个用于当鼠标进入控件的时候,另一个用于当鼠标离开控件的时候。这是因为线条宽度的动画会永远重复,如果这只有一个触发器,开始动画在鼠标进入控件的时候,这个动画将会开始而不会停止,由于这是一个顶级的timeline。因此我们需要1秒钟的Eventtrigger,响应到MouseLeave事件——使用StopAction来停止这个动画。
#这个示例的鼠标事件命名为Mouse.MouseEnter和Mouse.MouseLeave。这是有一点不寻常的,当事件被命名为定义它们的元素。这些事件是继承于UIElement基类,因此你可以希望它们被命名为UIElement.MouseEnter和UIElement.MouseLeave。可是,这些由UIElement提供的事件只是底层的附属事件外面的包装。底层的事件由System.Windows.Input命名空间的Mouse类定义,这是为什么事件名以Mouse.UIElement开始,简单而便利的包装了这些附属事件作为标准.NET事件。
每个Eventtrigger可以有你想要的任何事件,因此你可以一次性开始或废弃一些动画。
这通常是非常便利的使WPF自动开始和停止你的动画——在事件发生时。这就意味着你不需要写任何代码。可是,这并不总是一个适当的事件来用作触发器,因此在程序上开始一个动画,有时是很有用的。
8.3.1 使用代码加载动画
为了从代码上开始一个动画,理解timeline和时钟之间的不同是必要的。正如我们已经看到的,一个timeline层次是在一段延伸的时间内发生的一个或更多事情的描述——但这只是一个描述。timeline层次的存在,带有一个SetterTimeline和一个DoubleAnimation,并不是足够引起动画的发生。表现这个动画的工作由一个或多个时钟完成。
时钟是一个在运行期创建的对象,对timeline中的当前位置保持跟踪,执行timeline定义的无论什么动作。如果你回想到timeline图表,如图8-7,时钟就是知道我们在时间的那个位置的图表上端。
#timeline和时钟的关系并不像代码和多线程的关系。可执行代码定义了要表现什么操作,但是一个线程被要求执行代码。同样的,一个timeline描述了在一段特定的时间长度发生了什么,但是一个时钟被要求运行这个timeline。
WPF自动创建时钟。在创建一个用户界面的时候,它顺便访问相应的Stroyboard属性和为每个timeline创建时钟。如果一个带有storyboard的样式或模板被使用在多个地方,每个实例都有它自己的一组时钟——支持动画独立地运行。这是无妨的,否则,如果你有一个动画在鼠标进入按钮的时候运行,它会为屏幕上所有的按钮同时运行,这将不会是十分有用的。
通常,顶级timeline的时钟是自动开始的,基于他们的BeginTime。可是,如果你详细指定BeginTime为{x:Null},这个时钟将不会开始,因此这个动画不会运行。我们在前面的章节看到,如何使用触发器来加载动画。触发器中的BeginAction只是告诉WPF在触发器发生时,开始相应的timeline的时钟。你也可以编写代码来开始动画。
为了亲自开始一个动画,我们需要得到它的时钟。这些代码需要找到timeline的时钟,看上去有点不同的,取决于你是否处理timeline在一个Style、一个模板、还是一个顶级元素中。
示例8-19
这个动画将按钮的颜色改变为红色,而且还要返回。由于这个动画的BeginTime被设置为{x:Null},并且没有任何自动的触发器,我们需要写一些代码来运行它们。我们将这么做——通过添加一个Click句柄到示例8-19中的按钮。示例8-20显示了包含这个Click句柄的代码。
示例8-20
这个句柄获取了动画的timeline,接着获取它的Clock。它使用了时钟的控制器来运行这个动画。图8-10显示了这个运行中的动画。
图8-10
如果这个动画存在于一个样式中,这段代码工作得有点不同。示例8-21显示了带有动画的一个样式。(这个动画有和前一个示例同样的效果;它只是以一种不同的方式被应用。)
示例8-21
这个Click句柄必须要修改,因为动画现在定义在样式中。示例8-22显示了新的句柄。
示例8-22
当然,如果样式定义了一个模板,我们可能希望直接为模板的一部分定义动画。在这种情形中,动画会存在于模板的Stroyboard中,而不是样式的Stroyboard中。示例8-23显示了包含了一个带Stroyboard的模板的样式。
示例8-23
虽然事实上,动画现在嵌入到样式模板中,而不是样式中,我们仍可以和之前同样的方式加载这个动画——使用示例8-22所示的代码。图8-11显示了结果。
图8-11
在所有的这些示例中,我们使用了时钟的ClickController。这就为控制操作提供了可编程的接口,如开始、暂停、停止、倒带等等。这里我们使用了Begin——立即开始了动画。注意到我们只需要为顶级timeline开始这个时钟,子时钟将会被开始于恰当的时间,作为运行父一级的结果。
8.4 关键帧动画
到目前为止,我们只看到简单的点到点的动画。我们使用了To和From属性或者By属性来设计动画——相对于当前的属性值。这很适合简单的动画,但是我们可以构造序列来创建更复杂的动画,这可能是非常麻烦的。幸运的是,这是没有必要的。WPF提供了动画对象,允许我们详细指出一系列时间和值。
在影视中传统的动画中,这是普通的开始——通过绘制最重要的动画步骤。这些关键帧定义了场景的基本流程,捕获了它的最重要的点。只要一旦这些关键帧是满意的,是保留的帧绘图。这些关键帧之间的图像并不要求非常创造性的输入,它们只是简单的打算添加进去,从一个关键帧到另一个。WPF优化了同样的概念。你可以考虑简单的From和To方法——等价于提供两个关键帧,一个“before”帧和一个“after”帧——WPF会为你添加这两个帧。关键帧动画简单的扩展了多个帧的概念。
作为最简单的动画类型,关键帧仍然一次性为属性设定目标。因此它们并不与传统动画中关键帧一样,每一帧组成了整个的绘图。你不能提供两个绘图并告诉WPF从一个变换到另一个。
关键帧动画类型使用了命名装换TypeAnimationUsingKeyFrames。示例8-24显示了一个简单的动画:一个弹起的矩形,使用到了DoubleAnimationUsingKeyFrames。
示例8-24
这里有两个timeline。第一个移动矩形从左到右,使用常规的DoubleAnimation,第二个通过使用DoubleAnimationUsingKeyFrames控制了垂直的位置。这控制了5个帧,详细指出了矩形的需要的垂直位置,在半秒的时间内。如图8-12所示,这些关键帧显示了这个矩形,在它的跳动的顶部和底部,伴随着中途的点比中间点稍微高一点,指出一段时间的速度渐变。WPF为我们在这些位置之间加入了新元素。
图8-12
示例8-24中每个关键帧的值都使用LinearDoubleKeyFrame详细指出。这说明了使用了线形添写。改变的速度是介于两个帧之间的常量。这就引起了运动并不是特别平滑的。这个矩形在它下降时提高速度,而速度上的改变发生在可见的“阶段”——从动画的一幕到下一幕。我们可以减少这种影响,通过添加更多的关键帧,但是这里有一条更容易的方式。不是图8-13中左边显示的简单线性插值,而是获取一个曲线插值如右边显示,提高了平滑度,而不需要添加更多的关键帧。
图8-13
为了在我们想要的动画速度上获取更平滑的改动,我们可以使用SplineDoubleKeyFrame。带有一个样条关键帧,一条贝塞尔曲线详细指出了动画值是应该如何改变的。可是,这种曲线使用的方式并不是完全直接的。正如我们在第7章看到的,贝塞尔曲线可以用于定义曲线形状。可是,使用动画,我们不能简单地定义路径。一个点沿示例中的贝塞尔曲线而行。这条曲线是一个二维的形状,但是这个动画对象仅修改了y轴,这意味着它只在一个维度上产生影响。(记得示例8-24使用了2个SetterTimeline元素,每个都对应一个维度。)
代替以定义点的路径,贝塞尔曲线在一个样条关键帧上定义了一个数学函数的形式。这个函数把它的输入理解为关键帧的流逝时间的比例。作为输出,它提供了一个数字,指出之前的和当前的值混合在一起的比例。这条曲线总是从(0,0)移动到(1,1),但是你定位这两个控制点,决定了它的形状在这些极限之间。使用关键帧的KeySpline属性设置这些值。
图8-14显示了3个动画样条的示例,控制点标记在小矩形上。记得这些曲线简单的决定了动画前进的速度。第一个“曲线”是一条直线,意味着这个动画以常速前进。这等价于一个LinearDoubleKeyFrame。第二条曲线指出了动画开始缓慢而后加速。第三条曲线显示了动画开始迅速而后减速到停止。
图8-14
示例8-25是示例8-24的对关键帧的修改版本。这个动画传递了同样的关键帧值,但是使用样条来指出动画的速度应该逐渐改变。这使得这个动画感觉很平滑,而不需要添加更多的关键帧。
示例8-25
第一帧仍然使用LinearDoubleKeyFrame,因为这里没有“before”帧以进行插值。两个“downward”关键帧使用了曲线形状——类似于图8-14中间的那个。这导致了这个动画开始缓慢然后加速,正如你希望的在一个下落对象的动画中。两个“upward”关键帧使用了曲线形状——类似于图8-14右边的那个。这导致了这个动画逐渐缓慢直到这个对象到达顶部。这就提供了一个更有力的可视化近似:关于一个真实的对象是如何运动的。
这里还有一种可利用的的插值样式:四点插值细分算法。如果你使用了这样一个关键帧,WPF根本不会真正地“插值”。它会突然跳到详细指定的值。这就易于引进中断到你的动画中,如果必要。
注意到,WPF提供了关键帧的版本——大多数动画类型都支持它,不仅是Double类型,表8-2列出了这些类型。
表8-2
8.5 创建动画过程
所有在这章使用xaml举例说明的技术,都可以在代码中使用,正如你希望的。可是,代码可以使用动画在某种程度上不可能在xaml中实现的。
在代码中创建动画需要稍微多一点的努力——比使用标记。然而,代码提供了更多的弹性。你可以在运行期计算属性,而不是在xaml中硬编码,从而支持你的动画适应环境。例如,这可能是有用的——在当前窗体的大小基于动画的参数。
使用代码一个额外的好处是我们不需要使用storyboard,替代的,我们可以创建一些被称为“本地动画”的对象。“本地动画”直接应用到一个特定的属性,这并不是storyboard的一部分。对于简单的动画,“本地动画”的使用不比storyboard简单。猜想你的标记中包含下面的椭圆:
<Ellipse x:Name=”theEllipse” Width=”50” Height=”100” Fill=”Red” />
你可以为此在代码中创建和加载一个“本地动画”,如图8-26所示:
示例8-26
我们不仅不需要把这些放入storyboard集合中我们还不需要SetterTimeline。使用storyboard,SetterTimeline需要指出哪个对象和属性被设置了动画。使用“本地动画”,你可以直接将动画添加到目标对象的PersistentAnimation集合,详细指出该属性作为集合中的索引。
8.6我们进行到哪里了?
动画可以增强应用程序的交互感。它有利于更平滑的转换——当条目出现或消失的时候。它应该,当然,被用于体验和重新着色。如果你为应用程序中的每一个事物都设置了动画,这将是令人迷惑的一团乱麻。你还应该当心不要困惑你的用户——强迫他们等待动画的完成才可以进行处理。幸运的是,WPF使得关闭动画是简单的。所有的用户界面元素保持着活动状态——当动画还在进行的时候。
动画中的关键概念是timeline。Timeline是用来描述在特定的延伸时间内发生了什么的对象。他们形成了一个层次,允许动画的不同部分的关系被表示。这种动画的可执行体由时钟控制,这将提供我们一种开始和停止动画的方式。动画可以被嵌入到顶级元素中,但是它们也可以添加到样式和模板中。在样式和模板中,动画可以被事件自动触发。如果你想在代码中创建动画,你可以在运行期配置它们,提供更多的弹性,同时你还有机会直接应用它们到目标元素,如“本地动画”。