WPF 入门笔记 - 08 - 动画

感谢大家对上篇博文的支持💕

回到正题,今天和大家分享下学习动画过程中的内容。动画对我来讲还是蛮新鲜的,大家知道在接触WPF之前我只用过Winform,而Winform中并没有动画的概念,当想要实现某些“动画”效果时,我们必须从头构建自己的动画系统,正如微软文档中对动画的介绍,在Winform中做动画我们一般会结合计时器和一些绘图逻辑来实现我们想要的“动画”。

比如,我想让一个矩形逐渐从视野中消失,大概率会按照以下步骤来完成这项工作:

  • 创建一个计时器
  • 设定适当的Interval让计时器检查经历了多长时间
  • 在每次进入计时器时,根据时间计算矩形的透明度
  • 然后更新这个矩形

虽然这种方案看上去不难,但是将它应用到窗体上是很麻烦的,需要考虑各种各样的问题。而WPF则提供了这个非常有特色的内容,它有自己的属性动画系统,开发人员可以轻松制作出炫丽的动画效果,同时WPF还原生支持3D效果。

图形

在学习WPF动画之前,先来学习下WPF中的绘图技术。在上篇MVVM示例的博文中,在登录界面的右上角自绘了一个关闭按钮:

image-20230819212813034

这个关闭按钮中的❌就是通过绘制图形实现的。

我们之前说过,WPF 的图形系统是建立在微软的 DirectX 技术之上的,大家如果玩游戏的话应该会很熟悉这个东西,游戏中经常会听到 DirectX 11 或者 DirectX 12 之类的,这项技术诞生之初是专门为游戏开发人员设计的,是许多 Windows 游戏都需要的一种多媒体技术套件。

对于一般程序员来说,直接使用 DirectX 过于繁重;所以WPFDirectX.NET 编程模型之间找到了一种平衡,既充分利用 DirectX 的图形性能,又方便我们开发,当然了,这需要付出一些运行效率的代价😉

WPF有两套独立的图形系统,一套系统是以Shape类为基类;另一套是以Goemetry类为基类。Shape类是用于绘制基本形状的类,例如矩形、圆、线条。Goemetry类是用于绘制更复杂图形的类,比如曲线、多边形或者文本等。Shape 类和 Geometry 类的区别在于,Shape 类是基于物理模型的,而 Geometry 类是基于数学模型的。这意味着,Shape 类绘制的图形会受到物理因素的影响,例如屏幕分辨率和显示器的性能。Geometry 类绘制的图形则不会受到这些因素的影响,因此可以保证图形在任何设备上都具有相同的外观。

为什么WPF要提供两套独立的图形系统呢?这是因为在设计图形系统时,需要兼顾方便编程和提高实时性能两个因素。以Shape类为基类的图形是UIElement,你可以不用做什么工作,就可以像使用控件一样地使用Shape,其代价是实时性能。从Shape类中派生出来的图形元素要使用较多的资源,速度较慢。从Geometry类中派生出来的图形则使用较少的资源,适合组成复杂的图形,如地图等。

Shape及其派生类

在日常开发种,Shape类要比Geometry类更常用,因为前者提供了更为简单和直观的API,用于绘制基本形状。

image-20230819210913319

Shape类中派生出了六个类:LineEllipsePathPolygonPolylineRectangle

  • Line:直线,可以设置Stroke
  • Rectangle:矩形,既有Stroke又有Fill
  • Ellipse:椭圆 - 长宽相等即为正圆
  • Polygon:多边形 - 多条直线段围成的闭合区域
  • Polyline:折线 - 多条首尾相接的直线段组成(不闭合)
  • Path:路径,用来绘制多点连线(闭合)

Shape类中定义了两个类型为Brush的属性:StrokeFillStroke用来画线,Fill用来填充图形的内部区域。默认情况下,两者的值都为null,这个时候,在界面上是看不到绘制的图形的。此外还有StrechStrokeThicknessData等属性,就不一一列举了,可以F12自己看一下。

直线(Line)

直线是最简单的图形,使用X1、Y1两个属性可以设置它的起点坐标,X2、Y2两个属性则用来设置其终点坐标,控制起点/终点坐标就可以实现平行、交错等效果,一些属性还能控制画出虚线以及控制线段终点的形状。

<Window x:Class="LearningWPF.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:LearningWPF"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Title="MainWindow" Width="800"
        Height="450" mc:Ignorable="d" WindowStartupLocation="CenterScreen">
    <Grid>
        <StackPanel Margin="5">
            <Line Stroke="LightPink" StrokeThickness="25" X1="10" X2="100" Y1="50" Y2="60" />
            <Line X1="10" Y1="50" X2="450" Y2="20" StrokeEndLineCap="Round" StrokeDashArray="5" StrokeThickness="10">
                <Line.Stroke>
                    <LinearGradientBrush EndPoint="0,0.5" StartPoint="1,0.5">
                        <GradientStop Color="Blue" />
                        <GradientStop Offset="1" />
                    </LinearGradientBrush>
                </Line.Stroke>
            </Line>
        </StackPanel>
    </Grid>
</Window>

image-20230821175513650

矩形(Rectangle)

矩形由Stroke(边线)和Fill(填充)组成,前者和折线相同,后者是Brush类型的画刷。

<Rectangle Stroke="Linen" Fill="Green" Height="50" Width="300" StrokeThickness="10" />
<Rectangle Width="300" Height="50" Fill="HotPink" Stroke="OrangeRed" StrokeThickness="5" RadiusX="10"
           RadiusY="10" />

image-20230822104357551

椭圆(Ellipse)

椭圆可以看成 矩形的特例,当矩形的RadiusXRadiusY设置为最大值时,矩形就变成了椭圆。WPF提供椭圆Ellipse只是为了编程方便一点:

 <Ellipse Width="300" Height="50" Fill="HotPink" Stroke="OrangeRed" StrokeThickness="5" Opacity="0.8" />

image-20230822104827099

折线(Polyline)

折线由多条直线组成,上一条直线的终点是下一条直线的起点:

<Polyline Points="0,0 10,5 100,100 150,50 200,50" Stroke="HotPink" StrokeThickness="4" Fill="CadetBlue"/>

image-20230822105334353

折线通过Point属性设置多点来绘制,每个点写明横纵坐标即可。如果设置了Fill属性,会在折线的起点和终点之间进行填充。

多边形(Polygon)

多边形是折线的特例,它自动把折线的终点和起点相连:

 <Polygon Points="0,0 10,5 100,100 150,50 200,50" Stroke="HotPink" StrokeThickness="4" Fill="CadetBlue" />

image-20230822105924111

路径(Path)

路径Path可以说是WPF中最强大的绘图工具,它新增了一个类型为GeometryData属性,可以连接以Goemetry类为基类的另一套图形系统,相较于ShapeGemetry使用的资源要少的多。Gemetry只描述几何形状本身,并不负责图形在页面上的展示。但它可以通过和路径(Path)中的数据(Data)相连而在界面上展示出来,这也是Path强大的原因,一来它可以完全替代其他图形,而来它可以将Gemetry中的复杂图形结合进来。

在之前MVVM示例的Demo中,登录界面右上角的关闭按钮的❌就是通过路径PathData属性赋值绘制出来的:

image-20230822112836983

Data属性赋值的语法有两种:

  • 普通标签式标准语法

    • 在路径中嵌套Gemetry类中提供的各种形状:

    • <Path Stroke="HotPink" StrokeThickness="3">
        <Path.Data>
          <LineGeometry StartPoint="10,10" EndPoint="150,80" />
        </Path.Data>
      </Path>
      
    • image-20230822113443403

  • 专用于绘制几何图形的“路径标记语法”,也被称为迷你绘图语言🕘

上面的关闭按钮采用的便是专用于绘制几何图形的“路径标记语法”,普通标签式标准语法就不看了,写起来太繁琐了,一个稍微线条多的标签就得几十行的代码量,写标签什么的最烦人了💦。在绘制复杂的图形时,专用于绘制几何图形的“路径标记语法”是想要简洁写法的不二之选。

例如下面这个电力系统电压曲线图,这其实是通过路径以及后天代码定时更新数据点画出来的,厉害的嘞。代码部分就不贴了,比较复杂。

image-20230822120103712

路径标记语法实际上就是多个绘图命令的拼接,使用路径标记语法绘图时一般分三步:移动至起点 => 绘图 => 闭合图形:

  1. 移动到起点(Move To): 在路径标记语法中,使用"M"命令来指定绘图的起点。起点是你希望路径开始的位置。这个命令会将画笔移到指定的坐标,但不会绘制任何线条。示例:M10,20
  2. 绘制路径(Draw Path): 在移动到起点之后,你可以使用不同的命令来绘制线条、曲线和其他形状。一些常见的命令包括:
    • "L":绘制直线到指定坐标。
    • "C":绘制三次贝塞尔曲线。
    • "Q":绘制二次贝塞尔曲线。
    • "A":绘制椭圆弧。
    • "H":绘制水平线。
    • "V":绘制垂直线。 示例:L50,100 C80,50 150,150 200,100
  3. 闭合路径(Close Path): 如果你希望路径的结束点和起点连接起来,形成一个封闭的形状,你可以使用命令"Z"来闭合路径。这会将路径的当前点连接到起点,形成一个封闭的图形。示例:Z

image-20230822143153657

image-20230822143217868

路径标记语法是不区分大小写的,所以A与a、H与h都是等价的。

例如上面普通标签式语法例子中的那条线可以通过这样的路径来实现:

<Path Stroke="HotPink" StrokeThickness="3">
  <Path.Data>
    <LineGeometry StartPoint="10,10" EndPoint="150,80" />
  </Path.Data>
</Path>

<Path Margin="5" Data="M0,0 L150,80" Stroke="HotPink" StrokeThickness="4" />

image-20230822142736615

比如再来一条二次贝塞尔曲线:

<Path Stroke="Red" Data="M0,200 Q150,-100 300,200 400,50 500,40" />

image-20230822143945969

通过路径标记语法绘制图形的过程中,需要注意以下几点:

  • 表明图形起点 即M...

  • 每个命令组合必须和要求的一直,比如上面的贝塞尔曲线Q,每部分都必须满足既有控制点也得有终点:

    • image-20230822144400761
  • 剩余的命令效果参照上面的表中的描述用就可以了

Goemetry及其派生类

略~ 😎

动画

🥧鸡汤来喽~

一般来说,动画可以被看作是连续变量进行离散化,然后把离散的内容连续播放的过程。在WPF中,动画不一定是位置变化,它可以是任意相关属性的值随时间的变化,只要满足以下两条件即可:

● 该相关属性所在的类必须是DependencyObject的派生类,并且移植了IAnimatable接口

● 该相关属性的数据类型是WPF动画所支持的数据类型

也就是说,可以用来制作动画的属性必须是WPF动画所支持的依赖属性,好巧不巧的是,WPF对各数据类型动画的支持从控件到几何图形、从排版到风格和模板、几乎所有的对象都可以进行动画。WPF中用于支持动画的类大多位于System.Windows.Media.Animation名称空间中,大概有200多个类,根据不同的数据类型可以分为几大类,每一种数据类型WPF都提供了专门的类。

Double类型为例Animation类的典型类继承树结构如下图所示:

image-20230823202419163

对于其他数据类型,结构是类似的,都是从AnimationTimeLine派生出来的,一般情况下,每个类型会从AnimationTimeLine派生出一个xxxxxBase的抽象基类,然后它们又派生出3种具体的动画 - 简单动画(xxAnimation)、关键帧动画(xxAnimationUsingKeyFrame)、沿路径运动的动画(xxAnimationUsingPath)

image-20230822181730598

简单动画

所谓“简单动画”,就是仅仅由变化的起点、终点、变化幅度、变化时间4个要素构成的动画:

● 变化起点(From属性):如果没有指定变化起点则以变化目标属性的当前值为起点。【可以省略,前提是所选属性值必须是已经设置过硬编码的值,如果属性设置的是自动值会出现AnimationException:无法使用 NaN 的默认 origin 值】

● 变化终点(To属性):如果没有指定变化终点,程序将采用上一次动画的终点或默认值。【同样可省略,同样需要注意无值问题】

● 变化幅度(By属性):如果同时指定了变化终点,变化幅度将被忽略。

● 变化时间(Duration属性):必须指定,数据类型为Duration

下面是一个简单动画的例子:

<Window x:Class="LearningWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:local="clr-namespace:LearningWPF"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="MainWindow" Width="800" Height="500"
        mc:Ignorable="d"
        WindowStartupLocation="CenterScreen">
    <Grid>
        <StackPanel>
            <Button Name="btnShow"
                    Width="Auto"
                    Height="50"
                    VerticalAlignment="Center"
                    HorizontalAlignment="Center"
                    Click="btnShow_Click"
                    Content="Click Me"
                    FontSize="26"
                    Foreground="AntiqueWhite">
                <Button.Background>
                    <LinearGradientBrush>
                        <GradientStop Color="HotPink"
                                      Offset="0" />
                        <GradientStop Color="LightGreen"
                                      Offset="0.5" />
                    </LinearGradientBrush>
                </Button.Background>
            </Button>
        </StackPanel>
    </Grid>
</Window>
private void btnShow_Click(object sender, RoutedEventArgs e)
{
  DoubleAnimation doubleAnimation = new DoubleAnimation();
  doubleAnimation.From = btnShow.ActualWidth;
  doubleAnimation.To = this.Width - 150;
  //  doubleAnimation.By= this.Width - 300;
  doubleAnimation.Duration = TimeSpan.FromSeconds(1);
  btnShow.BeginAnimation(WidthProperty, doubleAnimation);
}

演示

ActualWidthWidth属性的区别:

Width属性反应的是选择的期望宽度,而AcutalWidth值指示的是最终使用的渲染宽度。如果使用自动布局,可能根本就没有的Width值,所Width属性只会返回Double.NaN值,开始动画时会抛异常。

image-20230823090848072

这是一个改变按钮Width属性的动画,因为Width属性是double类型,所以要用DoubleAinmation类,单击按钮后会把按钮的宽度变为当前窗体的宽度-150,时长1秒。这个例子虽然简单,但说明了WPF动画设计的一般步骤:

  • 选定动画对象;
  • 选定动画对象中的相关属性;
  • 根据相关属性的数据类型,选定相应的动画类,如本例中的Widthdouble类型,选用DoubleAnimation
  • 设置动画属性,如本例中的FromTo等;
  • 选择触发动画的条件,如本例中用单击鼠标开始动画。
  • 调用所要动画对象中的BeginAnimation方法,该方法把DoubleAnimation和按钮的WidthProperty联系起来

动画控制

使用FromToByDuration几个属性的组合就已经可以显示很多不同的效果了,但WPF动画系统的强大远不如此。从动画的类继承树可以知道,动画的很多特性都是由TimeLine类控制的,这个类提供了动画常用的属性,比如动画时间、开始时刻、动画重复次数、动画终止的状态等,除了上面的Duration动画时间,TimeLine还有以下属性,可以帮助实现更复杂的动画:

image-20230823101501274

比如可以使用AutoReverse属性实现自动复原,RepeatBehavior属性控制动画重复特性:

private void btnShow_Click(object sender, RoutedEventArgs e)
{
  DoubleAnimation doubleAnimation = new DoubleAnimation();
  doubleAnimation.From = btnShow.ActualWidth;
  doubleAnimation.To = this.Width - 150;
  doubleAnimation.Duration = TimeSpan.FromSeconds(1);
  doubleAnimation.AutoReverse = true;
  doubleAnimation.RepeatBehavior = new RepeatBehavior(4);
  btnShow.BeginAnimation(WidthProperty, doubleAnimation);
}

演示

故事板(Storyboard)

StoryboardParallelTimeline的派生类,支持多个动画并发进行。假如你设计的动画只使用了极少的设置内容,你可以使用故事板在Xaml中实现动画的设计,可以理解为故事板是BeginAnimation()方法的Xaml等价物。此外,动画的开始需要一个触发条件,为此WPF专门设计了一个触发器类BeginStoryBoard,它为Storyboard类提供了宿主,每个中能够且仅能够含有一个Storyboard

上面的例子加个按钮,通过故事板控制新按钮动画:

<Window x:Class="LearningWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:local="clr-namespace:LearningWPF"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="MainWindow" Width="800" Height="500" mc:Ignorable="d"
        WindowStartupLocation="CenterScreen">
    <Grid>
        <StackPanel>
            <Button Name="btnShow"
                    Width="Auto"
                    Height="50"
                    VerticalAlignment="Center"
                    HorizontalAlignment="Center"
                    Click="btnShow_Click"
                    Content="Click Me"
                    FontSize="26"
                    Foreground="AntiqueWhite">
                <Button.Background>
                    <LinearGradientBrush>
                        <GradientStop Color="HotPink"
                                      Offset="0" />
                        <GradientStop Color="LightGreen"
                                      Offset="0.5" />
                    </LinearGradientBrush>
                </Button.Background>
                <Button.Triggers>
                    <EventTrigger RoutedEvent="Button.Click">
                        <EventTrigger.Actions>
                            <BeginStoryboard>
                                <Storyboard>
                                    <DoubleAnimation AutoReverse="True"
                                                     Storyboard.TargetName="cmdGrow"
                                                     Storyboard.TargetProperty="Width"
                                                     Duration="0:0:1"
                                                     To="300" />
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger.Actions>
                    </EventTrigger>
                </Button.Triggers>
            </Button>
            <Button Padding="10"
                    Name="cmdGrow"
                    Height="40"
                    Width="200"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Center"
                    Content="Click me and make me grow" Margin="5"> 
            </Button>
        </StackPanel>
    </Grid>
</Window>

演示

故事板的优点在于可以在Xaml界面完成动画的设计以及触发事件的定义,可以使用通用结构捕获触发事件:

<EventTrigger  RoutedEvent="" >
  <BeginStoryboard>
    <Storyboard>
      //  具体动画类
    </Storyboard>
    </ BeginStoryboard>
</EventTrigger>

Storyboard类中含有三个重要的附加属性:

Target,类型为相关对象(DependencyObject);

TargetName,其类型为string,必须是FrameworkElementFrameworkContentElementFreezable对象的名字;

TargetProperty,其类型为PropertyPath,必须指向相关属性。

Storyboard标签内内可以定义多个动画,并发运行。比如,上面的例子改一下,不用Button_Click事件,改用故事板并且两按钮同时变化 - 并行动画:

<Window x:Class="LearningWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:local="clr-namespace:LearningWPF"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="MainWindow"
        Width="800"
        Height="500"
        mc:Ignorable="d"
        WindowStartupLocation="CenterScreen">
    <Grid>
        <StackPanel>
            <Button Name="btnShow"
                    Width="200"
                    Height="50"
                    VerticalAlignment="Center"
                    HorizontalAlignment="Center"
                    Content="Click Me"
                    FontSize="26"
                    Foreground="AntiqueWhite">
                <Button.Background>
                    <LinearGradientBrush>
                        <GradientStop Color="HotPink"
                                      Offset="0" />
                        <GradientStop Color="LightGreen"
                                      Offset="0.5" />
                    </LinearGradientBrush>
                </Button.Background>
                <Button.Triggers>
                    <EventTrigger RoutedEvent="Button.Click">
                        <EventTrigger.Actions>
                            <BeginStoryboard>
                                <Storyboard>
                                    <DoubleAnimation AutoReverse="True"
                                                     Storyboard.TargetName="cmdGrow"
                                                     Storyboard.TargetProperty="Width"
                                                     Duration="0:0:1"
                                                     To="300" />
                                    <DoubleAnimation AutoReverse="True"
                                                     Storyboard.TargetName="btnShow"
                                                     Storyboard.TargetProperty="Width"
                                                     Duration="0:0:1"
                                                     To="300" />
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger.Actions>
                    </EventTrigger>
                </Button.Triggers>
            </Button>
            <Button Padding="10"
                    Name="cmdGrow"
                    Height="40"
                    Width="200"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Center"
                    Content="Click me and make me grow" Margin="5"> 
            </Button>
        </StackPanel>
    </Grid>
</Window>

演示

关键帧动画

“关键帧动画” - 显然就是一段动画中比较重要的一些帧。帧,即帧率,游戏玩家应该熟悉什么是帧率,比如CSGO的游戏帧率300FPS,一般游戏内的FPS指的是画面每秒钟重绘的次数,帧率越高画面的刷新次数就越高,被一枪头的概率就越低,当然低头选手需要排除😅。放到WPF里,属性每次细微变化后产生的每一个新画面就称为帧,帧率越高动画越细腻。普通的动画类只支持从一个值到另一个值的线性内插,关键帧动画则允许我们设计动画过程中某些时刻达到的效果。

此外,关键帧动画可以完成前面所述动画的所有功能。

在关键帧动画里,没有FromToBy的属性,关键帧动画的目标值使用关键帧对象进行描述,因此称作“关键帧动画”。适用关键帧动画的前提需要有允许使用关键帧动画的类,比如DoubleAnimation有一个DoubleAnimationUsingKeyFrame的伴随类,对于其他的xxAnimation也一样【支持关键帧的动画类可以查阅MSDN文档】。

学习一下MSDN提供的关键帧动画示例:

<Rectangle Width="50"
           Height="50"
           HorizontalAlignment="Left"
           Fill="Blue">
  <Rectangle.RenderTransform>
    <TranslateTransform x:Name="MyAnimatedTranslateTransform" X="0" Y="0" />
  </Rectangle.RenderTransform>
  <Rectangle.Triggers>
    <EventTrigger RoutedEvent="Rectangle.MouseLeftButtonDown">
      <BeginStoryboard>
        <Storyboard>
          <DoubleAnimationUsingKeyFrames Storyboard.TargetName="MyAnimatedTranslateTransform" Storyboard.TargetProperty="X" Duration="0:0:10">
            <LinearDoubleKeyFrame KeyTime="0:0:0" Value="0" />
            <LinearDoubleKeyFrame KeyTime="0:0:2" Value="350" />
            <LinearDoubleKeyFrame KeyTime="0:0:7" Value="50" />
            <LinearDoubleKeyFrame KeyTime="0:0:8" Value="200" />
          </DoubleAnimationUsingKeyFrames>
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>
  </Rectangle.Triggers>
</Rectangle>

演示

这段Xaml创建了一个矩形,通过故事板Storyboard添加了一的点击事件触发的动画,矩形会从左边移动到右边。Storyboard之前的内容和之前是一样的,主要关注一下它里面的内容:

image-20230823160828825

  • <Rectangle>:定义一个宽度为 50、高度为 50 的蓝色矩形。
  • <TranslateTransform>:定义一个用于平移的变换,初始位置为 (0, 0)。
  • <EventTrigger>:当矩形的左键点击事件触发时执行动画。
  • <Storyboard>:定义动画的序列。
  • <DoubleAnimationUsingKeyFrames>:通过关键帧定义矩形的 X 坐标在动画过程中的变化。
  • <LinearDoubleKeyFrame>:定义不同时间点的关键帧,以实现位置的平滑过渡。
  • <BeginStoryboard>:触发动画开始。

通过这些关键帧,矩形元素将在10秒的动画过程中,从0秒开始,X位置从0逐渐变为350,然后在7秒时突然变为50,最后在8秒时变为200。这个过程会通过平滑的动画效果展示出来。

关键帧对象(Frame) 主要包含两个参数, Value是目标值, KeyTime 是到达目标值的时刻

"0:0:10" - 这是使用TimeSpan说明的动画时间,一般格式为:

days.hours:minutes:seconds.fraction.

注意在日和小时之间使用的是点“.”,在时分秒之间使用的是“:”,秒以下又是点“.”。常用动画通常在分秒之间,可以使用简化的形式:"0:0:5"。但不能把3.5秒写成“3.5”而是写成“0:0:3.5”,因为只写“3.5”表示3天5小时🎈

线性关键帧(Linear Keyframe)

  • 在线性关键帧中,属性的值在关键帧之间以恒定速率变化,即属性在相邻关键帧之间的变化是均匀的。
  • 线性关键帧会产生一个平稳的、匀速的过渡效果。
  • 在一个线性关键帧中,属性的变化率(速度)是固定的。

写一个拍球动画:

<Window x:Class="KeyFrameDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:local="clr-namespace:KeyFrameDemo"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="KeyFrameDemo"
        Width="800"
        Height="450"
        WindowStartupLocation="CenterScreen"
        mc:Ignorable="d">
  <Grid>
    <Ellipse Name="ball"
             Width="64"
             Height="64"
             HorizontalAlignment="Center"
             VerticalAlignment="Top"
             Fill="HotPink"
             RenderTransformOrigin="0.5,0.5"
             Stroke="LightPink"
             StrokeThickness="3">
      <Ellipse.RenderTransform>
        <TransformGroup>
          <ScaleTransform ScaleX="1" ScaleY="1" />
          <SkewTransform AngleX="0" AngleY="0" />
          <RotateTransform Angle="0" />
          <TranslateTransform X="0" Y="0" />
        </TransformGroup>
      </Ellipse.RenderTransform>
      <Ellipse.Triggers>
        <EventTrigger RoutedEvent="Ellipse.MouseLeftButtonDown">
          <BeginStoryboard>
            <Storyboard RepeatBehavior="Forever">
              <DoubleAnimationUsingKeyFrames BeginTime="0:0:0"
                                             Storyboard.TargetName="ball"
                                             Storyboard.TargetProperty="(Ellipse.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.Y)">
                <LinearDoubleKeyFrame KeyTime="00:00:00"
                                      Value="0" />
                <LinearDoubleKeyFrame KeyTime="00:00:03"
                                      Value="293" />
                <LinearDoubleKeyFrame KeyTime="00:00:06"
                                      Value="0" />
              </DoubleAnimationUsingKeyFrames>
              <ColorAnimationUsingKeyFrames BeginTime="0:0:0"
                                            Storyboard.TargetName="ball"
                                            Storyboard.TargetProperty="(Ellipse.Fill).(SolidColorBrush.Color)">
                <LinearColorKeyFrame KeyTime="00:00:00"
                                     Value="DeepPink" />
                <LinearColorKeyFrame KeyTime="00:00:03"
                                     Value="OrangeRed" />
                <LinearColorKeyFrame KeyTime="00:00:06"
                                     Value="Pink" />
              </ColorAnimationUsingKeyFrames>
            </Storyboard>
          </BeginStoryboard>
        </EventTrigger>
      </Ellipse.Triggers>
    </Ellipse>
  </Grid>
</Window>

这段代码实现了一个无限循环的动画,当点击椭圆时,它会在垂直方向上上下移动,同时填充颜色从深粉红变化到橙红再到粉红,每个颜色变化的持续时间为 3 秒。整个动画序列会不断循环播放:

演示

image-20230823172251483

非线性关键帧(Spline KeyFrame)

可以看到线性关键帧是匀速的,但有时候为了使效果更逼真,动画不一定都是均匀变化的。这个时候就需要用到Spline KeyFrame非线性关键帧:

  • 在非线性关键帧中,属性的值可以在关键帧之间以不同的速率变化,产生非均匀的变化效果。
  • 非线性关键帧允许属性在动画过程中出现加速或减速的效果,从而创造出更加生动和多样的动画效果。
  • 在一个非线性关键帧中,属性的变化率可以在关键帧之间不断变化。

还是上面的拍球例子,球向下运动时,由于重力的作用,球的速度应该是加快的;当球从地上弹起时,球的速度是减慢的。为此,可以用非线性关键帧对它改造一下:

<DoubleAnimationUsingKeyFrames BeginTime="0:0:0"
                               Storyboard.TargetName="ball"
                               Storyboard.TargetProperty="(Ellipse.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.Y)">
  <SplineDoubleKeyFrame KeyTime="00:00:00"
                        Value="0" />
  <SplineDoubleKeyFrame KeyTime="00:00:03"
                        Value="293"
                        KeySpline="0.9,0.06 1,1" />
  <SplineDoubleKeyFrame KeyTime="00:00:06"
                        Value="0"
                        KeySpline="0.06,0.9 1,1" />
</DoubleAnimationUsingKeyFrames>

KeySpline 属性的值是一个贝塞尔曲线的控制点,其值范围应该在 0 到 1 之间,详细内容可以看一下MSDN文档关键帧动画概述内插方法小节的内容

演示

此外,也可以使用不同的缓动函数(Easing Function)来替代 KeySpline。例如,你可以使用 QuadraticEaseCubicEase 等来实现想要的效果。

离散关键帧(Discrete KeyFrame)

离散关键帧(Discrete Keyframe)用于创建不连续、突然变化的动画效果。与其他关键帧类型不同,离散关键帧没有过渡,属性值会在关键帧之间突然改变。

WPF 动画中,离散关键帧的每个关键帧都是一个明确的属性值,并且不会在关键帧之间插值。当动画播放到离散关键帧时,属性值会突然从前一个关键帧的值变为当前关键帧指定的值。这种突变效果可以用于创造一些特殊的动画效果,比如文字闪烁、元素的跳跃等。

文字闪烁:

<Window x:Class="AnimationDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:AnimationDemo" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="Animation"
        Width="850" Height="450"
        WindowStartupLocation="CenterScreen"
        mc:Ignorable="d">
    <Grid>
        <TextBlock Text="Shoot for the moon; even if you miss, you'll land among the stars." FontSize="26" VerticalAlignment="Center" HorizontalAlignment="Center">
            <TextBlock.Foreground>
                <SolidColorBrush Color="Black"/>
            </TextBlock.Foreground>
            <TextBlock.Triggers>
                <EventTrigger RoutedEvent="Loaded">
                    <BeginStoryboard>
                        <Storyboard RepeatBehavior="Forever">
                            <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(TextBlock.Foreground).(SolidColorBrush.Color)">
                                <DiscreteColorKeyFrame KeyTime="0:0:0"
                                                       Value="HotPink" />
                                <DiscreteColorKeyFrame KeyTime="0:0:0.5"
                                                       Value="DarkMagenta" />
                                <DiscreteColorKeyFrame KeyTime="0:0:1"
                                                       Value="LimeGreen" />
                                <DiscreteColorKeyFrame KeyTime="0:0:1.5"
                                                       Value="OrangeRed" />
                            </ColorAnimationUsingKeyFrames>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </TextBlock.Triggers>
        </TextBlock>
    </Grid>
</Window>

show

在这个示例中,DiscreteColorKeyFrame 被用来在指定的时间点突然改变文本颜色,创造出了文本闪烁的效果。每个 DiscreteColorKeyFrame 关键帧都表示了一个明确的颜色,而不会在关键帧之间进行颜色的平滑过渡。

沿路径运动的动画

沿路径运动的动画是一种将元素沿着指定的路径进行移动的动画效果。在 WPF 中,可以使用 PathGeometryDoubleAnimationUsingPath 来实现沿路径运动的动画。

比如沿着之前画的贝塞尔曲线的路径运动的动画:

<Window x:Class="AnimationDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:local="clr-namespace:AnimationDemo"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="Animation"
        Width="850"
        Height="450"
        WindowStartupLocation="CenterScreen"
        mc:Ignorable="d">
    <Grid>
        <Canvas>
            <Path Data="M0,200 Q150,-100 300,200 400,50 500,40"
                  Stroke="Black"
                  StrokeThickness="1" />
            <Ellipse Name="ball"
                     Width="20"
                     Height="20"
                     HorizontalAlignment="Center"
                     VerticalAlignment="Top"
                     Fill="HotPink"
                     RenderTransformOrigin="0.5,0.5"
                     Stroke="LightPink"
                     StrokeThickness="3">
                <Ellipse.RenderTransform>
                    <TransformGroup>
                        <ScaleTransform ScaleX="1"
                                        ScaleY="1" />
                        <SkewTransform AngleX="0"
                                       AngleY="0" />
                        <RotateTransform x:Name="MyRotateTransform"
                                         Angle="0" />
                        <TranslateTransform x:Name="MyTranslateTransform"
                                            X="0"
                                            Y="200" />
                    </TransformGroup>
                </Ellipse.RenderTransform>
                <Ellipse.Triggers>
                    <EventTrigger RoutedEvent="Ellipse.MouseLeftButtonDown">
                        <BeginStoryboard>
                            <Storyboard RepeatBehavior="Forever">
                                <DoubleAnimationUsingPath AutoReverse="True"
                                                          RepeatBehavior="Forever"
                                                          Source="Angle"
                                                          Storyboard.TargetName="MyRotateTransform"
                                                          Storyboard.TargetProperty="Angle"
                                                          Duration="0:0:5">
                                    <DoubleAnimationUsingPath.PathGeometry>
                                        <PathGeometry Figures="M0,200 Q150,-100 300,200 400,50 500,40" />
                                    </DoubleAnimationUsingPath.PathGeometry>
                                </DoubleAnimationUsingPath>
                                
                                <DoubleAnimationUsingPath AutoReverse="True"
                                                          RepeatBehavior="Forever"
                                                          Source="Y"
                                                          Storyboard.TargetName="MyTranslateTransform"
                                                          Storyboard.TargetProperty="(Y)"
                                                          Duration="0:0:5">
                                    <DoubleAnimationUsingPath.PathGeometry>
                                        <PathGeometry Figures="M0,200 Q150,-100 300,200 400,50 500,40" />
                                    </DoubleAnimationUsingPath.PathGeometry>
                                </DoubleAnimationUsingPath>
                                
                                <DoubleAnimationUsingPath AutoReverse="True"
                                                          RepeatBehavior="Forever"
                                                          Source="X"
                                                          Storyboard.TargetName="MyTranslateTransform"
                                                          Storyboard.TargetProperty="(X)"
                                                          Duration="0:0:5">
                                    <DoubleAnimationUsingPath.PathGeometry>
                                        <PathGeometry Figures="M0,200 Q150,-100 300,200 400,50 500,40" />
                                    </DoubleAnimationUsingPath.PathGeometry>
                                </DoubleAnimationUsingPath>
                            </Storyboard>
                        </BeginStoryboard>
                    </EventTrigger>
                </Ellipse.Triggers>
            </Ellipse>
        </Canvas>
    </Grid>
</Window>

show

Ending...

以上内容就是本篇的所有内容了,图形系部分主要有ShapeGoemetry两大类,可以直接对Shape进行排版、设定风格和数据绑定,后者则需要通过视觉元素才能在屏幕上显示出来。动画则一般分为简单动画、关键帧动画以及沿路径运动的动画,日常使用过程种应该是关键帧动画用的多一点,当然除了文章中例举的关键帧类型,还有其他很多各式的关键帧类型,这些应该够用。

图形和动画之间应该加上图形转换的🤐

记得点赞哟💖

posted @ 2023-08-24 08:30  PixelKiwi  阅读(2356)  评论(2编辑  收藏  举报