【WPF学习】第四十九章 基本动画
在前一章已经学习过WPF动画的第一条规则——每个动画依赖于一个依赖项属性。然而,还有另一个限制。为了实现属性的动态化(换句话说,使用基于时间的方式改变属性的值),需要有支持相应数据类型的动画类。例如,Button.Width属性使用双精度数据类型。为实现属性的动态化,需要使用DoubleAnimation类。但Button.Paddin属性使用的是Thickness结构,所以需要使用ThicknessAnimation类。
该要求不像WPF动画的第一条规则那么绝对,第一条规则将动画局限于依赖项属性。这是因为对于没有相应动画类的依赖项属性,为了为该属性应用动画,可以针对相应的数据类型创建自己的动画类。System.Windows.Media.Animation名称空间已经为希望使用的大多数数据类型提供了动画类。
因为许多数据类型实际上不使用动画,所以没有相应的动画类。一个明显的例子是枚举类型。例如,可使用HorizontalAlignment属性控制如何在布局面板中放置元素,该属性使用的是HorizontalAlignment枚举值。然而,HorizontalAlignment枚举只允许从4个值中选择一个(Left、Right、Center和Stretch),这极大地限制了它在动画中的使用。尽管可在某个方向或其他方向之间进行交换,但不能将元素从一种对齐方式平滑过渡到另一中对齐方式。所以,没有为HorizontalAlignment数据类型提供动画类。可以自己为HorizontalAlignment数据类型构建动画类,但仍要受到4个枚举数值的限制。
引用类型通常不能应用动画,但它们的子属性可以。例如,所有内容控件都支持Background属性,从而可以设置Brush对象用来绘制背景。使用动画从一个画刷切换到另一个画刷的效率通常不高,但可以使用动画改变画刷的属性。例如,可改变SolidColorBrush画刷的Color属性(使用ColorAnimation类),或改变LinearGradientBrush画刷中GradientStop对象的Offset属性(使用DoubleAnimation类)。这扩展了WPF动画的应用范围,允许用户为元素外观的特定方面应用动画。
一、Animation类
根据目前为止提到的动画类型——DoubleAnimation和ColorAnimation——可能会任务所有的动画类都是以“类型名+Animation”方式命名。这种观点很接近实际情况,但不是非常准确。
实际上有两种类型的动画——在开始值和结束值之间以逐步增加的方式(被称为线性插值过程)改变属性的动画,以及从一个值突然变成另一个值得动画。DoubleAnimation和ColorAnimation属于第一种动画类型,他们使用插值平滑地改变值。然而,当改变特定的数据时,如String和引用类型的对象,插值就没有意义的。不是使用插值,这些数据类型使用一种称为“关键帧动画”的技术在特定时刻从一个值突然改变到另一个值。所有关键帧动画类都使用“类型名+AnimationUsingKeyFrames”的形式进行命名,比如StringAnimationUsingKeyFrames和ObjectAnimationUsingKeyFrames。
某些数据类型有关键帧动画类,但没有插值动画类。例如,可使用关键帧为字符串应用动画,但不能使用插值为字符串应用动画。然而,所有数据类型都支持关键帧动画,除非它们根本不支持动画。换句话说,所有具有(使用插值的)常规动画类(例如DoubleAnimation和ColorAnimation)的数据类型,也都有相应的用于关键帧动画的动画类型(如DoubleAnimationUsingKeyFrames和ColorAnimationUsingKeyFrames)。
实际上,还有一种动画类型。这种类型称为基于路径的动画,而且它们比使用插值或关键帧的动画更加专业。基于路径的动画修改数值使其符合由PathGeometry对象描述的形状,并且主要用于艳路径移动元素。基于路径的动画类使用“类型名+AnimationUsingPath”的形式进行命名,如DoubleAnimationUsingPath和PointAnimationUsingPath。
总之,在System.Windows.Media.Animation名称空间中间发现以下内容:
- 17个“类型名+Animation”类,这些类使用插值。
- 22个“类型名+AnimationUsingKeyFrames”类,这些类使用关键帧动画
- 3个“类型名+AnimationUsingPath”类,这些类使用基于路径的动画
所有这些动画类都继承自抽象的“类型名+AnimationBase”类,这些基类实现了一些基本功能,从而为创建自定义动画类提供了快捷方式。如果某个数据类型支持多种类型的动画,那么所有的动画类都继承自抽象的动画基类。例如,DoubleAnimation和DoubleAnimationUsingKeyFrames都继承自DoubleAnimationBase基类。
可通过查看这42个类快速决定哪些数据类型为动画提供了本地支持。下面是这42个类的完整列表:
BooleanAnimationUsingKeyFrames | ByteAnimation |
ByteAnimationUsingKeyFrames | CharAnimationUsingKeyFrames |
ColorAnimation | ColorAnimationUsingKeyFrames |
DecimalAnimation | DecimalAnimationUsingKeyFrames |
DoubleAnimation | DoubleAnimationUsingKeyFrames |
DoubleAnimationUsingPath | Int16Animation |
Int16AnimationUsingKeyFrames | Int32Animation |
Int32AnimationUsingKeyFrames | Int64Animation |
Int64AnimationUsingKeyFrames | MatrixAnimationUsingKeyFrames |
MatrixAnimationUsingPath | ObjectAnimationUsingKeyFrames |
PointAnimation | PointAnimationUsingKeyFrames |
PointAnimationUsingPath | Point3DAnimation |
Point3DAnimationUsingKeyFrames | QuarternionAnimation |
QuarternionAnimationUsingKeyFrames | RectAnimation |
RectAnimationUsingKeyFrames | Rotation3DAnimation |
Rotation3DAnimationUsingKeyFrames | SingleAnimation |
SingleAnimationUsingKeyFrames | SizeAnimation |
SizeAnimationUsingKeyFrames | StringAnimationUsingKeyFrames |
ThicknessAnimation | ThicknessAnimationUsingKeyFrames |
VectorAnimation | VectorAnimationUsingKeyFrames |
Vector3DAnimation | Vector3DAnimationUsingKeyFrames |
其中许多类型的含义不言自明。例如,一旦掌握DoubleAnimation类,就不在需要再分析SingleAnimation、Int16Animation、Int32Animation以及其他所有用于简单数值类型的动画类,它们都以相同的方式工作。除这些用于数值类型的动画类外,还会发现一些使用其他基本数据类型(如byte、bool、string以及char)的动画类,以及更多的用于处理二维和三维Drawing图元(Point、Size、Rect和Vector等)的动画类,用于所有元素的Margin和Padding属性的动画类(ThicknessAnimation)、用于颜色的动画类(ColorAnimation)以及用于任意引用类型对象的动画类(ObjectAnimationUsingKeyFrames)。
二、使用代码创建动画
最常用的动画技术是线性插值动画,这种技术平滑地从起点到终点修改属性值。例如,如果将开始数值设置为1,并且将结束数值设置为10,属性可能从1快速地变为1.1、1.2、1.3等,知道数值达到10.
WPF使用它所需的步长以确保在当前配置的帧率下得到平滑的动画。标准的帧率是60帧/秒。换句话说,WPF每隔1/60秒就会计算所有应用了动画的数值,并更新相应的属性。
使用动画的最简单方法是实例化在前面列出的其中一个动画类,配置该实例,然后使用希望修改的元素的BeginAnimation()方法。所有WPF元素,从UIElement基类开始,都继承了BeginAnimation()方法,该方法是IAnimatable接口的一部分。其他实现了IAnimatable接口的类包括ContentElement(文档流内容的基类)和Visual3D(3D可视化对象的基类)。
下图显示了一个非简单的、增加了按钮宽度的动画。当单击按钮时,WPF平滑地扩展按钮的两个侧边直到充满窗口。
为创建这种效果,使用动画修改按钮的Width属性。当单击按钮时,下面的代码创建并启用这个动画:
DoubleAnimation widthAnimation = new DoubleAnimation(); widthAnimation.From = 121; widthAnimation.To = this.Width - 30; widthAnimation.Duration = TimeSpan.FromSeconds(5); btnGrow.BeginAnimation(Button.WidthProperty, widthAnimation);
任何使用线性插值的动画最少需要三个细节:开始值(From)、结束值(To)和整个动画执行的时间(Duration)。在这个示例中,结束值基于包含按钮的窗口的当前宽度。使用插值的所有动画类都提供了这三个属性。
From、To和Duration属性看似简单,但应注意他们的几个重要细节。接下来将更深入地分析这些属性。
1.From属性
From值是Width属性的开始值。如果多次单击按钮,每次单击时,都会将Width属性重新设置为121,并且重新开始运行动画。即使当动画已在运行时单击按钮也同样如此。
在许多情况下,可能不希望动画从最初的From值开始。有如下两个常见的原因:
- 创建能够被触发多次,并逐次累加效果的动画。例如,可能希望创建每次单击时都增大一点的按钮。
- 创建可能相互重叠的动画。例如,可使用MouseEnter事件触发扩展按钮的动画,并使用MouseLeave事件触发将按钮缩小为原尺寸的互补动画(这通常称为“鱼眼”效果)。如果连续快速地讲鼠标多次移动到这种按钮上并移开,每个新动画就会打算上一个动画,导致按钮“跳”回到由From属性设置的尺寸。
当前示例属于第二种情况。如果当按钮正在增大时单击按钮,按钮的宽度就会被重新设置为121像素——这可能会出现抖动效果。为了纠正这个效果,只需要忽略设置From属性的代码语句即可:
DoubleAnimation widthAnimation = new DoubleAnimation(); widthAnimation.To = this.Width - 30; widthAnimation.Duration = TimeSpan.FromSeconds(5); btnGrow.BeginAnimation(Button.WidthProperty, widthAnimation);
现在有一个问题。为使用这种技术,应用动画的属性必须有预先设置的值。在这个示例中,这意味着按钮必须有硬编码的宽度(不管是在按钮标签中直接定义的,还是通过样式设置器应用的)。问题是在许多布局容器中,通常不指定宽度并且让容器根据元素的对齐属性控制宽度。对于这种情况,元素使用默认宽度,也就是特殊的Double.NaN值(这里的NaN代表"不是数字(not a number)")。不能为具有这种值得属性使用线性插值应用动画。
那么,解决方法是什么呢?在许多情况下,答案是硬编码按钮的宽度。正如看你到的,动画经常更精确地控制元素的尺寸和位置。实际上,对于能应用动画的内容,最常用的布局容器是Canvas面板,因为Canvas 面板允许更方便地移动内容(可能相互重叠)以及改变内容的尺寸。Canvas面板还是量级最轻的布局容器,因为当诸如Width的属性发生变化时不需要额外的布局工作。
在当前示例中,还有一种选择。可使用ActualWidth属性检索按钮的当前值,该属性给出的是按钮当前渲染的宽度。不能为ActualWidth属性应用动画(该属性是只读的)。但可以用该属性设置动画的From属性:
widthAnimation.From = btnGrow.ActualWidth;
这种技术既可用于基于代码的动画(如当前示例),也可用于将后面介绍的声明式动画(这时需要使用绑定表达式来获得ActualWidth属性的值)。
需要弄清的另一个问题是,当使用当前值作为动画的起点时——可能改变动画的运行速度。这时因为未调整动画的持续时间,是动画能够考虑到在初始化和最终值之间的宽度变小了。例如,假设创建的按钮不是使用From值而是从当前位置开始动画。如果当几乎达到最大宽度值时单击按钮,新的动画就开始了。尽管只有几个像素的空间可供使用,但这个动画仍呗配置为持续5秒(通过Duration属性)。所以,按钮的增速看起来变慢了。
只有当重新启动解决完成的动画时才会出现这种效果。尽管有些奇怪,但是大多数开发人员不会尝试为解决该问题而编写许多代码。相反,这被认为具有可以接受的问题。
2.To属性
就像可省略From属性一样,也可省略To属性。实际上,可同时省略From属性和To属性,像下面这样创建动画:
DoubleAnimation widthAnimation = new DoubleAnimation(); widthAnimation.Duration = TimeSpan.FromSeconds(5); btnGrow.BeginAnimation(Button.WidthProperty, widthAnimation);
乍一看,这个动画好像根本没有执行任何操作。这样想是符合逻辑的,因为To属性和From属性都被忽略了,他们将使用相同的值。但他们之间存在一点微妙且重要的区别。
当省略From属性时,动画使用当前值,并将动画纳入考虑范围。例如,如果按钮位于某个增长操作的中间,From值会使用扩展后的宽度。然而,当忽略To值时,动画使用不考虑动画的当前值。本质上,这意味着To值变为原数值——最后一次在代码中、元素标签中或通过样式设置的值.
在按钮示例中,这意味着如果开始了一个增长动画,然后使用上面的动画打断该动画,按钮将会从已经增长了之后的尺寸进行缩小,直到达到在XAML标记中设置的原始宽度。另一方面,如果在没有其它动画正在进行的情况下进行这段代码,不会发生任何事情,这是因为From值(动画后的宽度)和To(原始宽度)相等。
3.By属性
即使不使用To属性,也可以使用By属性,By属性用于创建按钮设置的数值改变值得动画而不是按给定目标改变值。例如,可创建一个动画,增大按钮的尺寸,使得比当前尺寸大10个单位,如下所示:
DoubleAnimation widthAnimation = new DoubleAnimation(); widthAnimation.By = 10; widthAnimation.Duration = TimeSpan.FromSeconds(5); btnGrow.BeginAnimation(Button.WidthProperty, widthAnimation);
在按钮示例中,这种方法不是必需的,因为可使用简单的计算设置To属性来实现相同的的效果,如下所示:
widthAnimation.To = btnGrow.Width + 10;
然而当使用XAML定义动画时,使用By值就变得更加合理了,因为XAML没有提供执行简单计算的方法。
大部分使用插值的动画类通常都提供了By属性,但并非全部如此。例如,对于非数值数据类型来说,By属性是没有意义的,比如ColorAnimation类使用的Color结构。
另有一种方法可得到类似的行为,而不需要使用By属性——可通过设置IsAdditive属性创建增加数值的动画。当创建这种动画时,当前值被自动添加到From值和To值。例如,分析下面这个动画:
DoubleAnimation widthAnimation = new DoubleAnimation(); widthAnimation.From = 0; widthAnimation.To = -10; widthAnimation.Duration = TimeSpan.FromSeconds(0.5); widthAnimation.IsAdditive = true;
这个动画是从当前值开始的,当达到比当前值少10个单位的值时完成。另一方面,如果使用下面的动画:
DoubleAnimation widthAnimation = new DoubleAnimation(); widthAnimation.From = 10; widthAnimation.To = 50; widthAnimation.Duration = TimeSpan.FromSeconds(0.5); widthAnimation.IsAdditive = true;
属性值跳到新值(比当前值大10个单位的值),然后增加值,直到达到最后的值,最后的值比动画开始前得的当前值大50个单位。
4.Duration属性
Duration属性很简单——是在动画开始时刻和结束时刻之间的时间间隔(时间间隔单位是毫秒、分钟、小时或喜欢使用的其他任何单位)。尽管在上一个示例中,动画的持续时间是使用TimeSpan对象设置的,但Duration结构定义了一种隐式转换,能偶根据需要将System.TimeSpan转换为System.Windows.Duration。这正是为什么下面的代码完全合理的原因:
widthAnimation.Duration = TimeSpan.FromSeconds(5);
那么,为什么使用全新的数据类型呢?因为Duration类型还提供了两个不能通过TimeSpan对象表示的特殊值——Duration.Automatic和Duration.Forever。在当前示例中,这两个值都没有用处(Automatic值只将动画设置为1秒得持续时间,而Forever值使动画具有无限的持续时间,这会防止动画具有任何效果)。然而,当创建更复杂的动画时,这些值就有用处了。
三、同时发生的动画
可使用BeginAnimation()方法同时启动多个动画。BeginAnimation()方法几乎总是立即返回,从而可以使用类似下面的代码同时为两个属性应用动画:
DoubleAnimation widthAnimation = new DoubleAnimation(); widthAnimation.From = 219; widthAnimation.To = this.Width - 30; widthAnimation.Duration = TimeSpan.FromSeconds(5); DoubleAnimation heightAnimation = new DoubleAnimation(); heightAnimation.From = 99; heightAnimation.To = this.Height - 50; heightAnimation.Duration = TimeSpan.FromSeconds(5); btnGrow.BeginAnimation(Button.WidthProperty, widthAnimation); btnGrow.BeginAnimation(Button.HeightProperty, heightAnimation);
在这个示例中,两个动画没有被同步,这意味着宽度和高速不会准确地再相同时间间隔内增长(通常,将看到按钮先增加宽度,紧接着增大高速)。可通过创建绑定到同一个时间县的动画,突破这一限制。
四、动画的生命周期
从技术角度看,WPF动画是暂时的,这意味着它们不能真正改变基本属性的值。当动画处于活动状态时,只是覆盖属性值。这是由依赖项属性的工作方式造成的,并且这是一个经常被忽视的细节,该细节会给用户带来极大的困惑。
单向动画(如增长按钮的动画)在运行结束后保持处于活动状态,这是因为动画需要将按钮的宽度保持为新值。这会导致如下不常见的问题——如果尝试使用代码在动画完成后修改属性值,代码将不起作用。因为代码只是为属性指定了一个新的本地值,但仍会优先使用动画之后的属性值。
根据准备完成的工作,可通过如下几种方法解决这个问题:
- 创建将元素重新设置为原始状态的动画。可通过创建不设置To属性的动画达到该目的。例如,将按钮的宽度减少到最后设置的尺寸的按钮缩小动画,之后就可能使用代码改变该属性了。
- 创建可翻转的动画。通过将AutoReverse属性设置为true来创建可翻转的动画。例如,当按钮增长动画不在增加按钮的宽度时,将反向播放动画,返回到原始宽度。动画的总持续事件也将翻倍。
- 改变FillBehavior属性。通常,FillBehavior属性被设置为HoldEnd,这意味着当动画结束时,会继续为目标元素应用最后的值。如果将FillBehavior属性改为Stop,只要动画结束,属性就会恢复为原来的值。
- 当动画完成时通过处理动画对象的Completed事件删除动画对象。
前三种方法改变了动画的行为。不管使用哪种方法,他们都将动画后的属性设置为原来的数值。如果这并非所希望的,那就需要使用最后一种方法。
首先,在启动动画钱,关联事件处理程序以响应动画完成事件:
widthAnimation.Completed += widthAnimation_Completed;
当引发Completed事件时,可通过调用BeginAnimation()方法来渲染不活动的动画。为此,只需要指定属性,并为动画对象传递null引用:
btnGrow.BeginAnimation(Button.WidthProperty, null);
当调用BeginAnimation()方法时,属性返回为动画开始之前的原始只。如果这并非所希望的结果,可记下动画应用的当前值,删除动画,然后手动为属性设置新值,如下所示:
double currentWidth = this.btnGrow.Width; btnGrow.BeginAnimation(Button.WidthProperty, null); btnGrow.Width = currentWidth;
需要注意的是,现在改变了属性的本地值。这可能影响其他动画的运行。例如,如果为按钮使用未指定From属性的动画,该动画就会使用这个新应用的属性值作为起点。大多数情况下,这正是所希望的行为。
五、Timeline类
每个动画需要使用几个重要属性,我们已经分析了其中几个属性:From和To属性(使用插值的动画类提供了这两个属性),以及Duration和FillBehavior属性(所有动画类都提供了这两个属性)。在继续学习之前,有必要深入分析必须使用的属性。
下图显示了WPF动画类的继承层次结构。该图包含了所有基类,但省略了全部42个动画类以及相应的TypeNameAnimationBase类:
图 动画类的继承层次结构
上图显示的层次结构包含了继承自Timeline抽象类的三个主要分支。当播放音频或视频文件时使用MediaTimeline类。AnimationTimeline分支用于到目前为止分析过的基于属性的动画系统。而TimelineGroup分支则允许同步时间线并控制它们的播放。
Timeline类中前几个有用的成员定义了已经介绍过的Duration属性,还有其他几个属性。下表列出了Timeline类的属性:
表 Timeline类的属性
尽管BeginTime、Duration、SpeedRatio以及AutoReverse属性都很简单,但其他一些属性需要进一步加以分析。接下来将深入分析AccelerationRatio、DecelerationRatio以及RepeatBehavior属性。
1.AccelerationRatio和DecelerationRatio属性
可以通过AccelerationRatio和DecelerationRatio属性压缩部分时间线,使动画运行得更快。并将拉伸其他时间线进行补偿,使总时间保持不变。
这两个属性都表示百分百值。例如,将AccelerationRatio属性设置为0.3表示希望使用动画持续时间中前30%的时间进行加速。例如,在一个持续10秒得动画中,前3秒会加速运行,而剩余的7秒会以恒定不变的速度运行(显然,在最后7秒钟得速度比没有加速的动画快,因为需要补偿前3秒中的缓慢启动)。如果将AccelerationRatio属性设置为0.3,并将DecelerationRatio属性也设置为0.3,那么前3秒会加速,在中间4秒保持固定的最大速度,在最后3秒减速。分析一下这种方式,显然,AccelerationRatio和DecelerationRatio属性值之和不能超过1,否则就需要超过100%的可用时间来执行所需的加速和减速。当然,可将AccelerationRatio属性设置为1(对于这种情况,动画速度从开始到结束一直在增加),或将DecelerationRatio属性设置为1(对于这种情况,动画速度从开始到结束一直在降低)。
加速和减速的动画常用于提供更趋自然的外观。然而,AccelerationRatio和DecelerationRatio属性只提供了相对简单的控制。例如,它们不能改变加速速度或者将其设置为指定的值。如果希望得到使用可变加速度的动画,需要定义一系列动画,逐个进行播放,并且为每个动画设置AccelerationRatio和DecelerationRatio属性,或者需要使用具有关键样条曲线帧动画。尽管这种技术提供了很大的灵活性,但一直跟踪所有细节是一件令人头疼的事情,并且对于构建动画来说,完美的情况是使用设计工具。
2.RepeatBehavior属性
使用RepeatBehavior属性可控制如何重复运行动画。如果希望重复固定次数,应为RepeatBehavior构造函数传递合适的次数。例如,下面的动画重复次数:
DoubleAnimation widthAnimation = new DoubleAnimation(); widthAnimation.To = this.Width - 30; widthAnimation.Duration = TimeSpan.FromSeconds(5); widthAnimation.RepeatBehavior = new RepeatBehavior(2); btnGrow.BeginAnimation(Button.WidthProperty, widthAnimation);
当运行这个动画时,按钮会增大尺寸(经过5秒),调回到原来的数值,然后再次增加尺寸(经过5秒),在按钮的宽度为整个窗口的宽度时结束。如果将AutoReverse属性设置为true,行为稍有不同——整个动画完成向前和向后运行(意味着先展开按钮,然后收缩),之后再重复一次。
除可以使用RepeatBehavior属性设置重复次数外,还可以用该属性设置重复的时间间隔。为此,只需要为RepeatBehavior对象的构造函数传递一个TimeSpan对象。例如,下面的动画重复13秒:
DoubleAnimation widthAnimation = new DoubleAnimation(); widthAnimation.To = this.Width - 30; widthAnimation.Duration = TimeSpan.FromSeconds(5); widthAnimation.RepeatBehavior = new RepeatBehavior(TimeSpan.FromSeconds(13)); btnGrow.BeginAnimation(Button.WidthProperty, widthAnimation);
在该例中,Duration属性指定整个动画历经5秒。因此,将RepeatBehavior属性设置为13秒将会引起两次重复,然后通过第三次重复动画,使按钮的宽度处于中间位置(在3秒得位置)。
最后,也可使用RepeatBehavior.Forever值使动画不断地重复自身:
widthAnimation.RepeatBehavior = RepeatBehavior.Forever;