动画提示和技巧

更新:2007 年 11 月

在处理 WPF 中的动画时,可以借助于许多提示和技巧来优化动画的性能并避免出现挫折。

一般问题

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

如果您使用某个动画对滚动条或滑块的位置进行动画处理,而且该动画的 FillBehaviorHoldEnd(默认值),则用户将再也无法移动滚动条或滑块。原因在于,即使该动画已经结束,它仍将重写目标属性的基值。若要防止动画重写属性的当前值,请移除它或者为它的 FillBehavior 赋予 Stop。有关更多信息及示例,请参见如何:在使用演示图板对属性进行动画处理后设置该属性

对动画输出进行动画处理不起作用

如果某个对象是另一个动画的输出,则不能对该对象进行动画处理。例如,如果您使用 ObjectAnimationUsingKeyFramesRectangleFillRadialGradientBrush 动画处理为 SolidColorBrush,则将无法对 RadialGradientBrushSolidColorBrush 的任何属性进行动画处理。

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

在某些情况下,在对属性进行动画处理之后,即使动画已经结束,似乎也无法更改该属性的值。原因在于,即使该动画已经结束,它仍在重写该属性的基值。若要防止动画重写属性的当前值,请移除它或者为它的 FillBehavior 赋予 Stop。有关更多信息及示例,请参见如何:在使用演示图板对属性进行动画处理后设置该属性

更改时间线不起作用

尽管大部分 Timeline 属性是可以进行动画处理和数据绑定的,但是更改处于活动状态的 Timeline 的属性值似乎不起作用。原因在于,当 Timeline 开始时,计时系统会创建 Timeline 的一个副本,并使用该副本来创建一个 Clock 对象。修改原始对象不会影响到计时系统所创建的副本。

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

  • 如果时间线为 Storyboard 或属于它,您可以使用 BeginStoryboardBegin 方法来重新应用其演示图板,使它反映所做的更改。这会带来副作用,即还会重新启动动画。在代码中,可以使用 Seek 方法将演示图板移回到其从前的位置。

  • 如果使用 BeginAnimation 方法将动画直接应用于属性,请再次调用 BeginAnimation 方法并将修改后的动画传递给此方法。

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

有关时间线和时钟的更多信息,请参见动画和计时系统概述

FillBehavior.Stop 不按照预期方式工作

有时,将 FillBehavior 属性设置为 Stop(例如,将一个动画“切换”到另一个动画时)似乎不起作用,因为它的 SnapshotAndReplace 具有 HandoffBehavior 设置。

下面的示例创建一个 CanvasRectangleTranslateTransform。将对 TranslateTransform 进行动画处理,以便在 Canvas 中到处移动 Rectangle

<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) 将 TranslateTransformX 属性从 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) 也是对同一个 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。当由于使用 SnapshotAndReplace HandoffBehavior 而将第一个动画替换为第二个动画时,第一个动画的 FillBehavior 将不起作用。

FillBehavior 和 Completed 事件

随后的几个示例演示 Stop FillBehavior 似乎不起作用的另一个情形。该示例也是使用 Storyboard 将 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);
}

下面是用来将第二个 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 的动画。

无论动画的启动方式如何,都可以使用下面的方法。

  • 若要从特定的属性中移除动画,请使用 BeginAnimation(DependencyProperty, AnimationTimeline) 方法。将正进行动画处理的属性指定为第一个参数,将 null 指定为第二个参数。这样做将从属性中移除所有的动画时钟。

有关用来对属性进行动画处理的不同方法的更多信息,请参见属性动画技术概述

使用组合 HandoffBehavior 会占用系统资源

使用 Compose HandoffBehaviorStoryboardAnimationTimelineAnimationClock 应用到某个属性时,以前与该属性关联的任何 Clock 对象都会继续占用系统资源;计时系统不会自动移除这些时钟。

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

主要是生存期很长的对象上的动画会有此问题。 将对象作为垃圾回收时,同时将断开其时钟的连接并将其时钟作为垃圾回收。

有关时钟对象的更多信息,请参见动画和计时系统概述

请参见

概念

动画概述