• 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揭秘》

  • 相关阅读:
    面试题汇总
    Chromium中多线程及并发技术要点(C/C++)
    关于《Swift开发指南》背后的那些事
    HDU 3080 The plan of city rebuild(除点最小生成树)
    2.1.3策略模式(5.9)
    Shell脚本之监视指定进程的执行状态
    敏捷开发中的10大错误认识
    mysql插入中文数据报错:incorrect string value
    JapserReport导出PDF Could not load the following font错误
    冒泡排序
  • 原文地址:https://www.cnblogs.com/lsxqw2004/p/4629585.html
Copyright © 2020-2023  润新知