WPF 提供了广泛的动画可能性,从简单到非常复杂。 在本章中,我们将深入探讨 WPF 属性动画系统,但主要关注那些可以适用于实际业务应用程序的部分。 我们将研究如何实时控制正在运行的动画,并主要关注基于 XAML 的语法。 然后我们将看到如何将动画直接构建到我们的应用程序框架中。
在 WPF 中,动画是通过定期重复更改各个属性值来创建的。 动画由许多组件组成:我们需要一个计时系统、一个负责更新特定类型对象值的动画对象和一个合适的动画属性。
为了能够为属性设置动画,它必须是 DependencyObject 的 Dependency Property,并且其类型必须实现 IAnimatable 接口。 由于大多数 UI 控件都扩展了 DependencyObject 类,这使我们能够为大多数控件的属性设置动画。
此外,必须存在相关类型属性的动画对象。 在 WPF 中,动画对象还兼作计时系统,因为它们扩展了 Timeline 类。 在研究各种动画对象之前,让我们先来看看计时系统。
调查时间表
动画需要某种计时机制,负责在正确的时间更新相关的属性值。 在 WPF 中,这种计时机制是由抽象的 Timeline 类提供的,简而言之,它代表了一段时间。 所有可用的动画类都扩展了这个类并添加了它们自己的动画功能。
当 Timeline 类用于动画时,会制作并冻结一个内部副本,因此它是不可变的。 此外,还会创建一个 Clock 对象来保存 Timeline 对象的运行时计时状态,并负责动画属性更新的实际计时。 Timeline 对象本身除了定义相关的时间段外几乎没有什么作用。
当我们定义 Storyboard 对象或调用 Animatable.BeginAnimation 方法之一时,会自动为我们创建 Clock 对象。 请注意,我们通常不需要直接关注这些 Clock 对象,但了解它们有助于理解更大的图景。
有许多不同类型的 Timeline 对象,从 AnimationTimeline 类到 TimelineGroup 和 ParallelTimeline 类。 但是,出于动画目的,我们主要使用 Storyboard 类,它扩展了 ParallelTimeline 和 TimelineGroup 类,并添加了动画目标属性和用于控制时间线的方法。 让我们首先研究基本 Timeline 类的主要属性。
Duration 属性指定由关联的 Timeline 对象表示的时间。 但是,时间线可以有重复,因此对 Duration 属性的更准确描述可能是它指定关联的时间线对象的单次迭代的时间。
duration 属性的类型为 Duration,它包含一个 TimeSpan 属性,该属性包含指定持续时间值的实际时间。 但是,WPF 包含一个类型转换器,它使我们能够以以下格式在 XAML 中指定此 TimeSpan 值,其中方括号突出显示可选段:
Duration="[Days.]Hours:Minutes:Seconds[.FractionalSeconds]"
Duration="[Days.]Hours:Minutes"
但是,除了 TimeSpan 持续时间之外,Duration 结构还接受其他值。 有一个值为 Automatic,它是包含其他时间线的组件时间线的默认值。 在这些情况下,此值仅意味着父时间线的持续时间将与其子时间线的最长持续时间一样长。 我们没有什么目的明确地使用这个值。
然而,还有一个对我们非常有用的价值。 Duration 结构还定义了一个表示无限时间段的 Forever 属性。 我们可以使用这个值来使动画无限期地继续,或者更准确地说,只要它的相关视图正在显示:
Duration="Forever"
时间线对象将在其持续时间结束时停止播放。 如果它有任何与之关联的子时间线,那么它们也将在此时停止播放。 然而,时间线的自然持续时间可以使用其他属性来延长或缩短,我们很快就会看到。
一些时间线,例如 ParallelTimeline 和 Storyboard 类,能够包含其他时间线,并且可以通过为 Duration 属性设置自己的值来影响它们的持续时间,这将覆盖由子时间线设置的值。 让我们改变一个 第 5 章中较早的动画示例,使用正确的作业控件来演示这一点:
<Rectangle Width="0" Height="0" Fill="Orange">
<Rectangle.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard Duration="0:0:2.5">
<DoubleAnimation Storyboard.TargetProperty="Width" To="300.0"
Duration="0:0:2.5" />
<DoubleAnimation Storyboard.TargetProperty="Height" To="300.0"
Duration="0:0:5" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Rectangle.Triggers>
</Rectangle>
在前面的示例中,我们有一个 Rectangle 对象,其尺寸最初设置为零。 Storyboard 对象包含两个独立的动画对象,它们将动画其尺寸从零到三百像素。 为矩形宽度设置动画的动画对象的持续时间为两秒半,而为高度设置动画的动画对象的持续时间为五秒。
但是,包含的 Storyboard 对象的持续时间为两秒半,因此这将在两秒半后停止两个子动画对象的时间线,无论它们声明的持续时间如何。 这样做的结果将是动画完成后,我们的 Rectangle 对象将显示为一个矩形,而不是具有相等高度和宽度值的正方形。
如果我们更改了故事板的持续时间以匹配较长的子动画的持续时间,或者更改动画的持续时间以匹配较短的子动画的持续时间,那么我们的动画形状将以正方形结束,而不是矩形。
调整动画元素的指定持续时间的另一种方法是设置其 AutoReverse 属性。 实际上,将此属性设置为 True 通常会使 Duration 属性指定的时间长度加倍,因为时间线将在完成其正常的向前迭代后反向播放。 让我们改变前面示例中的故事板来演示这一点:
<Storyboard Duration="0:0:5">
<DoubleAnimation Storyboard.TargetProperty="Width" To="300.0"
Duration="0:0:2.5" AutoReverse="True" />
<DoubleAnimation Storyboard.TargetProperty="Height" To="300.0"
Duration="0:0:5" />
</Storyboard>
现在,两个子时间线将具有相同的总持续时间,因为第一个以前较短的时间线实际上已加倍长度。 但是,这将导致第一个时间轴将矩形的宽度设置为 300 像素,然后返回到零,因此动画完成后它将不可见。 另请注意,我们必须将父情节提要持续时间设置为 5 秒,以便查看子时间线的差异。
再次注意,在包含其他时间线的时间线上设置的属性将影响子时间线上这些属性的值。 因此,在父时间轴(Storyboard 对象)上将 AutoReverse 属性设置为 True 将使子动画运行的总时间加倍; 在我们的例子中,使用以下示例,矩形现在将总共动画十秒:
<Storyboard Duration="0:0:5" AutoReverse="True">
<DoubleAnimation Storyboard.TargetProperty="Width" To="300.0"
Duration="0:0:2.5" AutoReverse="True" />
<DoubleAnimation Storyboard.TargetProperty="Height" To="300.0"
Duration="0:0:5" />
</Storyboard>
重复的avior属性是重复行为类型,也可以影响时间轴的总体持续时间。 与AutoreVerse属性不同,它也可以缩短整个持续时间以及延长它。 使用重复行为属性,我们可以使用不同行为以多种方式指定值。
最简单的方法是计算我们希望将时间线的原始持续时间乘以多少次。 预先存在的 XAML 类型转换器使我们能够通过在计数后指定 x 来设置 XAML 中的重复计数,如以下示例所示。 请注意,我们还可以在此处指定带小数位的数字,包括小于一的值:
<Storyboard Duration="0:0:5" AutoReverse="True" RepeatBehavior="2x">
<DoubleAnimation Storyboard.TargetProperty="Width" To="300.0"
Duration="0:0:2.5" AutoReverse="True" />
<DoubleAnimation Storyboard.TargetProperty="Height" To="300.0"
Duration="0:0:5" />
</Storyboard>
在此示例中,正常持续时间为 5 秒,但 AutoReverse 属性设置为 True,因此持续时间加倍。 但是,RepeatBehavior 属性设置为 2x,这会将两倍的十秒乘以二十秒。 这个乘数值 2 将存储在 RepeatBehavior 结构的 Count 属性中。
使用 count 选项的替代方法是简单地设置我们希望动画持续的持续时间。 用于设置 Duration 属性的相同 XAML 语法也可用于设置 RepeatBehavior 属性。 同样,RepeatBehavior 结构也定义了一个代表无限时间的 Forever 属性,我们可以使用该值使动画无限期地继续。
另一个可以影响动画持续时间的属性是 SpeedRatio 属性。 该值乘以其他相关的持续时间属性,因此可以加快和减慢相关的时间线。 现在让我们再次更新我们的示例以帮助解释此属性:
<Storyboard Duration="0:0:5" AutoReverse="True" SpeedRatio="0.5">
<DoubleAnimation Storyboard.TargetProperty="Width" To="300.0"
Duration="0:0:2.5" AutoReverse="True" />
<DoubleAnimation Storyboard.TargetProperty="Height" To="300.0"
Duration="0:0:5" SpeedRatio="2" />
</Storyboard>
同样,此处的正常持续时间为 5 秒,并且 AutoReverse 属性设置为 True,因此持续时间加倍。 但是,SpeedRatio 属性设置为 0.5,因此加倍的持续时间再次加倍为 20 秒。 请注意,SpeedRatio 值为 0.5 表示正常速度的一半,因此是正常持续时间的两倍。
第二个子时间线也设置 SpeedRatio 属性,但它设置为 2,因此它的速度加倍,持续时间减半。 由于其指定的持续时间是其兄弟时间线的两倍,并且它的速度现在是两倍,这具有重新同步两个子动画的效果,因此两个维度现在一起增长,作为一个正方形,而不是一个 长方形。
我们可以使用另外两个与速度相关的属性来微调我们的动画:AccelerationRatio 和 DecelerationRatio 属性。 这些属性调整 相关动画分别加速和减速所花费的时间比例。 虽然这种效果有时可能很微妙,但如果使用得当,它也可以为我们的动画提供专业的触感。
这两个属性的可接受值介于零和一之间。 如果两个属性一起使用,那么它们的值的总和必须仍然保持在零和一之间。 不遵守此规则将导致在运行时抛出以下异常:
The sum of AccelerationRatio and DecelerationRatio must be less than or equal to one.
在这些属性中单独输入超出可接受范围的值也会导致错误,尽管这样做会导致编译错误:
Property value must be between 0.0 and 1.0.
让我们看一个突出显示这两个属性的不同值之间差异的示例:
<StackPanel Margin="20">
<StackPanel.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard RepeatBehavior="Forever" Duration="0:0:1.5"
SpeedRatio="0.5" Storyboard.TargetProperty="Width">
<DoubleAnimation Storyboard.TargetName="RectangleA"
AccelerationRatio="1.0" From="0" To="300" />
<DoubleAnimation Storyboard.TargetName="RectangleB"
AccelerationRatio="0.8" DecelerationRatio="0.2"
From="0" To="300" />
<DoubleAnimation Storyboard.TargetName="RectangleC"
AccelerationRatio="0.6" DecelerationRatio="0.4"
From="0" To="300" />
<DoubleAnimation Storyboard.TargetName="RectangleD"
AccelerationRatio="0.5" DecelerationRatio="0.5"
From="0" To="300" />
<DoubleAnimation Storyboard.TargetName="RectangleE"
AccelerationRatio="0.4" DecelerationRatio="0.6"
From="0" To="300" />
<DoubleAnimation Storyboard.TargetName="RectangleF"
AccelerationRatio="0.2" DecelerationRatio="0.8"
From="0" To="300" />
<DoubleAnimation Storyboard.TargetName="RectangleG"
DecelerationRatio="1.0" From="0" To="300" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</StackPanel.Triggers>
<Rectangle Name="RectangleA" Fill="#FF0000" Height="30" />
<Rectangle Name="RectangleB" Fill="#D5002B" Height="30" />
<Rectangle Name="RectangleC" Fill="#AB0055" Height="30" />
<Rectangle Name="RectangleD" Fill="#800080" Height="30" />
<Rectangle Name="RectangleE" Fill="#5500AB" Height="30" />
<Rectangle Name="RectangleF" Fill="#2B00D5" Height="30" />
<Rectangle Name="RectangleG" Fill="#0000FF" Height="30" />
</StackPanel>
这段代码在 StackPanel 控件中定义了许多 Rectangle 对象,每个对象都有自己关联的 DoubleAnimation 元素,在 1.5 秒内将其宽度从 0 增加到 300 像素。
在这里,我们使用了 Storyboard.TargetName 和 Storyboard.TargetProperty 属性来定位来自单个 EventTrigger 的矩形,以减少前面示例中的代码量。 我们将很快详细介绍这些附加属性,但现在,我们只说它们用于指定目标元素和属性以进行动画处理。
每个动画都以不同的矩形为目标,并为 AccelerationRatio 和 DecelerationRatio 属性设置了不同的值。 顶部矩形的动画将其 AccelerationRatio 属性设置为 1.0,底部矩形的动画将其 DecelerationRatio 属性设置为 1.0。
中间矩形的动画具有不同的值。 矩形越高,AccelerationRatio 属性值越高,DecelerationRatio 属性值越低;矩形越低,AccelerationRatio 属性值越低,DecelerationRatio 属性值越高。
运行此示例时,我们可以清楚地看到各种比率值之间的差异。 在每次迭代开始附近的某个时间点,我们可以看到使用较高 AccelerationRatio 值进行动画处理的顶部矩形的大小没有像使用较高 DecelerationRatio 值进行动画处理的较低矩形那样增长; 但是,所有矩形几乎同时达到 300 像素:
Timeline 类中另一个有用的属性是 BeginTime 属性。 顾名思义,它设置了开始动画的时间; 它可以被认为是一个延迟时间,它相对于父时间轴和兄弟时间轴延迟其动画的开始。
此属性的默认值为 0 秒,当它设置为正值时,延迟仅在时间线开始时发生一次,并且不受可能在其上设置的其他属性的影响。 它通常用于延迟一个或多个动画的开始,直到另一个动画完成。 让我们再次调整前面的例子来证明这一点:
<Rectangle Width="0" Height="1" Fill="Orange">
<Rectangle.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Width" To="300.0"
Duration="0:0:2" />
<DoubleAnimation Storyboard.TargetProperty="Height" To="300.0"
Duration="0:0:2" BeginTime="0:0:2" />
<DoubleAnimation Storyboard.TargetProperty="Width" To="0.0"
Duration="0:0:2" BeginTime="0:0:4" />
<DoubleAnimation Storyboard.TargetProperty="Height" To="0.0"
Duration="0:0:2" BeginTime="0:0:4" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Rectangle.Triggers>
</Rectangle>
在这个例子中,我们有一个单像素高的矩形,其宽度向外增长直到三百像素宽,然后垂直增长直到三百像素高。 在这一点上,它的尺寸同样缩小,直到形状缩小到零。
这是通过在宽度增加动画运行时延迟最后三个动画,然后在高度增加动画运行时延迟最后两个动画来实现的。 最后两个动画的 BeginTime 属性设置为相同的值,以便它们彼此同步启动和运行。
最后一个真正有用的时间线属性是 FillBehavior 属性,它指定当时间线到达其总持续时间或填充周期的末尾时数据绑定属性值应该发生什么。 此属性的类型为 FillBehavior,只有两个值。
如果我们将此属性设置为 HoldEnd 值,则数据绑定属性值将保持在动画结束之前达到的最终值。 相反,如果我们将此属性设置为 Stop 的值,即默认值,则数据绑定的属性值将恢复为该属性在动画开始之前的原始值。 让我们用一个简单的例子来说明这一点:
<StackPanel Margin="20">
<StackPanel.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard Duration="0:0:1.5" SpeedRatio="0.5"
Storyboard.TargetProperty="Opacity">
<DoubleAnimation Storyboard.TargetName="RectangleA" To="0.0"
FillBehavior="HoldEnd" />
<DoubleAnimation Storyboard.TargetName="RectangleB" To="0.0"
FillBehavior="Stop" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</StackPanel.Triggers>
<Rectangle Name="RectangleA" Fill="Orange" Height="100"
HorizontalAlignment="Stretch" Margin="0,0,0,20" />
<Rectangle Name="RectangleB" Fill="Orange" Height="100"
HorizontalAlignment="Stretch" />
</StackPanel>
在该示例中,清楚地证明了两个填充枚举实例之间的差异。 我们有两个相同大小的矩形,具有相同的时间表设置,以为它们的缺陷属性值设置动画,但其填充属性值除外。
两个矩形在相同的时间内从不透明逐渐变为不可见,但是一旦两个时间线完成,FillBehavior 属性设置为 Stop 的矩形立即再次变得可见,就像在动画开始之前一样,而 FillBehavior 属性设置为 HoldEnd 的另一个保持不可见,就像在动画结束时一样。
虽然这涵盖了 Timeline 类直接公开的主要属性,但还有一些属性是由许多扩展 Timeline 类的动画类声明的,这些属性对于完全理解是必不可少的。 它们是 From、By 和 To 属性,它们指定动画的起点和终点。
因为动画类生成属性值,所以不同的属性类型有不同的动画类。 例如,生成 Point 值的动画类称为 PointAnimation 类,所有普通动画类都遵循相同的命名模式,使用 Animation 形式的相关类型的名称,例如 ColorAnimation。
普通动画类,通常称为 From、By 和 To 动画,通常需要指定两个值,尽管有时可以隐式提供其中之一。 然后,相关属性将沿着两个指定值之间的自动插值路径进行动画处理。
最常见的是使用 From 属性提供起始值,使用 To 属性提供结束值。 但是,我们也可以指定单个开始、结束或偏移值,第二个值将从动画属性的当前值中获取。 我们可以使用 By 属性设置偏移值,这表示属性值将在持续时间内更改的确切量。
为这些不同的属性指定值会对生成的动画产生截然不同的影响。 单独使用 From 属性将在所需值处开始动画,并将动画属性直到它达到属性的基值。
单独使用 To 属性将从其当前值开始为属性设置动画,并在指定值处结束。 仅使用 By 属性将使属性从其当前值开始设置动画,直到达到该值与指定偏移量的总和。
这三个属性的组合可用于定位正确的属性值范围。 设置 From 和 By 属性将从 From 属性指定的值开始动画,并对属性进行动画处理,直到达到 By 属性指定的偏移量。
一起设置 From 和 To 属性将从 From 属性指定的值开始动画,并为属性设置动画,直到 To 属性指定的值。 由于 By 和 To 属性都指定了动画的结束值,如果 By 属性指定的值都设置在动画元素上,则它们将被忽略。
虽然这些更常见的动画使用 From、By 和 To 属性中的一个或两个来指定要设置动画的相关属性的值范围,但还有另一种方法可以指定目标值。 现在让我们看一下关键帧动画。
引入关键帧
键帧动画使我们能够执行我们无法与来自的动画做的许多事情。 与那些动画不同,使用键帧动画,我们能够以通常是动画的离散步骤指定两个以上的目标值并在离散步骤中动画对象。 因此,还有更多动画keyware类比动画类,例如:rectanimationUsingKeyFrames,SizeAnimationUsingKeyFrame。
每个 AnimationUsingKeyFrames 类都有一个 KeyFrames 属性,我们用关键帧填充该属性以指定在动画期间必须传递的各种值。 每个关键帧都有一个 KeyTime 和一个 Value 属性来指定值和应该达到的相对时间。
如果没有用零秒的关键时间声明关键帧,则动画将从相关属性的当前值开始。 动画将根据关键帧的 KeyTime 属性值而不是声明它们的顺序对关键帧进行排序,并将根据它们的插值方法在各种值之间创建转换,我们稍后会发现。
请注意,KeyTime 属性的类型为 KeyTime,这使我们能够使用除 TimeSpan 值之外的值类型来设置它。 我们还可以指定百分比值,它决定了每个关键帧将被分配的指定动画持续时间的百分比。 请注意,我们需要使用累积值,这样最终的关键帧关键时间值将始终为 100%。
或者,我们可以使用许多特殊值。 当我们想要一个具有恒定速度的动画时,无论指定的值如何,我们都可以为每个关键帧指定 Paced 值。 这会考虑每个关键帧值之间的变化,然后再将它们间隔在父时间轴的持续时间内并创建平滑、均匀的过渡。
与此方法相比,我们还可以为每个关键帧指定 Uniform 值,这基本上在父动画的持续时间内将关键帧均匀地隔开。 为此,它只需计算关键帧的数量并将该数字除以总持续时间长度,以便每个关键帧将持续相同的时间。
不同的 AnimationUsingKeyFrames 类有不同类型的关键帧,并且使用的插值方法也不同。 这些关键帧的命名约定遵循格式 KeyFrame,例如:LinearDoubleKeyFrame。
有三种插值方法。 第一个是离散的,它不执行插值,只是从一个值跳转到另一个值。 此方法对于设置布尔值或对象值很有用。
下一个方法是线性的,它在关键帧的值和前一个关键帧的值之间执行线性插值。 这意味着动画看起来很流畅,但如果您的关键帧时间间隔不均匀,则会加速和减速。
最后也是最复杂的插值方法是 Spline,但它也为用户提供了对动画时间的最大控制。 它添加了另一个名为 KeySpline 的属性,它使我们能够在从 0.0,0.0 延伸到 1.0,1.0 的贝塞尔曲线上指定两个控制点。 第一个控制点影响曲线的前半部分,而第二个控制点影响曲线的后半部分。
使用这两个控制点,我们可以调整动画在其持续时间内的速度。例如,使用第一个控制点设置为 0.0,1.0,第二个设置为 1.0,0.0 将导致原始线性曲线的最大失真和 导致动画将快速加速,然后在中间几乎停止,然后在结束时再次显着加速。
有了这两点,我们就可以完全控制每对关键帧值之间的值变化速度。 当尝试创建看起来更逼真的动画时,这种类型的插值最有用。 请注意,我们可以在每个关键帧动画中自由混合和匹配具有不同插值方法的关键帧。
例如,假设我们想要为 Point 元素设置动画。 在这种情况下,我们需要使用 PointAnimationUsingKeyFrames 类,然后可以选择代表不同插值方法的关键帧类。 在此示例中,我们可以使用 DiscretePointKeyFrame、LinearPointKeyFrame 和 SplinePointKeyFrame 类的任意组合。
请注意,由于 KeyFrames 属性设置为 ContentPropertyAttribute 属性中的名称输入参数,该属性构成每个 AnimationUsingKeyFrames 类中声明的类签名的一部分,我们不需要在 XAML 中显式声明此属性,并且可以声明 直接在这些元素内的各种关键帧,如以下代码所示:
<Ellipse Width="100" Height="100" Stroke="Black" StrokeThickness="3">
<Ellipse.Fill>
<RadialGradientBrush>
<GradientStop Color="Yellow" Offset="0" />
<GradientStop Color="Orange" Offset="1" />
</RadialGradientBrush>
</Ellipse.Fill>
<Ellipse.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard RepeatBehavior="Forever"
Storyboard.TargetProperty="Fill.GradientOrigin">
<PointAnimationUsingKeyFrames>
<DiscretePointKeyFrame Value="0.5, 0.5" KeyTime="0:0:0" />
<LinearPointKeyFrame Value="1.0, 1.0" KeyTime="0:0:2" />
<SplinePointKeyFrame KeySpline="0,0.25 0.75,0" Value="1.0, 0.0"
KeyTime="0:0:4" />
<LinearPointKeyFrame Value="0.0, 0.0" KeyTime="0:0:5" />
<SplinePointKeyFrame KeySpline="0,0.75 0.25,0" Value="0.5, 0.5"
KeyTime="0:0:8" />
</PointAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Ellipse.Triggers>
</Ellipse>
在此示例中,我们声明了一个椭圆形状,其 Fill 属性设置为 RadialGradientBrush 类的一个实例。 画笔中心为黄色,边缘为橙色。 请注意,这些画笔有一个名为 GradientOrigin 的属性,它指定 渐变的中心点,默认为点 0.5,0.5。 在本例中,我们为该属性设置动画,其效果类似于在 3D 球周围移动光源:
我们使用带有 Loaded 事件的 EventTrigger 来启动我们的动画,并将相关情节提要上的 RepeatBehavior 属性设置为 Forever。 如前所述,我们将 TargetProperty 属性设置为设置为 Fill 属性的画笔的 GradientOrigin 属性。
在情节提要中,我们声明了一个 PointAnimationUsingKeyFrames 元素,并直接在其中声明了许多不同的 KeyFrame 对象。 如前所述,我们无需显式声明 KeyFrames 属性即可在其中声明这些关键帧元素。
请注意,此处使用的 DiscretePointKeyFrame 元素完全是可选的,如果删除也不会改变任何内容。 这是因为点 0.5,0.5 既是动画的起始值,也是渐变画笔的默认值,也是动画的结束值。 此外,如果我们省略一个零时间关键帧,则会为我们隐式添加一个具有此值的关键帧。
接下来,我们声明一个 LinearPointKeyFrame 元素,它将以线性、均匀的方式对从点 0.5,0.5 到点 1.0,1.0 的渐变原点进行动画处理。 之后,我们有一个 SplinePointKeyFrame 元素,它将动画从前一点到点 1.0,0.0 的渐变原点。 请注意 KeySpline 属性,它在动画进行时调整动画的速度。
从那里,我们使用另一个 LinearPointKeyFrame 元素在一秒钟内平滑均匀地过渡到点 0.0,0.0。 最后,我们使用第二个 SplinePointKeyFrame 元素将渐变原点动画化回圆心及其起始位置,占用总持续时间的最后三秒。
运行此示例时,我们可以清楚地看到它在两个 LinearPointKeyFrame 元素的周期内均匀地为渐变原点设置动画,并在两个 SplinePointKeyFrame 元素的周期内改变速度。
讲故事(Telling stories)
虽然扩展 Timeline 类的各种动画类可用于直接在代码中为控件属性设置动画,但为了仅使用 XAML 声明和触发动画,我们需要使用 Storyboard 类。 这就是所谓的容器时间线,因为它扩展了抽象的 TimelineGroup 类,使其能够包含子时间线。
Storyboard 类扩展的另一个容器时间线类是 ParallelTimeline 类,这些类使我们能够对子时间线进行分组并将其属性设置为一个组。 在创建更复杂的动画时,如果我们需要做的只是延迟一组子时间线的开始,我们应该使用 ParallelTimeline 类而不是 Storyboard 类,因为它更有效。
我们可以重写前面的 BeginTime 示例以使用 ParallelTimeline 元素来延迟最后两个时间线的开始。 让我们看看它可能是什么样子:
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Width" To="300.0"
Duration="0:0:2" />
<DoubleAnimation Storyboard.TargetProperty="Height" To="300.0"
Duration="0:0:2" BeginTime="0:0:2" />
<ParallelTimeline BeginTime="0:0:4">
<DoubleAnimation Storyboard.TargetProperty="Width" To="0.0"
Duration="0:0:2" />
<DoubleAnimation Storyboard.TargetProperty="Height" To="0.0"
Duration="0:0:2" />
</ParallelTimeline>
</Storyboard>
由于 Storyboard 类是一个 Timeline 对象,它也具有与各种动画对象相同的属性。 它从 ParallelTimeline 类继承的另一个属性是 SlipBehavior 属性。 只有当我们想要将动画时间线与 MediaTimeline 元素的播放同步时,此属性才真正有用,但值得了解。
此属性是枚举类型 SlipBehavior,它只有两个成员。 Grow 值指定我们不需要动画时间轴与媒体时间轴同步,并且是此属性的默认值。
相反,Slip 值表示我们希望动画时间线在必要时向前或向后滑动,以使它们与正在播放的媒体保持同步。 如果使用此设置时加载媒体需要时间,则情节提要中的动画时间线将等到媒体准备好并在该点继续。
除了从各种基类继承的属性之外,Storyboard 类还声明了三个重要的附加属性,这些属性对于将动画定位到单个 UI 元素和/或其属性至关重要。
Storyboard.Target Attached 属性指定应该设置动画的 UI 控件,但仅设置此属性是不够的,因为它没有指定目标属性。 此属性属于对象类型,尽管它只能与 DependencyObject 类型的对象一起使用。
为了使用这个属性,我们需要指定一个引用目标 UI 控件的绑定路径。 如果目标元素扩展了 FrameworkElement 或 FrameworkContentElement 类,那么一种方法是命名目标元素并使用 ElementName 绑定来引用它:
Storyboard.Target="{Binding ElementName=TargetControlName}"
大多数 UI 元素都扩展了这两个声明 Name 属性的类之一。 但是,如果我们为目标控件提供一个名称,那么就有一种更简单的方法来定位它。 除了使用 Storyboard.Target 属性,我们可以使用 Storyboard.TargetName 附加属性来指定目标元素,只使用它们声明的名称,而无需任何绑定:
Storyboard.TargetName="TargetControlName"
我们并不总是需要指定此属性值,因为有时可以隐式计算出目标元素。 如果相关故事板以 BeginStoryboard 元素开始,则声明它的 UI 元素将成为目标。 此外,如果相关故事板是另一个时间线的子级,则将继承父级时间线的目标。
Storyboard 类声明的最重要的属性是 TargetProperty 附加属性。 我们使用这个属性来指定我们想要在目标元素上设置动画的属性。 请注意,为了使其工作,目标属性必须是依赖属性。
有时,我们可能希望针对不扩展前面提到的任何一个框架类的对象; 在 WPF 中,我们还能够针对扩展 Freezable 类的可冻结类。 为了在 XAML 中定位这些类之一,我们需要使用 x:Name 指令指定对象的名称,因为它们没有 Name 属性。
附带说明一下,声明自己的 Name 属性的 WPF 类实际上将 name 值映射到 x:Name 指令,这是 XAML 规范的一部分。 在这些情况下,我们可以自由地使用其中任何一个来为元素注册名称,但我们不能同时设置两者。
请注意,我们的动画仍然可以引用未命名的元素,尽管它们需要间接引用。 我们不需要直接引用它们,而是需要指定 父属性或可冻结对象的名称,然后在 TargetProperty 附加属性中链接属性,直到我们到达所需的元素。 我们在上一节的最后一个示例中使用了这种方法:
Storyboard.TargetProperty="Fill.GradientOrigin"
在这种情况下,我们引用了 RadialGradientBrush 类型的 Fill 属性,然后我们从那里链接到画笔的 GradientOrigin 属性。 请注意,如果我们在此处使用 SolidColorBrush 类的实例,则此引用将失败,因为该画笔中没有 GradientOrigin 属性。 但是,虽然动画将无法工作,但这不会导致引发任何错误。
控制情节提要
为了在 XAML 中启动故事板,我们需要使用 BeginStoryboard 元素。 此类扩展了 TriggerAction 类,如果您还记得的话,这是我们需要在 EventTrigger 类的 TriggerActionCollection 以及 TriggerBase.EnterActions 和 TriggerBase.ExitActions 属性中使用的类型。
我们通过在代码中将它设置为 Storyboard 属性来指定要与 BeginStoryboard 元素一起使用的情节提要。 使用 XAML 时,Storyboard 属性被隐式设置为在 BeginStoryboard 元素中声明的情节提要。
BeginStoryboard 动作负责将动画时间线与动画目标及其目标属性连接起来,还负责启动其故事板中的各种动画时间线。 一旦满足其父对象的触发条件,它通过调用关联的 Storyboard 对象的 Begin 方法来执行此操作。
如果一个已经运行的故事板被要求重新开始,无论是间接地,使用 BeginStoryboard 动作,还是直接地,使用 Begin 方法,会发生什么将取决于 HandoffBehavior 属性设置的值。
该属性是枚举类型 HandoffBehavior 并且有两个值。 默认值为 SnapshotAndReplace ,这将更新内部时钟,并且本质上具有将时间线的一个副本替换为