动画提示和技巧
在 WPF 中使用动画时,有许多提示和技巧可以使动画性能更好,并节省你的挫折感。
一般问题
对滚动条或滑块的位置进行动画处理会冻结它
如果使用一个 FillBehavior 为 HoldEnd(默认值)的动画对滚动条或滑块的位置进行动画处理,则用户将无法再移动该滚动条或滑块。 这是因为,即使动画已结束,它仍然在重写目标属性的基值。 若要使动画不再替代该属性的当前值,请删除它,或为其赋予 Stop 的 FillBehavior。 有关详细信息及示例,请参阅在使用情节提要对属性进行动画处理后设置该属性。
对动画的输出进行动画处理没有效果
无法对作为另一个动画输出的对象进行动画处理。 例如,如果使用 ObjectAnimationUsingKeyFrames 对 Rectangle 从 RadialGradientBrush 到 SolidColorBrush的 Fill 进行动画处理,则无法对 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 进行动画处理。
第一个 Storyboard B1
对 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>
第二个 Storyboard B2
也对相同 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。
此动画问题主要出现在生存期较长的对象上。 当对某个对象进行垃圾回收时,它的时钟也会断开连接并进行垃圾回收。
有关时钟对象的详细信息,请参阅 动画和计时系统概述。