动画

通过动画可以创建真正的动态用户界面。
动画是 WPF 模型的核心部分。这意味着为了让画面动起来,不需要使用计时器以及事件处理代码。而可以使用声明的方式创建动画,使用方便的类配置动画,使画面动起来而不编写任何 C# 代码。动画还能够将自身无缝的集成到普通的 WPF 窗口或者页面中。


理解 WPF 动画

在基于 Windows 的平台中(如 Windows 窗体和 MFC),开发人员曾经尝试从头开始构建自己的动画系统。最常用的技术是联合使用计时器和一些自定义的绘图逻辑。WPF 通过一个新的的基于属性的动画系统,改变了这种状况。

基于时间的动画

基于属性的动画

基本动画

WPF 动画的第一条规则 —— 每个动画依赖于依赖项属性。然而,还有另一个规则性限制。为了动画一个属性(换句话说,使用依据时间的方式改变属性的值),需要有一个支持该属性类型的动画类。

System.Windows.Media.Aniamtion 命名空间为希望使用的大多数数据类型提供了动画。

Animation 类

有两种类型的动画 —— 在一个开始值和结束值之间以逐步增加的方式(被称为线性插值的过程)改变属性的动画,以及从一个值突然改变到另外一个值的动画。

还有另一种动画类型。这种类型被称为基于路径的动画,并且它比使用插值或关键帧的动画更加专业。基于路径的动画修改数值使其符合由 PathGeometry 对象描述的形状,并且它主要用于沿着路径移动元素。

所有的关键帧动画类都使用 “类型名 + AnimationUsingKeyFrames” 形式命名,如 StringAnimationUsingKeyFrames 类。

基于路径的动画类使用 “类型名 + AnimationUsingPath” 的形式进行命名,如 DoubleAnimationUsingPath 动画类和 PointAnimationUsingPath 动画类。

在 System.Windows.Media.Animation 命名空间中将会发现以下内容:

  • 17 个线性插值动画类,这些类使用线性插值动画。
  • 22 个关键帧动画类,这些类使用关键帧动画。
  • 3 个路径动画类,这些类使用基于路径的动画。

这三种类中的每个类都继承自抽象的 TypeNameAnimationBase 类,这些基类实现了一些基本功能。从而为创建自己的动画类提供了一个快捷方式。如果数据类型支持多种类型的动画,那么所有的动画类都继承自抽象的基类。

使用代码创建动画

每个依赖项属性每次只能响应一个动画。如果开始第二个动画,第一个动画就会自动放弃。

在许多情况下,可能不希望动画从最初的 From 值开始。如下是两个常见的原因。

  • 创建一个能够被触发多次,并且逐次累加效果的动画。
  • 创建可能相互重叠的动画。

    <Window x:Class="WpfApplication1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="350" Width="525">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Button Content="Click and Make Me Grow" Height="23" HorizontalAlignment="Center"  Name="button1" VerticalAlignment="Center" Width="214" Click="button1_Click" />
            <Button Content="Storyboard by Trigger" Grid.Row="1" Height="23" HorizontalAlignment="Center"  Name="button2" VerticalAlignment="Center" Width="214">
                <Button.Triggers>
                    <EventTrigger RoutedEvent="Button.Click">
                        <EventTrigger.Actions>
                            <BeginStoryboard>
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetProperty="Width" To="300" Duration="0:0:5" />
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger.Actions>
                    </EventTrigger>
                </Button.Triggers>
            </Button>
        </Grid>
    </Window>
    
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
    using System.Windows.Media.Animation;
    
    namespace WpfApplication1
    {
        /// <summary>
        /// MainWindow.xaml 的交互逻辑
        /// </summary>
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
            }
    
            private void button1_Click(object sender, RoutedEventArgs e)
            {
                DoubleAnimation widthAnimation = new DoubleAnimation();
                widthAnimation.From = button1.ActualWidth;
                widthAnimation.To = this.Width - 30;
                widthAnimation.Duration = TimeSpan.FromSeconds(5);
                widthAnimation.DecelerationRatio = 0.7;
                button1.BeginAnimation(Button.WidthProperty, widthAnimation);
            }
        }
    }
    

同时发生的动画

可以使用 BeginAnimation() 方法同时加载多个动画。BeginAnimation() 方法几乎总是立即返回(不可靠)。可以通过创建绑定到同一个时间线的动画,突破这一限制。

动画生命周期

从技术上讲,WPF 动画是暂时的,这意味着它不能真正的改变基本属性的值。而动画是活动的,它只是简单的覆盖属性值。这是由依赖项属性的工作方式造成的,并且这是一个经常会被忽视的细节,该细节会给用户带来严重的困扰。

单向的动画在其运动结束之后会保持有效。这是因为需要使用动画包含按钮新的属性值。这会造成一个不常见的问题 —— 即,如果视图使用代码在动画完成之后修改属性值,修改属性的代码不会起作用。因为代码只是简单的为属性指定了一个新的本地值,但是仍然会优先使用动画之后的属性值。

解决方法

  • 创建将元素重新设置为其原始状态的动画。可以通过创建不设置 To 属性的动画达到该目的。
  • 创建翻转动画。通过将 AutoReverse 属性设置为 true 创建翻转动画。
  • 改变 FillBehavior 属性。通常,FillBehavior 属性被设置为 HoldEnd,这意味着当动画结束时,它继续为目标元素应用最后的数值。如果就 FillBehavior 属性修改为 Stop,只要动画结束,属性就会恢复为其原来的数值。

前三个选项改变了动画的行为。不管怎样,他们都将动画后的属性值返回到原来的数值。如果这不是希望的,那么久需要使用最后一种选择。

首先,在加载动画之前,管理事件处理程序响应动画完成事件:

widthAnimation.Completed += animation_Completed();

当引发 Completed 事件时,可以通过调用 BeginAnimation() 方法删除动画。为此,只需要简单的指定属性,并且为动画对象传递一个 null 引用。

Timeline 类

当播放音频或视频文件时使用 MediaTimeline 类。AnimationTimeline 分支中的类用于到目前为止分析过的基于属性的动画系统。而 TimelineGroup 分支中的类用于同步时间线并控制它们的回放。

Timeline 类的属性

  • BeginTime 设置被添加到动画开始之前的延迟时间(TimeSpan 类型)。这一延迟时间被加到总时间中,所以一个持续时间为 5 秒钟并且具有 5 秒延迟的动画,其总时间为 10 秒。当同步在同一时间开始但按顺序应用其效果的不同动画时,BeginTime 属性是很有用的。
  • Duration 使用 Duration 对象设置动画从开始到结束的运行时间。
  • SpeedRatio 增加或减少动画的速度。通常,SpeedRatio 属性值是 1。如果 SpeedRatio 属性的值为 5,动画的速度会变为原来的 5 倍。 如果减少该属性,动画会变慢(如果 SpeedRatio 属性的值为 0.5,动画时间将变为原来的两倍)。可以通过改变动画的 Duration 得到相同的结果。当应用 BeginTime 延迟时,不考虑 SpeedRatio 属性的值。
  • AccelerationRatio 和 DecelerationRatio 使动画不是线性的,从而开始时比较慢然后增加速度(通过增加 AccelerationRatio 属性值)或者结束时降低速度(通过增加 DecelerationRatio 属性值)。这两个属性的值都在0-1之间,并且开始时为 0。此外,这两个属性值之后不能超过1。
  • AutoReverse 如果为true,当动画完成时会自动以相反的方向播放。这也会使动画运行的时间加倍。如果增加了 SpeedRatio 属性值,它会应用到最初的动画播放以及反向的动画播放。BeginTime 属性值只应用于动画的开始 —— 不延迟反向动画。
  • FillBehavior 决定当动画结束时如何操作。通常,可以将属性值保持为固定的结束值(FillBehavior.HoldEnd),但是也可以选择将属性值返回到原来的数值(FillBehavior.Stop)。
  • RepeatBehavior 通过该属性可以使用指定次数或指定的时间间隔重复动画。用于设置这个属性的 RepeatBehavior 对象的属性决定了确切的行为。

声明式动画和故事板

故事板

故事板是增强的时间线。可以使用它分组多个动画,并且它还具有控制动画播放的能力 —— 暂停、停止以及改变播放位置。然而,StoryBoard 类提供的最基本的功能是,它能够使用 TargetProperty 属性和 TargetName 属性指向一个特点的属性和特定的元素。换句话说,故事板在动画和希望应用动画的属性之间架起了一座桥梁。

事件触发器

可以在如下 4 个位置定义事件触发器

  • 在样式中(Style.Triggers 集合)
  • 在数据模板中(DataTemplate.Triggers 集合)
  • 在控件模板中(ControlTemplate.Triggers 集合)
  • 直接在一个元素中定义事件触发器(FrameworkElement.Triggers 集合)

当创建事件触发器时,需要指定开始触发器的路由事件和由触发器执行的一个(或多个)动作。对于动画,最普通的动作是 BeginStoryBoard,该动作相当于调用 BeginAnimation() 方法。

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Button Content="Click and Make Me Grow" Height="23" HorizontalAlignment="Center"  Name="button1" VerticalAlignment="Center" Width="214" Click="button1_Click" />
        <Button Content="Storyboard by Trigger" Grid.Row="1" Height="23" HorizontalAlignment="Center"  Name="button2" VerticalAlignment="Center" Width="214">
            <Button.Triggers>
                <EventTrigger RoutedEvent="Button.Click">
                    <EventTrigger.Actions>
                        <BeginStoryboard>
                            <Storyboard>
                                <DoubleAnimation Storyboard.TargetProperty="Width" To="300" Duration="0:0:5" />
                            </Storyboard>
                        </BeginStoryboard>
                    </EventTrigger.Actions>
                </EventTrigger>
            </Button.Triggers>
        </Button>
    </Grid>
</Window>
  1. 使用样式关联触发器

    使用事件触发器时关联动画最常用的方式。但它不是唯一选择。如果使用位于样式、数据模板或者控件模板中的 Triggers集合,也可以创建一个当属性值发生改变时进行相应的属性触发器。

    可以使用两种方式为属性触发器关联动作。可以使用 Trigger.EnterActions 设置当属性改变到指定的数值时希望执行的动作,也可以使用 Trigger.ExitActions 设置当属性改变回原来的数值时执行的动作。这是一种包装一对互补动画的简便方法。

    在样式中并不是必须使用属性触发器。也可以使用事件触发器。

    不是必须定义与使用样式的按钮相分离的样式(也可以使用内部样式),但是这种两部分相分离的方法更加常用,并且提供了为多个元素应用相同动画的灵活性。

  2. 使用模板关联触发器

    一种重用动画的最强大的方式是通过在模板中定义动画。

    <Window x:Class="WpfApplication1.AnimationInTemplateWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="AnimationInTemplateWindow" Height="300" Width="300" Loaded="Window_Loaded">
        <Window.Resources>
            <Style TargetType="{x:Type ListBoxItem}">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type ListBoxItem}">
                            <Border x:Name="Border" ClipToBounds="True" BorderBrush="AliceBlue" BorderThickness="1" CornerRadius="3">
                                <ContentPresenter />
                            </Border>
                            <ControlTemplate.Triggers>
                                <EventTrigger RoutedEvent="ListBoxItem.MouseEnter">
                                    <EventTrigger.Actions>
                                        <BeginStoryboard>
                                            <Storyboard>
                                                <DoubleAnimation Storyboard.TargetProperty="FontSize" To="20" Duration="0:0:1" />
                                            </Storyboard>
                                        </BeginStoryboard>
                                    </EventTrigger.Actions>
                                </EventTrigger>
                                <EventTrigger RoutedEvent="ListBoxItem.MouseLeave">
                                    <EventTrigger.Actions>
                                        <BeginStoryboard>
                                            <Storyboard>
                                                <DoubleAnimation Storyboard.TargetProperty="FontSize" BeginTime="0:0:0.5" Duration="0:0:0.2" />
                                            </Storyboard>
                                        </BeginStoryboard>
                                    </EventTrigger.Actions>
                                </EventTrigger>
                                <Trigger Property="IsMouseOver" Value="True">
                                    <Setter TargetName="Border" Property="BorderBrush" Value="Blue" />
                                </Trigger>
                                <Trigger Property="IsSelected" Value="True">
                                    <Setter TargetName="Border" Property="Background" Value="Red" />
                                    <Setter TargetName="Border" Property="TextBlock.Foreground" Value="Gray" />
                                </Trigger>
                            </ControlTemplate.Triggers>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </Window.Resources>
        <Grid>
            <ListBox x:Name="lixtBox1">
    
            </ListBox>
        </Grid>
    </Window>
    
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Shapes;
    using System.Threading;
    using System.Collections.ObjectModel;
    using System.Collections;
    
    namespace WpfApplication1
    {
        /// <summary>
        /// AnimationInTemplateWindow.xaml 的交互逻辑
        /// </summary>
        public partial class AnimationInTemplateWindow : Window
        {
            public AnimationInTemplateWindow()
            {
                InitializeComponent();
            }
    
            private void Window_Loaded(object sender, RoutedEventArgs e)
            {
                this.Dispatcher.BeginInvoke((ThreadStart)delegate(){
                    lixtBox1.ItemsSource = new ObservableCollection<int>(Enumerable.Range(1, 10));
                });
            }
        }
    }
    

重叠动画

故事板提供了改变处理重写动画方式的能力 —— 换句话说就是,它可以决定第二个动画何时被应用到已经具有一个正在运行的动画的属性上。可以使用 BeginStoryboard.HandoffBehavior 属性改变处理重叠动画的方式。

通常,当两个动画相互重叠时,第二个动画会立即覆盖第一个动画。这种行为就是所谓的快照并替换(由 HandoffBehavior 枚举中的 SnapshotAndReplace 值表示)。当第二个动画开始时,第二个动画获取属性当前值(根据第一个动画)的快照,停止第一个动画,并使用新的的动画替换第一个动画。

另外一个 HandoffBehavior 选择是 Compose,这种方式会将第二个动画融合到第一个动画的时间线中。使用组合的 HandoffBehavior 行为需要更大的开销。因为当第二个动画开始时,用于运行原来动画的时钟不能被释放。相反,这个时钟会继续保持存活知道应用动画的对象呗垃圾回收或者为相同的属性应用了一个新的动画。

若性能变成了问题,WPF 团队推荐一旦动画完成就收到为动画释放动画时钟(而不是等到垃圾回收期回收它们)。

同时发生的动画

Storyboard 类间接的继承自 TimelineGroup 类,所以 Storyboard 类具有包含多个动画的功能。而且这些动画可以作为一组进行管理 —— 这意味着它们在相同的时间开始。

控制播放

到目前为止,已经在事件触发器中使用了一个动作 —— 加载动画的 BeginStoryboard 动作。然而,一旦创建了故事板,还可以使用其他动作控制故事板。这些动作都继承自 ControllableStoryboardAction 类。

  • PauseStoryboard 停止播放动画并保持它的当前位置。
  • ResumeStoryboard 恢复播放暂停的动画。
  • StopStoryboard 停止播放动画,并且将动画时钟重新设置到开始点。
  • SeekStoryboard 跳动动画时间线中的特定位置。如果动画当前正在播放,它继续从新位置播放。如果动画当前是暂停的,它继续保持暂停。
  • SetStoryboardSpeedRatio 改变整个故事板(而不仅仅是改变一个内部动画)的 SpeedRatio 属性值
  • SkipStoryboardToFill 将故事板移动到时间线的终点。从技术上讲,这个时期就是所谓的填充期(fill region)。对于标准的动画,FillBehavior 属性设置为 HoldEnd,动画继续保持到最后的值。
  • RemoveStoryboard 移除故事板,停止所有正在运行的动画并将属性返回其原来的、最后一次设置的数值。这和对恰当的元素使用 null 动画对象调用 BeginAnimation() 方法的效果相同。

停止一个动画并不等于完成一个动画(除非将 FillBehavior 属性设置为 Stop)。这是因为即使一个动画到达其时间线的终点,它仍然应用其最后的值。类似的,当一个动画暂停时,它继续应用最近的中间值。然而,当一个动画停止时,它不再应用任何数值,并且属性值会恢复为动画之前的值。

为了让这些动画成功的完成,必须在同一个 Triggers 集合中定义所有的触发器。如果将 Storyboard 动作的触发器和 PauseStoryboard 动作的触发器放置到不同的集合中,PauseStoryboard 动作就不能完成。

<Window x:Class="WpfApplication1.AnimationWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="AnimationWindow" Height="300" Width="300">
    <Window.Triggers>
        <EventTrigger SourceName="StartButton" RoutedEvent="Button.Click">
            <BeginStoryboard Name="fadeStoryboardBegin">
                <Storyboard>
                    <DoubleAnimation Storyboard.TargetName="imgDay" Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:10" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
        <EventTrigger SourceName="PauseButton" RoutedEvent="Button.Click">
            <PauseStoryboard BeginStoryboardName="fadeStoryboardBegin" />
        </EventTrigger>
        <EventTrigger SourceName="ResumeButton" RoutedEvent="Button.Click">
            <ResumeStoryboard BeginStoryboardName="fadeStoryboardBegin" />
        </EventTrigger>
        <EventTrigger SourceName="StopButton" RoutedEvent="Button.Click">
            <StopStoryboard BeginStoryboardName="fadeStoryboardBegin" />
        </EventTrigger>
        <EventTrigger SourceName="MiddleButton" RoutedEvent="Button.Click">
            <SeekStoryboard BeginStoryboardName="fadeStoryboardBegin" Offset="0:0:5" />
        </EventTrigger>
    </Window.Triggers>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"  />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Image x:Name="imgNight" Source="night.jpg" Stretch="Fill" />
        <Image x:Name="imgDay" Source="day.jpg" Stretch="Fill" />
        <StackPanel Orientation="Horizontal" Grid.Row="1">
            <Button Content="开始" x:Name="StartButton" />
            <Button Content="暂停" x:Name="PauseButton" />
            <Button Content="恢复" x:Name="ResumeButton" />
            <Button Content="停止" x:Name="StopButton" />
            <Button Content="移到中间" x:Name="MiddleButton" />
        </StackPanel>
    </Grid>
</Window>

注意,必须要为 BeginStoryboard 动画提供一个名称。其他触发器通过 BeginStoryboard 的名称,连接到相同的故事板。

当使用故事板动作时将会遇到一个限制。它们提供的属性(如 SeekStoryboard.Offset 属性和 SetStoryboardSpeedRatio 属性)不是依赖属性,这会限制使用数据绑定表达式。例如不能自动读取 Slider.Value 属性值,并将其应用到 SetStoryboardSpeedRatio.SpeedRatio 动作,因为 SpeedRatio 属性不接受数据绑定表达式。通过代码使用 Storyboard 对象的 SpeedRatio 属性也是不能解决这个问题的。因为当动画开始时,读取 SpeedRatio 值并创建一个时钟动画。在此之后,即使改变了 SpeedRatio 属性的值,动画仍然会继续保持正常的速度。

如果希望动态调整速度和位置,唯一的解决方法是使用代码。Storyboard 类中的方法提供了和触发器相同的功能,包括 Begin() 方法、Pause() 方法、Resume() 方法、Seek() 方法、Stop() 方法、SkipToFill() 方法、SetSpeedRatio() 方法以及 Remove() 方法。

为了访问 Storyboard 对象,需要确保在其标记中设置其 Name 属性。

不要将 Storyboard 对象的名称(在代码中使用故事板时需要该名称)和 BeginStoryboard 动作的名称(为控制故事面板的触发器关联动作时需要改名称)相混淆。

擦除效果

<Window x:Class="WpfApplication1.EraseAnimationWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="EraseAnimationWindow" Height="300" Width="300">
    <Window.Triggers>
        <EventTrigger SourceName="StartButton" RoutedEvent="Button.Click">
            <BeginStoryboard Name="fadeStoryboardBegin">
                <Storyboard>
                    <DoubleAnimation Storyboard.TargetName="visibleStop" Storyboard.TargetProperty="Offset" From="0" To="1.2" Duration="0:0:12" />
                    <DoubleAnimation Storyboard.TargetName="transparentStop" BeginTime="0:0:2" Storyboard.TargetProperty="Offset" From="0" To="1" Duration="0:0:10" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
        <EventTrigger SourceName="PauseButton" RoutedEvent="Button.Click">
            <PauseStoryboard BeginStoryboardName="fadeStoryboardBegin" />
        </EventTrigger>
        <EventTrigger SourceName="ResumeButton" RoutedEvent="Button.Click">
            <ResumeStoryboard BeginStoryboardName="fadeStoryboardBegin" />
        </EventTrigger>
        <EventTrigger SourceName="StopButton" RoutedEvent="Button.Click">
            <StopStoryboard BeginStoryboardName="fadeStoryboardBegin" />
        </EventTrigger>
        <EventTrigger SourceName="MiddleButton" RoutedEvent="Button.Click">
            <SeekStoryboard BeginStoryboardName="fadeStoryboardBegin" Offset="0:0:5" />
        </EventTrigger>
    </Window.Triggers>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"  />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Image x:Name="imgNight" Source="night.jpg" Stretch="Fill" >
        </Image>
        <Image x:Name="imgDay" Source="day.jpg" Stretch="Fill" >
            <Image.OpacityMask>
                <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
                    <GradientStop Offset="0" Color="Transparent" x:Name="transparentStop" />
                    <GradientStop Offset="0" Color="Black" x:Name="visibleStop" />
                </LinearGradientBrush>
            </Image.OpacityMask>
        </Image>
        <StackPanel Orientation="Horizontal" Grid.Row="1" HorizontalAlignment="Center" Margin="5">
            <Button Content="开始" x:Name="StartButton" />
            <Button Content="暂停" x:Name="PauseButton" />
            <Button Content="恢复" x:Name="ResumeButton" />
            <Button Content="停止" x:Name="StopButton" />
            <Button Content="移到中间" x:Name="MiddleButton" />
        </StackPanel>
    </Grid>
</Window>

监视动画进度

从故事板中检索当前动画时钟的唯一方法是使用类似 GetCurrentTime() 和 GetCurrentProgress() 方法。无法从属性中获取相同的信息。

故事板事件说明

  • Completed

    动画到达终点。

  • CurrentGlobalSpeedInvalidated

    速度发生了变化,或者动画被暂停、重新开始、停止或者移动到了一个新位置。当动画时钟反转时以及当它加速和减速时,也会引发该事件。

  • CurrentStateInvalidated

    动画开始或结束。

  • CurrentTimeInvalidated

    动画时钟已经向前移动了一个步长,正在更改动画。当动画开始、停止或者结束也会引发该事件。(通常,每秒移动 60 次。但是如果执行的代码需要更多的时间,可能会丢失时间刻度)。

  • RemoveRequested

    动画正在被移除。使用动画的属性会随后返回到其原来的值。

实例

<Window x:Class="WpfApplication1.MonitorAnimationWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MonitorAnimationWindow" Height="300" Width="300">
    <Window.Triggers>
        <EventTrigger SourceName="StartButton" RoutedEvent="Button.Click">
            <BeginStoryboard Name="fadeStoryboardBegin">
                <Storyboard x:Name="fadeStoryboard" CurrentTimeInvalidated="fadeStoryboard_CurrentTimeInvalidated">
                    <DoubleAnimation Storyboard.TargetName="imgDay" Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:10" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
        <EventTrigger SourceName="PauseButton" RoutedEvent="Button.Click">
            <PauseStoryboard BeginStoryboardName="fadeStoryboardBegin" />
        </EventTrigger>
        <EventTrigger SourceName="ResumeButton" RoutedEvent="Button.Click">
            <ResumeStoryboard BeginStoryboardName="fadeStoryboardBegin" />
        </EventTrigger>
        <EventTrigger SourceName="StopButton" RoutedEvent="Button.Click">
            <StopStoryboard BeginStoryboardName="fadeStoryboardBegin" />
        </EventTrigger>
        <EventTrigger SourceName="MiddleButton" RoutedEvent="Button.Click">
            <SeekStoryboard BeginStoryboardName="fadeStoryboardBegin" Offset="0:0:5" />
        </EventTrigger>
    </Window.Triggers>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"  />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Image x:Name="imgNight" Source="night.jpg" Stretch="Fill" />
        <Image x:Name="imgDay" Source="day.jpg" Stretch="Fill" />
        <StackPanel Orientation="Horizontal" Grid.Row="1" HorizontalAlignment="Center" Margin="5">
            <Button Content="开始" x:Name="StartButton" />
            <Button Content="暂停" x:Name="PauseButton" />
            <Button Content="恢复" x:Name="ResumeButton" />
            <Button Content="停止" x:Name="StopButton" />
            <Button Content="移到中间" x:Name="MiddleButton" />
        </StackPanel>
        <StackPanel Grid.Row="2" HorizontalAlignment="Center" Margin="5">
            <TextBlock x:Name="tbTime" Text="00:00:00" />
            <ProgressBar Minimum="0" Width="150" Height="20" Maximum="1" x:Name="pbTime" />
            <Slider x:Name="timeSlider" Minimum="0" Maximum="1" />
        </StackPanel>
    </Grid>
</Window>

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Media.Animation;

namespace WpfApplication1
{
    /// <summary>
    /// MonitorAnimationWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MonitorAnimationWindow : Window
    {
        public MonitorAnimationWindow()
        {
            InitializeComponent();
        }

        /// <summary>
        /// 动画时钟向前移动
        /// </summary>
        /// <param name="sender">Clock 对象</param>
        /// <param name="e"></param>
        private void fadeStoryboard_CurrentTimeInvalidated(object sender, EventArgs e)
        {
            Clock clock = (Clock)sender;
            if(clock.CurrentProgress==null)
            {
                tbTime.Text = "[[ Stoped !]]";
                pbTime.Value = 0;
                timeSlider.Value = 0;
            }
            else
            {
                tbTime.Text = clock.CurrentTime.ToString();
                pbTime.Value = (double)clock.CurrentProgress;
                timeSlider.Value = (double)clock.CurrentProgress;
            }
        }
    }
}

期望的帧速率

WPF 试图保持以 60 帧/秒的速度运行动画。这样可以确保从开始到结束得到平滑、流畅的动画。当然,WPF 不可能实现这一目标。如果同时运行多个复杂的动画,并且 CPU 或者显卡不能承受的话,整个帧速率会下降(最好的情形),甚至可能会跳跃以进行补偿(最坏的情形)。

尽管很少增加帧速率,但是可能会选择降低帧速率,这可能是因为以下两个原因之一:

  • 动画使用更低的帧速率看起来也很好,因为不希望浪费额外的 CPU 周期。
  • 应用程序运行在性能较差的 CPU 或显卡上,并且知道使用高的帧速率时这个动画的渲染效果还不如使用更低的帧速率渲染的效果好。

开发人员有时会任务 WPF 提供了用于根据显卡硬件的性能降低帧速率的代码。然后情况并非如此。相反,WPF 总是试图保持 60 帧/秒,除非显式的告诉它使用其他帧速率。

调节帧速率很容易。可以简单的为包含动画的故事板使用 Timeline.DesiredFrameRate 附加属性。下面的示例将帧速率减半。

<Storyboard Timeline.DesiredFrameRate="30" />

实例

<Window x:Class="WpfApplication1.FrameRatesWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="FrameRatesWindow" Height="300" Width="300">
    <Window.Resources>
        <BeginStoryboard x:Key="beginStoryboard">
            <Storyboard Timeline.DesiredFrameRate="{Binding ElementName=txtFrameRate,Path=Text}">
                <DoubleAnimation Storyboard.TargetName="ellipse" Storyboard.TargetProperty="(Canvas.Left)" From="0" To="300" Duration="0:0:5" />
                <DoubleAnimation Storyboard.TargetName="ellipse" Storyboard.TargetProperty="(Canvas.Top)" From="300" To="0" Duration="0:0:2.5" />
            </Storyboard>
        </BeginStoryboard>
    </Window.Resources>
    <Window.Triggers>
        <EventTrigger RoutedEvent="Window.Loaded">
            <EventTrigger.Actions>
                <StaticResource ResourceKey="beginStoryboard" />
            </EventTrigger.Actions>
        </EventTrigger>
        <EventTrigger RoutedEvent="Button.Click">
            <EventTrigger.Actions>
                <StaticResource ResourceKey="beginStoryboard" />
            </EventTrigger.Actions>
        </EventTrigger>
    </Window.Triggers>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
            <!--<RowDefinition Height="auto" />-->
        </Grid.RowDefinitions>
        <Canvas ClipToBounds="True">
            <Ellipse Name="ellipse" Fill="Red" Width="10" Height="10" />
        </Canvas>
        <StackPanel Grid.Row="1">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="期望速率" />
                <TextBox Name="txtFrameRate" Width="150" />
            </StackPanel>
            <Button Content="重复" />
        </StackPanel>
    </Grid>
</Window>

知识共享许可协议
《WPF 动画学习笔记 1》 常伟华 创作。
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议 | 3.0 中国大陆许可协议进行许可。

站内公告