WPF,Silverlight与XAML读书笔记第四十 可视化效果之动画

说明:本系列基本上是《WPF揭秘》的读书笔记。在结构安排与文章内容上参照《WPF揭秘》的编排,对内容进行了总结并加入一些个人理解。

动画(Animation)专业点说就是结合定时的API实现对象移动等效果,简单的说就是使对象动起来,包括对象颜色,大小,透明度及其它属性的变化,我们可以控制这个变化的持续时间,并使对像在这个过程中可以响应用户输入。在WPF中,动画被明确定义为随着时间推移变化属性的值。在WPF中实现动画有很多种方式:

手工实现动画

    在WPF中实现动画最原始的方法是使用定时器,并且在每次"Tick"时调用回调函数,并在回调函数中更新相关属性,并且当属性变化到目标时,停止定时器,并取消事件处理函数的订阅。所采用的计时器既可以是.NET基础类库System.Threading或System.Timers中传统的Timer,也可以是WPF自带的DispatherTimer。它们在使用上基本类似,都实现设置Interval属性,然后为Tick事件订阅时间处理函数。

提示:WPF内置的DispatherTimer与.NET中其他Timer区别最主要的一点,DispatcherTimer的处理函数在UI线程上被调用,对于客户端程序开发,在操作UI元素时,就能避免跨线程问题,在回调函数中直接操作UI元素。反之我们需要如下代码将操作放到UI线程上。

1 void Callback(object sender,EventArgs e)
2 {
3     Dispatcher.Invoke(DispatcherPriority.Normal, new TimerDispatcherDelegate(DoTheRealWork));
4 }

另外,默认情况下,DispatcherPriority这个枚举类型使用Background这个优先级。如果想让回调函数的处理有不同的优先级,可以通过构造DispatcherTimer时显式传入不同的DispatherPriority来实现。

    上述方法存在两个根本缺点:不能与WPF的渲染引擎同步;无法根据显示器的垂直刷新率进行同步。

    另一种实现动画的方法是,通过System.Windows.Media.CompostionTarget的Rendering事件。这个事件在布局后的渲染过程中每帧触发一次,注意,这个触发是在添加了事件处理函数之后才会发生,原本基于WPF保留模式的原理,当UI失效后WPF才会触发渲染。通过处理这个事件可以实现基于帧的动画。当要实现大量高精度的动画时(如碰撞检测等对物理效果的模拟),使用这种方法甚至好过后面要介绍的动画类。另外面板上元素布局的变化的动画也常使用这种方式实现。

    介绍了两种实现动画的方法,下面着重介绍WPF中实现动画最"正宗"的方式 – 使用System.Windows.Media.Animation命名空间下的动画类。

首先,我们需要了解使用动画类两个关键点:

  • 通过动画类,只能改变一个依赖属性的值
  • 通过动画类实现的动画速度与"时间速度"无关。通俗点说,动画不会在硬件变快时也变得更快,而会更平滑,这个帧率由WPF内部根据多个条件控制。(一个对比的例子是flash,如一个flash游戏,你会发现在性能越高的机子上flash东湖速度越快)

System.Windows.Media.Animation下包含了很多不同的动画类,它们看起来差不多,因为不同的数据类型要通过不同的动画类来实现动画。WPF针对22种不同的数据类型内置了动画类,这些类型见下表:

与.NET核心数据类型相关

WPF数据类型

Boolean

Thickness

Byte

Color

Char

Size

Decimal

Rect

Int16

Point

Int32

Point3D

Int64

Vector

Single

Vector3D

Double

Rotationi3D

String

Matrix

Object

Quaternion

下面我们以其中的三种为例,介绍它们的使用。实现动画可以通过C#代码,也可以通过XAML。首先我们来看使用C#代码实现动画,我们变换的目标很简单,一个Button:

1 <Canvas>
2     <Button x:Name="btn">示例</Button>
3 </Canvas>

通过DoubleAnimation实现动画的代码也很简单:

 1 public class Window1 : Window
 2 {
 3     public Window1()
 4     {
 5         InitializeComponent();
 6         //定义动画
 7         DoubleAnimation a = new DoubleAnimation();
 8         a.From = 50;
 9         a.To = 100;
10         //开始动画
11         b.BeginAnimation(Button.WidthProperty, a);
12     }
13 }

    这是DoubleAnimation最简单的应用,其中涉及到From,To两个最基本的属性。

首先我们说一下与From,To属性相关的更多话题。在刚刚我们定义的动画中,From属性由50开始。如果在触发动画时,Buttton的ActualWidth不为50,则WPF会先将按钮的Width由ActualWidth的值变为50,而这也会产生一个跳跃感。解决方法很简单,可以将设置From属性的代码注释,如此动画将以Button的当前宽度(ActualWidth)作为初始值开始。(即使Button的当前宽度大于To值也可以,动画将是Button宽度缩短的过程。)特别注意使用这种方法时一定要给Button的Width属性显式设置一个值,否则一些情况下虽然通过布局,Button看起来是有一定宽度(为填充容器而被拉伸),但Width的值为NaN。

提示:忽略From设置使动画平滑非常重要,特别是动画用来响应用户动作并会重复被触发时。这也很好理解,如有两次连续的点击,第二次发生在第一次动画进行到一般的时候,在忽略From的情况下,第二次点击的动画会接着第一次动画进行到的位置继续,而不是回到一个指定的初始值从头开始。又如我们给一个按钮设置了鼠标移入时放大,鼠标离开后缩小,省略From也会避免按钮发生跳变。

另外,To属性的设置也是可选的,如下面代码:

1 DoubleAnimation a = new DoubleAnimation();
2 a.From = 50;
3 //a.To = 100;
4 a.Duration = new Duration(TimeSpan.Parse("0:0:5"));

代码中设置的Button在动画进行时,宽度会由50变化为其Width属性指定的值(隐式目标值)。

By属性:通过使用这个属性,我们可以直接指定变化幅度,而非目标值。在指定了By属性(省略From设置)的情况下,目标依赖属性将由当前值变换到当前值加By值。

接着我们看一下其它有趣的属性及事件。

  • Duration属性:该属性用来定义动画的持续时间,默认值是1秒钟。DoubleAnimation通过线性内插在持续时间内平滑的改变double类型的依赖属性值。内部一个函数定期调用来完成这些变换。

    这个DoubleAnimation的对象可以重用,我们可以将其传入其它要想添加动画的对象的BeginAnimation方法。

使用代码设置设置Duration的方法如下:

1 a.Duration = new Duration(TimeSpan.Parse("0:0:5"));
 

如代码所示,Duration构造函数所需的是一个TimpSpan对象。

提示:TimeSpan的Parse方法可以接受很多种格式的字符串并将其转换为相应的TimeSpan对象。标准字符串的格式形如:"天.时:分:秒.小数"。所以,如"2"表示2天;而"2.5"表示两天5小时;"0:2"表示2分钟;"0:0:2"表示2秒钟。

另外,本部分很多示例都会使用TimeSpan.Parse方法,主要因为其支持解析的字符串与TimeSpan转换器支持的相同,可以很方便的将字符串在C#与XAML间复用,如单在C#中使用,可以通过TimeSpan的FromSeconds等方法提高方便性。

提示:Duration与TimeSpan的区别

Duration比TimeSpan多了如下两个值:Duration.Automatic和Duration.Forever。它们用于后文将要介绍的Storyboard等复杂场景中。

  • Duration.Automatic - 是所有动画类Duration属性的默认值,即上文提到的1秒的TimeSpan。
  • Duration.Forever – 当如DoubleAnimation这样的动画类的Duration属性被设置为这个值后,会使动画类控制的依赖属性停留在初始值。WPF无法在当前时间与结束时间内作内插。这个值一般用于复杂的动画类。

 

  • BeginTime属性:当需要在调用BeginAnimation后延时开始一个动画,TimeSpan类型的BeginTime属性就是你需要的。该属性的设置同样简单:
    1 a.BeginTime = TimeSpan.Parse("0:0:5");

    BeginTime也可以设置为负值,表示从中间起开始播放动画,如:

    1 a.BeginTime = TimeSpan.Parse("-0:0:2.5");

    表示动画将从2.5秒开始。

  • SpeedRatio属性

    该属性可以用于调整动画的速度,默认值为1。当设置值小于1(大于0)时会减慢动画速度,大于1时会加快动画速度相应的倍数。该属性作用的原理就是按倍数调整了Duration。注意,SpeedRatio不改变BeginTime作用范围外的时间设置。

例如:

1 DoubleAnimation a = new DoubleAnimation();
2 a.BeginTime = TimeSpan.Parse("0:0:5");
3 //使动画快2倍
4 a.SpeedRatio = 2;
5 a.From = 50;
6 a.To = 100;
7 a.Duration = new Duration(TimeSpan.Parse("0:0:5"));

如代码,动画仍然会在5秒后开始,但原本5秒的动画会在2.5秒内完成。

  • AutoReverse属性

    这个属性用来控制回放效果,回放会用与动画相同的时间把属性由To值到From值进行变化。注意上述SpeedRatio属性的值也会影响回放的速度。而BeginTime不会对回放产生延迟效果,回放总是在正常动画完成后立即开始。

  • RepeatBehavior

    RepeatBehavior可以实现几种动画播放模式。

    • 将动画重复一定次数
    • 将动画重复一定时间
    • 提前切断动画的播放

    RepeatBehavior有一个构造函数接受double类型的参数,用来设置动画重复的小数,由于是double类型,我们可以设置如2.5这样的数值是最后一遍重复仅执行一半。RepeatBehavior另一个构造函数接受TimeSpan类型的参数,用来指定动画及重复总共的时间,当指定的时间小于1次动画所需的时间即得到被切断的效果。

    注意对于使用TimeSpan构造的RepeatBehavior,SpeedRatio不会缩短动画总共时间,但其会影响播放速度,从而会影响播放的次数。

另外,RepeatBehavior属性也可以被设置为RepeatBehavior.Forever,这样动画会无限重复。

提示:由于决定动画的因素有前面介绍过的多个属性来影响,所以计算动画的实际持续时间需要考虑多个因素。下面两个公式基本总结了这些情况:

  • 当RepeatBehavior使用double值初始化时

动画全过程时间=

  • 当RepeatBehavior使用TimeSpan值初始化

动画全过程时间=BeginTime+RepeatBehavior

如果此时AutoReverse属性也设置为true,则回放也会与原动画一起被重复。但BeginTime设置的延迟不会被重复。

 

  • AccelerationRatio和DecelerationRatio属性

    默认情况下,动画值的变化是线性的,而AccelerationRatio和DecelerationRatio就是用来使变化变为非线性的属性。AccelerationRatio表示目标值由初始值开始加速变化持续的时间的百分比。而DecelerationRatio表示目标值由开始减速变化到目标值持续时间所占的百分比。显然这两个属性的和要小于100%。

  • IsAdditive属性

    默认值false,表示目标属性的当前值是否应该被添加为动画的From属性值。

  • IsCumulative属性

    与IsAdditive类似,但仅用于与RepeatBehavior一起使用。举个例子,如通过RepeatBehavior将一个50到100的动画重复3遍。当该属性设为true(且AutoReverse设为false)时,目标属性的值会由50逐渐变到200。

  • FillBehavior属性

    此属性默认值HoldEnd,表示动画结束时停在最后,当设置为Stop时,动画在结束后会跳回开始值。

总结下,WPF提供了许多功能看似重复的属性,正是为了可以更方便的在下面要介绍的XAML方式中通过数据绑定实现动画。

除了在代码中实现动画,通过XAML实现动画是更常见的做法。动画的内容在实际应用中一般放置在资源中,这样可以方便复用并且可以方便的在C#代码中访问动画(如调用BeginAnimation开始一个动画,并且可以随时使用代码改变XAML中定义的属性来动态改变动画),另外,WPF/Silverlight完全支持在XAML中初始化动画,这是由下文要介绍的EvnetTrigger类支持的。关键在于EventTrigger支持通过Actions属性来设置动画(Actinos也是EventTrigger唯一可以包含的东西,但3种类型的Trigger都可以包含Action)。

在XAML中,通过改变基于时间的一些属性可以使一个对象实现动画。时间通过时间线来进行定义,例如,让一个对象在5秒过程中由屏幕左侧移动到右侧,则可以通过一个5秒的时间线设置Canvas.Left属性值由0变为Canvas.Width。

下面将逐一介绍实现动画的关键点

Trigger与Event Trigger

Trigger用来定义触发事件,事件触发动画。(当前Silverlight只支持Event Trigger这一种Trigger。)每个UI对象都有一个名为Trigger的属性,可以在其中定义一个或多个事件触发器(Event Trigger)。

在一个对象上实现动画第一步就是定义事件触发器容器:

1 <Rectangle x:Name="rect" Fill="Pink" Width="100" Height="100">
2     <Rectangle.Triggers>
3                 
4     </Rectangle.Triggers>
5 </Rectangle>

接着定义一个EventTrigger放到事件触发器容器中,通过Event Trigger的RoutedEvent属性定义一个触发动画的事件。对于此处的Rectangle元素只支持Loaded事件作为RoutedEvent,如果是其他如Button等的元素,则可支持Button.Click这样的事件),当此事件发生时,下文介绍的Actions属性定义的动画就被触发了。

1 <EventTrigger RoutedEvent="Rectangle.Loaded">
2 </EventTrigger>

故事版Storyboard

BeginStoryboard是一种事件触发动作,其中包含了定义动画详细信息的Storyboard,一个Storyboard中可以包含许多条时间线(TimeLine),这正是所有Animation共享的基类,而Storyboard的内容属性Children正是TimeLine集合类型。这为我们提供了极大的便利:我们可以不必使用在Trigger中包含多个BeginStoryboard的方式来将多个动画结合在一起,只需要在一个Storyboard中添加不同的Animation就好了,而且借助TargetName,TargetProperty和BeginTime等附加属性我们可以单独给每个动画指定目标、设置开始时间。把BeginStoryboard放在EventTrigger就可以很容易的完成一个动画的定义。

这些元素实现的功能与上文我们在C#代码中使用的BeginAnimation等价,Storyboard的TargetProperty指定了动画关联的依赖属性。而BeginStoryboard将动画(Storyboard)关联到事件触发器上,不能将Storyboard或DoubleAnimation直接应用到EventTrigger的原因,在与它们都不是Action对象。而且,只有动画(Animation)放置在Storyboard中,才能被由XAML中初始化。示例代码:

 1 <Rectangle x:Name="rect" Fill="Pink" Width="100" Height="100">
 2     <Rectangle.Triggers>
 3         <EventTrigger RoutedEvent="Rectangle.Loaded">
 4             <BeginStoryboard>
 5                 <Storyboard>
 6                         
 7                 </Storyboard>
 8             </BeginStoryboard>
 9         </EventTrigger>
10     </Rectangle.Triggers>
11 </Rectangle>

BeginStoryboard继承自TriggerAction类,用于设置EventTrigger的Actions属性(Actions被定义为EventTrigger的内容属性)。同样继承自TriggerAction的类还有故事版相关的如下命令(Command):

  • PauseStoryboard:该命令暂停故事板的动画,
  • ResumeStoryboard:当故事板处于暂停时,该命令用于恢复
  • SeekStoryboard:定位动画

这些都是可以与一起使用的。

上面介绍的内容已经建立起了动画的框架,下面要介绍的都是与具体动画样式定义相关的对象。

与C#代码定义动画方式做个对比,原本BeginAnimation所完成的制定目标依赖属性(TargetProperty)及将动画关联到触发器并指定什么时候开始的人物分别由Storyboard的TargetProperty属性和BeginStoryboard元素通过Storyboard及其属性来完成。

动画(变化)类型

  • Double类型变化

这种变化的目标是对象中double类型的属性,如Canvas.Left或Opacity,有如下两类:

  • DoubleAnimation
  • DoubleAnimationUsingKeyFrames
  • Color类型变化 这种变化的目标是对象中Color类型的属性,如背景色或描边颜色,有如下两类:
    • ColorAnimation
    • ColorAnimationUsingKeyFrames
  • Point类型变化 这种变化的目标是对象中Point类型的属性,如线段的StartPoint等,有如下两类:
    • PointAnimation
    • PointAnimationUsingKeyFrames

所有以上这些类型都定义了一个From属性(如果不定义默认为当前属性参数)与To属性分别表示属性变化的初始与结束值。或者通过By参数定义一个特性的属性参数。以下所述的操作都是基于这六类动画类型。

定义动画对象(和属性)

    为了方便介绍我们以DoubleAnimation为例,对于其它动画类型是一样的。DoubleAnimation通过2个附加属性Storyboard.TargetName与Storyboard.TargetProperty分别定义动画的目标对象与目标对象的属性。如名字所示这两个属性定义于Storyboard中,但作为附加属性,它们可以应用于任意Storyboard的子元素中。这样可以为同一个Storyboard中不同的Animation指定不同的目标对象。

TargetName接收的名称是目标对象中使用x:Name属性定义的名称。当不设置TargetName时,表示目标对象为定义触发器的对象。注意,当把动画放置在Style中时,需要显示设置TargetName为想要应用动画的对象。因为这时,对象目标默认为模板对象,这一般不是你想要的结果。

TargetProperty的类型为PropertyPath,可以支持各种或简单或复杂的情况,如一个带有许多子属性的属性。这个Property不需要转换器就可以工作,但我们一定要保证设置的属性存在且可访问。如果想要变化的是目标对象的属性是一个附加属性,需要将这个附加属性使用括号括起来。例如,下面的例子中在名为rect的矩形对象的Canvas.Left附加属性上定义了一个Double类型的动画:

1 <DoubleAnimation Storyboard.TargetName="rect" Storyboard.TargetProperty="(Canvas.Left)"/>

定义动画时间

    DoubleAnimation中的Duration属性用来设置动画的持续时间,属性的格式为HH:MM:SS,下面的例子在上一个例子基础上增加了持续时间5秒的定义,XAML中设置的是00:00:05,也可简写为0:0:5:

1 <DoubleAnimation Storyboard.TargetName="rect" Storyboard.TargetProperty="(Canvas.Left)" Duration="00:00:05"/>

设置动画的开始时间

如果不希望动画立即执行,可以指定给DoubleAnimation的BeginTime属性一个时间表示多长时间后开始动画。BeginTime的格式与Duration属性相同,依然我们在上面例子的基础上添加这个属性:

1 <DoubleAnimation Storyboard.TargetName="rect" Storyboard.TargetProperty="(Canvas.Left)" Duration="00:00:05" BeginTime="0:0:5"/>

这次我们使用了简写的时间格式。

设置动画速率

使用SpeedRatio属性可以改变动画的速率,例如对于前面那个5秒时长的动画,如果将SpeedRatio设置为2,则动画持续时间会变为10秒,而如果SpeedRatio被设置为0.2,则持续时间会缩短为1秒。代码如下:

1 <DoubleAnimation Storyboard.TargetName="rect" Storyboard.TargetProperty="(Canvas.Left)" Duration="00:00:05" BeginTime="0:0:5" SpeedRatio="0.2"/>

设置自动反转

将AutoReverse属性设置为True可以使定义的动画按相反的方式自动播放,这个反向播放是在正向播放进行完成后自动进行,所以如果一个持续5秒的动画的AutoReverse被设置True后,整个播放时间会变为10秒,示例代码:

1 <DoubleAnimation Storyboard.TargetName="rect" Storyboard.TargetProperty="(Canvas.Left)" Duration="00:00:05" BeginTime="0:0:5" SpeedRatio="0.2" AutoReverse="True"/>

设置重复播放

RepeatBehaviour属性用来定义动画结束时的行为,这个属性的设置方式有如下几种:

  • 定义一个时间。时间线会暂停这个定义的时长后循环开始这个动画。
  • 定义Forever来不断循环这个动画。
  • 通过数字加一个x来设置循环的次数。如3x表示循环播放3次。

下面我们继续在上面例子的基础上做演示,并且我们增加了To属性的设置,这样就实际形成一个动画:

1 <DoubleAnimation Storyboard.TargetName="rect" Storyboard.TargetProperty="(Canvas.Left)" Duration="00:00:05" BeginTime="0:0:5" SpeedRatio="0.2" AutoReverse="True" To="500" RepeatBehavior="3x"/>

细说DoubleAnimation类型动画

From参数用来指定变化的初始值,如果不指定则默认为目标属性的当前值。To与By两个属性都可以指定结束值,在两个属性都被设置时,To有更高的优先级。

细说ColorAnimation类型动画

From属性是变换起始颜色,如果不指定就是变化对象当前的颜色,To与By属性指定结束颜色,同样To有更高的优先级。由于对象的颜色通常是通过Brush来指定,所以变换的目标属性不是Fill而是实际填充Fill的如SolidBrush对象的Color属性,下面的例子可以很好的解释这一点:

1 <ColorAnimation Storyboard.TargetName="rect" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)" To="Pink" Duration="0:0:5"/>

正如我们前文说过的,附加属性需要使用括号括起来。

细说PointAnimation类型动画

这个动画对一个点基于时间进行变换,类似与其它动画类型,From与To(By)分别设置动画的起始与结束点。下面给出一个例子,动画目标贝塞尔曲线的终止点在动画过程中由(300, 100)变为(300, 600):

 1 <Path Stroke="Pink">
 2     <Path.Data>
 3         <PathGeometry>
 4             <PathFigure StartPoint="100,100">
 5                 <QuadraticBezierSegment x:Name="seg" Point1="200,0" Point2="300,100"/>
 6             </PathFigure>
 7         </PathGeometry>
 8     </Path.Data>
 9     <Path.Triggers>
10         <EventTrigger RoutedEvent="Path.Loaded">
11             <BeginStoryboard>
12                 <Storyboard>
13                     <PointAnimation Storyboard.TargetName="seg" Storyboard.TargetProperty="Point2" From="300,100" To="300,600" Duration="00:00:05"/>
14                 </Storyboard>
15             </BeginStoryboard>
16         </EventTrigger>
17     </Path.Triggers>
18 </Path>

除了以上介绍的使用事件触发动画,还可以通过属性触发动画,我们以一段定义于Style中的Trigger来展示:

 1 <Window.Resources>
 2     <Style TargetType="{x:Type Button}">
 3         <Style.Triggers>
 4             <Trigger Property="IsMouseOver" Value="True">
 5                 <Trigger.EnterActions>
 6                     <BeginStoryboard>
 7                         <Storyboard>
 8                             <DoubleAnimation Storyboard.TargetProperty="Width" To="2" />
 9                         </Storyboard>
10                     </BeginStoryboard>
11                 </Trigger.EnterActions>
12                 <Trigger.ExitActions>
13                     <BeginStoryboard>
14                         <Storyboard>
15                             <DoubleAnimation Storyboard.TargetProperty="Width" To="1" />
16                         </Storyboard>
17                     </BeginStoryboard>
18                 </Trigger.ExitActions>
19             </Trigger>
20         </Style.Triggers>
21     </Style>
22 </Window.Resources>

如代码所示,一个属性触发器有两个Action集合:EnterActions和ExitActions。例子中两个Action分别在属性IsMouseOver被设置为true和false时触发。

上面介绍的所有这些动画,依赖属性由初始值到目标值的变化稍显单调,无非是简单的线性内插,或者简单的加减速变化,下面我们看一下另一种动画类型 - 关键帧动画,通过它我们可以在动画过程中指定的时间设置特定的值。

关键帧动画

    首先我们要了解关键帧的作用,之前我们介绍的动画,在整个动画发生的过程中,速率是固定的,如果想要动画进行的过程中有不同的变化速度,如开始结束的过程有渐快渐慢的效果,则我们可以在动画的时间线上添加关键帧,从而把动画分为多段来控制。实现关键桢动画需要使用特定的类,每个普通的动画类都有一个名为xxxAnimationUsingKeyFrame的伴随类来实现关键帧动画。关键帧动画的一个关键属性是KeyTime,用于定义这个关键帧的结束时间,从而把一段动画分为多段。内置的关键帧有如下几种类型:

  • LinearKeyFrame:LinearKeyFrame定义的关键帧之间变化效果是线性的。但由于关键帧间指定的运行时间和幅度可能不同,所以动画整体上可能会表现出加速或减速效果。下面的例子使用DoubleAnimationUsingKeyFrames与LinearDoubleKeyFrame演示了LinearKeyFrame类型关键帧的使用:
    1 <BeginStoryboard>
    2     <Storyboard>
    3         <DoubleAnimationUsingKeyFrames Storyboard.TargetName="rect" Storyboard.TargetProperty="(Canvas.Left)">
    4             <LinearDoubleKeyFrame KeyTime="0:0:1" Value="300"/>
    5             <LinearDoubleKeyFrame KeyTime="0:0:9" Value="600"/>
    6         </DoubleAnimationUsingKeyFrames>
    7     </Storyboard>
    8 </BeginStoryboard>
  • DiscreteKeyFrame:如果不想使用线性变化方式可以使用不连续关键帧 – DiscreteKeyFrame。两个这种关键桢间的变化没有任何形式的过渡,直接由一个值变为另一个值,我们先看一个例子:
    1 <BeginStoryboard>
    2     <Storyboard>
    3         <DoubleAnimationUsingKeyFrames Storyboard.TargetName="rect" Storyboard.TargetProperty="(Canvas.Left)">
    4             <DiscreteDoubleKeyFrame KeyTime="0:0:1" Value="300"/>
    5             <DiscreteDoubleKeyFrame KeyTime="0:0:9" Value="600"/>
    6         </DoubleAnimationUsingKeyFrames>
    7     </Storyboard>
    8 </BeginStoryboard>

    这个例子与前一个例子唯一的不同是使用DiscreteDoubleKeyFrame替换了LinearDoubleKeyFrame,使用不连续关键帧,对象的值会在关键帧位置直接跳跃变化到这个帧中指定的值。如上面的例子,到达第一个帧之前,Canvas.Left保持不变,到达第一个帧时,Canvas.Left值直接变为300。同样在PointAnimationUsingKeyFrames与ColorAnimationUsingKeyFrames中DiscretePointKeyFrame与DiscreteColorKeyFrame会起到类似的效果。

还有一个值得注意的是,有5种DiscreteXXXKeyFrame是这种关键桢独有的,没有对应的LinearXXXKeyFrame或SplineXXXKeyFrame,它们分别是用于Boolean,Char,Matrix,Object和String类型的DiscreteKeyFrame,因为这些类型做内插完全没有意义。使用这些特殊的关键帧我们可以实现一些很有趣的效果,如下代码:

1 <StringAnimationUsingKeyFrames Storyboard.TargetProperty="Text" Duration="0:0:.5">
2     <DiscreteStringKeyFrame Value="play"/>
3     <DiscreteStringKeyFrame Value="Play"/>
4     <DiscreteStringKeyFrame Value="pLay"/>
5     <DiscreteStringKeyFrame Value="plAy"/>
6     <DiscreteStringKeyFrame Value="plaY"/>
7 </StringAnimationUsingKeyFrames>

这个例子中单词会出现每个字母依次变为大写的效果!

  • SplineKeyFrame:Spline关键帧可以用来定义平滑的加速或减速过程。每一个LinearKeyFrame都有对应的SplineKeyFrame,两者最大的区别在于后者提供了一个KeySpline属性。SplineKeyFrame的原理是定义一条三次贝塞尔曲线,在动画过程中,动画中变化的对象值会根据这条曲线的斜率进行变化。SplineDoubleKeyFrame的KeySpline属性用来定义三次贝塞尔曲线的两个控制点,两个控制点X,Y值的范围均为0到1,贝塞尔曲线的起止点被默认设置为(0,0), (1,1)。 由于KeySpline转换器,可以使用最直观的字符串的方式来设置两个点坐标。看下面这个例子:
    1 <BeginStoryboard>
    2     <Storyboard>
    3         <DoubleAnimationUsingKeyFrames Storyboard.TargetName="rect" Storyboard.TargetProperty="(Canvas.Left)">
    4             <SplineDoubleKeyFrame KeyTime="0:0:5" KeySpline="0.3,0 0.6,1" Value="600"/>
    5         </DoubleAnimationUsingKeyFrames>
    6     </Storyboard>
    7 </BeginStoryboard>

    这段代码中通过KeySpline属性把两个控制点分别指定为(0.3,0),(0.6,1)。所得的三次贝塞尔曲线如下图(通过Silverlight SDK Sample Browser截取):

动画变化的速度将由这条贝塞尔曲线的斜率决定,图像可以看出斜率由小变大然后又变小,即动画速度由慢到快最后又变慢。SplineColorKeyFrame也是同样的道理。当然要想最高效的得到一条想要的贝塞尔曲线,应该使用如Expression Blend这样的工具。

提示,这些KeyFrame对象中的KeyTime属性除了定义为TimeSpan对象之外,都可以接受一个百分比作为参数,这样很方便实现具体时间无关的动画。

提示:除了传统的动画,基于关键帧的动画,WPF还提供了基于路径的动画,这些类命名形如:XXXAnimationUsingPath,它们有很强的专用性,被设计用来改变PathGeometry对象。大概场景就是当我们使用PathGeometry作为动画路径时,改变PathGeometry也就改变了动画的路径。由于目标针对性强,AnimationUsingPath针对的类型也很少,只有Double,Point和Matrix,但这对于改变Geometry足够了。另外AnimationUsingPath对PathGeometry中两点之间也是使用了线性内插的方式。

 

前文介绍的方式中我们通过把<Storyboard>放在<BeginStoryboard>中通过触发器来使动画自动播放。另一种可以自行控制动画播放的方式也很简单。我们可以通过Storyboard提供的Begin, Resume和Pause等方法来控制Storyboard的播放。这种情况下Storyboard一般存储于<Resources>中,在C#代码中我们通过FindName()方法找到Storyboard的实例,并通过其方法实现对动画的控制。

Storyboard不但是一个Timeline的容器,其本身也派生自Timeline,也就是说之前我们介绍的Duration,BeginTime,SpeedRatio等等属性也可以用来设置Storyboard。而且在Storyboard上添加的设置对所有定义于Storyboard中的子动画都有效。

将动画定义放在Style中的方法也非常简单,把EventTrigger定义设置到Style的Trigger属性就可以了。

最后来说一下动画性能问题:对于一个动画我们为了使其可以在性能比较低的机子上正常运行,在性能较好的机子上展现出更好的效果,可以通过System.Windows.Media命名空间的RenderCapability类的Tier静态属性和TierChanged静态事件来进行控制。另一个方法是在性能比较低的机子上通过Storyboard的DesiredFrameRate附加属性减少帧率。

本文完

参考:

《WPF揭秘》

原文地址:https://www.cnblogs.com/lsxqw2004/p/4629585.html