动画提示和技巧
在 WPF 中处理动画时,有一些提示和技巧可使动画效果更佳并避免挫折。
一般问题
对滚动条或滑块的位置进行动画处理将冻结它
如果使用一个 FillBehavior 为 HoldEnd(默认值)的动画对滚动条或滑块的位置进行动画处理,则用户将无法再移动该滚动条或滑块。 这是因为,即使动画已结束,它仍然在重写目标属性的基值。 若要使动画不再替代该属性的当前值,请删除它,或为其赋予 Stop 的 FillBehavior。 有关详细信息及示例,请参阅在使用情节提要对属性进行动画处理后设置该属性。
对动画的输出进行动画处理没有效果
如果某个对象是另一个动画的输出,则无法对该对象进行动画处理。 例如,如果使用 ObjectAnimationUsingKeyFrames 对某个 Rectangle 的 Fill 进行从 RadialGradientBrush 到 SolidColorBrush 的动画处理,则无法对 RadialGradientBrush 或 SolidColorBrush 的任何属性进行动画处理。
在对属性进行动画处理后无法更改该属性的值
在某些情况下,在对属性进行动画处理后,即使在动画结束后,看起来仍无法更改该属性的值。 这是因为即使动画已结束,它仍然在重写该属性的基值。 若要使动画不再替代该属性的当前值,请删除它,或为其赋予 Stop 的 FillBehavior。 有关详细信息及示例,请参阅在使用情节提要对属性进行动画处理后设置该属性。
更改时间线没有效果
尽管大多数 Timeline 属性都可以进行动画处理和数据绑定,但更改活动 Timeline 的属性值似乎没有效果。 这是因为,当 Timeline 开始时,计时系统将创建 Timeline 的副本并使用它创建 Clock 对象。 修改原件对系统的副本没有影响。
若要使 Timeline 反映更改,必须重新生成它的时钟,并用它来替换以前创建的时钟。 系统不会自动生成时钟。 以下是应用时间线更改的几种方法:
如果时间线是 Storyboard 或属于它,则可以通过使用 BeginStoryboard 或 Begin 方法重新应用其情节提要来使其反映更改。 这还会产生重新启动动画的附带影响。 在代码中,可以使用 Seek 方法将情节提要向前移回其之前的位置。
如果已使用 BeginAnimation 方法将动画直接应用到某个属性,则再次调用 BeginAnimation 方法并向其传递已修改的动画。
如果要直接在时钟级别上工作,请创建并应用一组新的时钟,然后用它们替换之前生成的一组时钟。
有关时间线和时钟的详细信息,请参阅动画和计时系统概述。
FillBehavior.Stop 不按预期方式工作
有时,将 FillBehavior 属性设置为 Stop 似乎没有效果,例如当一个动画“切换”到另一个动画时,由于它具有 SnapshotAndReplace 的 HandoffBehavior 设置而没有效果。
下面的示例将创建一个 Canvas、Rectangle 和 TranslateTransform。 将对 TranslateTransform 进行动画处理,以使 Rectangle 围绕 Canvas 移动。
<Canvas Width="600" Height="200">
<Rectangle
Canvas.Top="50" Canvas.Left="0"
Width="50" Height="50" Fill="Red">
<Rectangle.RenderTransform>
<TranslateTransform
x:Name="MyTranslateTransform"
X="0" Y="0" />
</Rectangle.RenderTransform>
</Rectangle>
</Canvas>
本部分中的示例使用上述对象演示 FillBehavior 属性行为不符合预期的几种情况。
针对多个动画的 FillBehavior="Stop" 和 HandoffBehavior
有时,当某个动画替换为第二个动画时,似乎会忽略其 FillBehavior 属性。 以下面的示例为例,该示例创建两个 Storyboard 对象并使用它们对上述示例中所示的相同 TranslateTransform 进行动画处理。
第一个 StoryboardB1
对 TranslateTransform 的 X 属性进行从 0 到 350 的动画处理,这会将矩形向右移动 350 像素。 当动画到达其持续时间的末尾并停止播放时,X 属性会恢复为其原始值 0。 因此,矩形向右移动 350 像素,然后跳回其原始位置。
<Button Content="Start Storyboard B1">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard x:Name="B1">
<DoubleAnimation
Storyboard.TargetName="MyTranslateTransform"
Storyboard.TargetProperty="X"
From="0" To="350" Duration="0:0:5"
FillBehavior="Stop"
/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
第二个 StoryboardB2
也对相同 TranslateTransform 的 X 属性进行动画处理。 由于仅设置了此 Storyboard 中动画的 To 属性,因此动画使用它进行动画处理的属性的当前值作为其起始值。
<!-- Animates the same object and property as the preceding
Storyboard. -->
<Button Content="Start Storyboard B2">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard x:Name="B2">
<DoubleAnimation
Storyboard.TargetName="MyTranslateTransform"
Storyboard.TargetProperty="X"
To="500" Duration="0:0:5"
FillBehavior="Stop" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
如果在第一个 Storyboard 播放时单击第二个按钮,用户可能看到以下行为:
第一个情节提要结束并将矩形发送回其原始位置,因为动画的 FillBehavior 为 Stop。
第二个情节提要生效,从当前位置(现在为 0)播放动画到 500。
但情况并非如此。 矩形没有跳回,而是继续向右移动。 这是因为第二个动画使用第一个动画的当前值作为其起始值,并从该值开始播放动画到 500。 当第二个动画因为使用了 SnapshotAndReplaceHandoffBehavior 而取代了第一个动画时,第一个动画的 FillBehavior 无关紧要。
FillBehavior 和 Completed 事件
下面的示例演示了 StopFillBehavior 看似没有任何效果的另一种情况。 同样,该示例使用情节提要对 TranslateTransform 的 X 属性进行从 0 到 350 的动画处理。 但此次,该示例注册了 Completed 事件。
<Button Content="Start Storyboard C">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard Completed="StoryboardC_Completed">
<DoubleAnimation
Storyboard.TargetName="MyTranslateTransform"
Storyboard.TargetProperty="X"
From="0" To="350" Duration="0:0:5"
FillBehavior="Stop" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
Completed 事件处理程序启动另一个 Storyboard,对相同的属性进行从其当前值到 500 的动画处理。
private void StoryboardC_Completed(object sender, EventArgs e)
{
Storyboard translationAnimationStoryboard =
(Storyboard)this.Resources["TranslationAnimationStoryboardResource"];
translationAnimationStoryboard.Begin(this);
}
Private Sub StoryboardC_Completed(ByVal sender As Object, ByVal e As EventArgs)
Dim translationAnimationStoryboard As Storyboard = CType(Me.Resources("TranslationAnimationStoryboardResource"), Storyboard)
translationAnimationStoryboard.Begin(Me)
End Sub
以下是将第二个 Storyboard 定义为资源的标记。
<Page.Resources>
<Storyboard x:Key="TranslationAnimationStoryboardResource">
<DoubleAnimation
Storyboard.TargetName="MyTranslateTransform"
Storyboard.TargetProperty="X"
To="500" Duration="0:0:5" />
</Storyboard>
</Page.Resources>
运行 Storyboard 时,可能希望 TranslateTransform 的 X 属性进行从 0 到 350 的动画处理,完成后再还原到 0(因为其 FillBehavior 设置为 Stop),然后进行从 0 到 500 的动画处理。 而 TranslateTransform 则进行从 0 到 350,然后到 500 的动画处理。
这是因为 WPF 引发事件的顺序,也因为属性值已缓存,除非该属性失效,否则不会重新计算。 首先处理 Completed 事件,因为该事件是由根时间线(第一个 Storyboard)触发。 此时,X 属性仍然返回其经过动画处理后的值,因为它尚未失效。 第二个 Storyboard 使用缓存的值作为其起始值并开始进行动画处理。
性能
在导航离开页面后动画继续运行
当用户导航离开包含正在运行的动画的 Page 后,这些动画将继续播放,直到对 Page 进行垃圾回收。 根据正在使用的导航系统,导航离开的页面可能无限期地保留在内存中,在此期间始终通过动画消耗资源。 当页面包含不断运行的(“氛围”)动画时,这一点最明显。
出于此原因,在导航离开页面时,最好使用 Unloaded 事件删除动画。
删除动画有多种不同的方法。 以下技术可用于删除属于 Storyboard 的动画。
若要删除通过事件触发器启动的 Storyboard,请参阅如何:删除情节提要。
若要使用代码删除 Storyboard,请参阅 Remove 方法。
无论动画如何启动,都可以使用下一个技术。
- 要从特定属性中移除动画,请使用 BeginAnimation(DependencyProperty, AnimationTimeline) 方法。 将正在进行动画处理的属性指定为第一个参数,并将
null
指定为第二个参数。 这将从该属性中删除所有动画时钟。
有关对属性进行动画处理的不同方法的详细信息,请参阅属性动画技术概述。
使用组合 HandoffBehavior 会消耗系统资源
当使用 ComposeHandoffBehavior 将 Storyboard、AnimationTimeline 或 AnimationClock 应用于属性时,之前与该属性关联的任何 Clock 对象都会继续消耗系统资源;计时系统不会自动删除这些时钟。
为避免在使用 Compose 应用大量时钟时出现性能问题,应该在完成后从经动画处理过的属性中删除组合时钟。 删除时钟有多种方法。
要从属性中删除所有时钟,请使用经动画处理的对象的 ApplyAnimationClock(DependencyProperty, AnimationClock) 或 BeginAnimation(DependencyProperty, AnimationTimeline) 方法。 将正在进行动画处理的属性指定为第一个参数,并将
null
指定为第二个参数。 这将从该属性中删除所有动画时钟。要从时钟列表中删除特定 AnimationClock,请使用 AnimationClock 的 Controller 属性来检索 ClockController,然后调用 ClockController 的 Remove 方法。 这通常在某个时钟的 Completed 事件处理程序中完成。 注意,ClockController 只能控制根时钟,子时钟的 Controller 属性将返回
null
。 另请注意,如果时钟的有效持续时间是永远,将不会调用 Completed 事件。 在这种情况下,用户需要确定何时调用 Remove。
此动画问题主要出现在生存期较长的对象上。 当对某个对象进行垃圾回收时,它的时钟也会断开连接并进行垃圾回收。
有关时钟对象的详细信息,请参阅动画和计时系统概述。