动画提示和技巧

在 WPF 中处理动画时,有一些提示和技巧可使动画效果更佳并避免挫折。

一般问题

对滚动条或滑块的位置进行动画处理将冻结它

如果使用一个 FillBehaviorHoldEnd(默认值)的动画对滚动条或滑块的位置进行动画处理,则用户将无法再移动该滚动条或滑块。 这是因为,即使动画已结束,它仍然在重写目标属性的基值。 若要使动画不再替代该属性的当前值,请删除它,或为其赋予 StopFillBehavior。 有关详细信息及示例,请参阅在使用情节提要对属性进行动画处理后设置该属性

对动画的输出进行动画处理没有效果

如果某个对象是另一个动画的输出,则无法对该对象进行动画处理。 例如,如果使用 ObjectAnimationUsingKeyFrames 对某个 RectangleFill 进行从 RadialGradientBrushSolidColorBrush 的动画处理,则无法对 RadialGradientBrushSolidColorBrush 的任何属性进行动画处理。

在对属性进行动画处理后无法更改该属性的值

在某些情况下,在对属性进行动画处理后,即使在动画结束后,看起来仍无法更改该属性的值。 这是因为即使动画已结束,它仍然在重写该属性的基值。 若要使动画不再替代该属性的当前值,请删除它,或为其赋予 StopFillBehavior。 有关详细信息及示例,请参阅在使用情节提要对属性进行动画处理后设置该属性

更改时间线没有效果

尽管大多数 Timeline 属性都可以进行动画处理和数据绑定,但更改活动 Timeline 的属性值似乎没有效果。 这是因为,当 Timeline 开始时,计时系统将创建 Timeline 的副本并使用它创建 Clock 对象。 修改原件对系统的副本没有影响。

若要使 Timeline 反映更改,必须重新生成它的时钟,并用它来替换以前创建的时钟。 系统不会自动生成时钟。 以下是应用时间线更改的几种方法:

  • 如果时间线是 Storyboard 或属于它,则可以通过使用 BeginStoryboardBegin 方法重新应用其情节提要来使其反映更改。 这还会产生重新启动动画的附带影响。 在代码中,可以使用 Seek 方法将情节提要向前移回其之前的位置。

  • 如果已使用 BeginAnimation 方法将动画直接应用到某个属性,则再次调用 BeginAnimation 方法并向其传递已修改的动画。

  • 如果要直接在时钟级别上工作,请创建并应用一组新的时钟,然后用它们替换之前生成的一组时钟。

有关时间线和时钟的详细信息,请参阅动画和计时系统概述

FillBehavior.Stop 不按预期方式工作

有时,将 FillBehavior 属性设置为 Stop 似乎没有效果,例如当一个动画“切换”到另一个动画时,由于它具有 SnapshotAndReplaceHandoffBehavior 设置而没有效果。

下面的示例将创建一个 CanvasRectangleTranslateTransform。 将对 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 进行动画处理。

第一个 StoryboardB1TranslateTransformX 属性进行从 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 也对相同 TranslateTransformX 属性进行动画处理。 由于仅设置了此 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 播放时单击第二个按钮,用户可能看到以下行为:

  1. 第一个情节提要结束并将矩形发送回其原始位置,因为动画的 FillBehaviorStop

  2. 第二个情节提要生效,从当前位置(现在为 0)播放动画到 500。

但情况并非如此。 矩形没有跳回,而是继续向右移动。 这是因为第二个动画使用第一个动画的当前值作为其起始值,并从该值开始播放动画到 500。 当第二个动画因为使用了 SnapshotAndReplaceHandoffBehavior 而取代了第一个动画时,第一个动画的 FillBehavior 无关紧要。

FillBehavior 和 Completed 事件

下面的示例演示了 StopFillBehavior 看似没有任何效果的另一种情况。 同样,该示例使用情节提要对 TranslateTransformX 属性进行从 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 时,可能希望 TranslateTransformX 属性进行从 0 到 350 的动画处理,完成后再还原到 0(因为其 FillBehavior 设置为 Stop),然后进行从 0 到 500 的动画处理。 而 TranslateTransform 则进行从 0 到 350,然后到 500 的动画处理。

这是因为 WPF 引发事件的顺序,也因为属性值已缓存,除非该属性失效,否则不会重新计算。 首先处理 Completed 事件,因为该事件是由根时间线(第一个 Storyboard)触发。 此时,X 属性仍然返回其经过动画处理后的值,因为它尚未失效。 第二个 Storyboard 使用缓存的值作为其起始值并开始进行动画处理。

性能

在导航离开页面后动画继续运行

当用户导航离开包含正在运行的动画的 Page 后,这些动画将继续播放,直到对 Page 进行垃圾回收。 根据正在使用的导航系统,导航离开的页面可能无限期地保留在内存中,在此期间始终通过动画消耗资源。 当页面包含不断运行的(“氛围”)动画时,这一点最明显。

出于此原因,在导航离开页面时,最好使用 Unloaded 事件删除动画。

删除动画有多种不同的方法。 以下技术可用于删除属于 Storyboard 的动画。

无论动画如何启动,都可以使用下一个技术。

有关对属性进行动画处理的不同方法的详细信息,请参阅属性动画技术概述

使用组合 HandoffBehavior 会消耗系统资源

当使用 ComposeHandoffBehaviorStoryboardAnimationTimelineAnimationClock 应用于属性时,之前与该属性关联的任何 Clock 对象都会继续消耗系统资源;计时系统不会自动删除这些时钟。

为避免在使用 Compose 应用大量时钟时出现性能问题,应该在完成后从经动画处理过的属性中删除组合时钟。 删除时钟有多种方法。

此动画问题主要出现在生存期较长的对象上。 当对某个对象进行垃圾回收时,它的时钟也会断开连接并进行垃圾回收。

有关时钟对象的详细信息,请参阅动画和计时系统概述

另请参阅