《15天玩转WPF》—— 优美的动画详解(完结篇)

时间来到了 4月5日,清明已过,吾辈继续努力 . . .
这一篇写完之后,以后就用WPF写一些小程序了,之后开始算法的学习 . . .

那时少年,万花皆有逐梦时 . . .


文章目录

  1. 何为动画?

  2. 简单独立动画

    2.1 简单线性动画

    2.2 高级动画控制

    2.3 关键帧动画

    2.4 特殊的关键帧

    2.5 路径动画

  3. 场景

我们可以看出来,独立动画分为很多种,如果我们可以灵活的使用这些独立动画,我们就可以将它们分在一个场景之中,分成一个完整的故事动画界面 . . . (相关知识下面会讲到)

首先说明一下,关键帧动画是串行工作,场景是并行工作的 . . . 所以使用场景我们可以进行各种各样的骚操作,其中 路径动画也是很牛逼,具体是什么样的,下面我们来一一讲解 . . .

.


何为动画?

动画自然就是 “会动的画”。动画的本质就是一个时间段内对象位置、角度、颜色、透明度等属性值的连续变化。 WPF规定,可以用来制作动画的属性必须是依赖属性 . . .

WPF的动画是一种运动,这种运动的主体就是各种 UI元素,这种运动本身就是施加在 UI元素上的一些 Timeline派生类的实例加以表达,下面用到的所有动画类型都是派生于这个类. . .

WPF把简单动画称为 AnimationTimeline,比如 目录2.
WPF把一组协同的动画称为 Storyboard(故事板,并行),比如目录3

这里主要是概念部分,目录2与目录3主要用实例来体现出这些概念 . . .

它们的派生关系图如下:

在这里插入图片描述

我们可以将各种各样的动画派生类,组合放在 Storyboard中,这样我们就可以看到像电影中那样许多动画并行工作了 . . .

.


简单独立动画

我们知道,动画就是一个属性的值产生连续的变化而已。任何一个属性都有自己的数据类型,比如 UIElement 的Width 和 Hight属性为Double类型等等。几乎针对每个可能的数据类型,WPF动画子系统都为其准备了相应的动画类型,这些动画类型均派生自 AnimationTimeline。

它们包括:

在这里插入图片描述

我们发现,它们都是以base结尾,说明它们都是抽象基类。而这些抽象基类又能派生出 3 种动画,即 简单动画、关键帧动画、沿路径运动的动画,它们在我们的目录中提到过,我们将在下面的文章一一讲解。

例如 DoubleAnimationBase,它的3个派生动画类为:

在这里插入图片描述

因为在WPF动画系统中 Double 类型的属性用得最多,,所以此文只讲述 DoubleAnimationBase的派生类。会用这个类,其他的动画类型则轻而易举的掌握 . . .

下面让我们来见识一下各种简单的独立动画吧 . . .
.
.

简单线性动画

“简单线性动画” 由4个要素组成的动画,它们分别是:

  • 变化时间(Duration 属性):必须指定,动画的时长多少
  • 变化终点(To 属性):变化终点,如果没有则采用 上一次动画的终点默认值
  • 变化幅度(By 属性):变化幅度,变化的规律,使用时不可使用变化终点
  • 变化起点(From 属性):变化起点,如果没有指定则以目标属性的当前值为起点

例如下面的这个例子,来演示一下简单线性动画的使用,XAML代码如下:

<Button Width="60" Height="60" Content="Move" Click="Button_Click_1" 
                HorizontalAlignment="Left" VerticalAlignment="Top">
            <Button.RenderTransform>
                <TranslateTransform x:Name="tt" X="30" Y="30"/>
            < /Button.RenderTransform>
</Button>

因为我们使用的是动画,所以我们与动画关联的是 RenderTransform属性,变形相关文章如下:
《飞天入地WPF》—— 图形的效果与变形

当我们点击这个按钮时,这个按钮会产生一个简单的动画:线性移动

它的事件处理如下所示:

private void Button_Click_1(object sender, RoutedEventArgs e)
{
	DoubleAnimation daX = new DoubleAnimation();
	DoubleAnimation daY = new DoubleAnimation();

	daX.From = 30;
	daY.From = 30;

	Random r = new Random();
	daX.To = r.NextDouble() * 300;
	daY.To = r.NextDouble() * 200;

	Duration duration = new Duration(TimeSpan.FromMilliseconds(500));
	daX.Duration = duration;
	daY.Duration = duration;

	this.tt.BeginAnimation(TranslateTransform.XProperty, daX);
	this.tt.BeginAnimation(TranslateTransform.YProperty, daY);
}

首先我们创建简单动画类型,它有着起点、终点、时长,如果我们需要指点变化幅度,则终点不需要指示,并且起点也不需要指示,例如下面:

daX.By = 100;
daY.By = 100;

//daX.From = 30;
//daY.From = 30;
//Random r = new Random();
//daX.To = r.NextDouble() * 300;
//daY.To = r.NextDouble() * 200;

最后我们使用BeginAnimation与目标的依赖属性相关联,并开始动画 . . .

使用By属性 和 To属性的不同效果如下所示:

  • To属性:

在这里插入图片描述
每次都从一个固定的起点到一个随机的终点 . . .

  • By属性:

在这里插入图片描述

每次移动都是一个固定的长度,起点为当前目标属性的 X、Y . . .

.

.

高级动画控制

何为高级? 只不过在简单动画的基础上加了一点骚操作而已 . . .
其实本身并不复杂 . . .

动画的骚操作属性有如下这些:

属性 描述 应用举例
AccelerationRatio 加速速率( 0.0 - 1.0) 模拟汽车启动
DecelerationRatio 减速速率( 0.0 - 1.0) 模拟汽车刹车
SpeedRatio 动画实际播放速度与正常速度的比值 快进播放、慢动作
AutoReverse 从终点到起点从来一次 倒退播放
RepeatBehavior 重复行为,取 Forever 为永远循环 循环播放
BeginTime 播放前的等待时间 多个动画之前的协同
EasingFunction 缓冲式渐变 乒乓球弹跳效果

这些属性的配合使用,可以产生很多奇妙的效果 . . .

EasingFunction 是一个扩展性非常强的属性,它的取值是 IEasingFunction接口类型,WPF自带的 IEasingFunction 派生类有十多种,每个派生类都能产生不同的效果,我从别处转载了一篇相关的文章,大家可以自己试一试:
《飞天入地WPF》—— IEasingFunction的十种派生类(转载)

我们来演示一下乒乓球的弹跳式的效果:

BounceEase bounceEase = new BounceEase();
bounceEase.Bounces = 3;
bounceEase.Bounciness = 2;
daY.EasingFunction = bounceEase;

首先我们指定起点与终点,然后我们将这个类型赋值给了 daY,它是和纵座标有关的. . .

效果如下图所示:

在这里插入图片描述

还有其它几种好玩的效果,可以自行测试 . . .

.
.

关键帧动画

关键帧动画允许程序员为一段动画设置几个 “里程碑”,动画执行到里程碑所在的时间点时(KeyTime被动画控制的属性值也必须达到设定的值(Value,这些时间线上的 “里程碑”就是关键帧 . . .

我们来设计一个 Button,它的动画路线是一个 “Z”字形,如下图所示:

在这里插入图片描述

如果我们不用关键帧将会非常的痛苦,需要用到 BeginTime或者 Complete,此处不再多讲 . . .

下面我们来实现这一个小例子 . . .

XAML代码如下:

<Button Content="Move" HorizontalAlignment="Left" VerticalAlignment="Top"
                Width="80" Height="80" Click="Button_Click">
            <Button.RenderTransform>
                <TranslateTransform x:Name="tt" X="0" Y="0"/>
            </Button.RenderTransform>
</Button>

点击按钮开始执行动画,事件处理器如下:

private void Button_Click(object sender, RoutedEventArgs e)
{
	// 创建动画
	DoubleAnimationUsingKeyFrames dakX = new DoubleAnimationUsingKeyFrames();
	DoubleAnimationUsingKeyFrames dakY = new DoubleAnimationUsingKeyFrames();
	// 动画时长
	dakX.Duration = new Duration(TimeSpan.FromMilliseconds(900));
	dakY.Duration = new Duration(TimeSpan.FromMilliseconds(900));
	// 创建关键帧
	LinearDoubleKeyFrame x_kf_1 = new LinearDoubleKeyFrame();
	LinearDoubleKeyFrame x_kf_2 = new LinearDoubleKeyFrame();
	LinearDoubleKeyFrame x_kf_3 = new LinearDoubleKeyFrame();
	// 为关键帧赋值
	x_kf_1.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(300));
	x_kf_1.Value = 200;
	x_kf_2.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(600));
	x_kf_2.Value = 0;
	x_kf_3.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(900));
	x_kf_3.Value = 200;
	// 给动画加入关键帧
	dakX.KeyFrames.Add(x_kf_1);
	dakX.KeyFrames.Add(x_kf_2);
	dakX.KeyFrames.Add(x_kf_3);

	// 与上面一样
	LinearDoubleKeyFrame y_kf_1 = new LinearDoubleKeyFrame();
	LinearDoubleKeyFrame y_kf_2 = new LinearDoubleKeyFrame();
	LinearDoubleKeyFrame y_kf_3 = new LinearDoubleKeyFrame();
	y_kf_1.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(300));
	y_kf_1.Value = 0;
	y_kf_2.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(600));
	y_kf_2.Value = 180;
	y_kf_3.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(900));
	y_kf_3.Value = 180;
	dakY.KeyFrames.Add(y_kf_1);
	dakY.KeyFrames.Add(y_kf_2);
	dakY.KeyFrames.Add(y_kf_3);

	// 关联属性,开始动画
	this.tt.BeginAnimation(TranslateTransform.XProperty, dakX);
	this.tt.BeginAnimation(TranslateTransform.YProperty, dakY);
}

其中下面的这段代码:

x_kf_1.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(300));
x_kf_1.Value = 200;
x_kf_2.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(600));
x_kf_2.Value = 0;
x_kf_3.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(900));
x_kf_3.Value = 200;

他的意思是动画到了 KeyTime时,它的值就必须到达 Value

程序会自动计算两个关键帧之间的关系,移动轨迹 . . .

效果如下图所示:

在这里插入图片描述

我们可以用关键帧做出各种各样的有规律的线性运行 . . .

.
.

特殊的关键帧

DoubleAnimationUsingKeyFrames 的 KeyFrames 属性的数据类型是 DoubleKeyFrameCollection,此集合类可接收的类型为 DoubleKeyFrame。DoubleKeyFrame 是一个抽象类 . . .

它的所有派生类如下:

  • LinearDoubleKeyFrame:线性变化关键帧(均匀的变化,上面已经使用过了 . . .)
  • DiscreteDoubleKeyFrame:不连续变化关键帧,跳跃性的变化
  • SplineDoubleKeyFrame:样条函数式变化关键帧,目标属性的变化速率是贝塞尔曲线
  • EasingDoubleKeyFrame:缓冲式变化关键帧,目标属性以某种缓冲形式变化

.
上面有一个类型我用红色标记了,为什么呢 ? 因为这个类型非常的常用,它可以取代
LinearDoubleKeyFrame . . .

SplineDoubleKeyFrameKeySpline属性可以控制速率的变化(变化速率是贝塞尔曲线)

控制点有两个:ControlPoint1 与 ControlPoint2 . . .
.
控制点速率的各种取值样式有如下的各种可能:
在这里插入图片描述

.
.
下面我们来用 SplineDoubleKeyFrame 取代上个例子的 LinearDoubleKeyFrame,完成同样的Button “Z”型移动动画,只不过加了一个速率的变化 . . .

XAML代码如下:

<Button Content="Move" HorizontalAlignment="Left" VerticalAlignment="Top"
                Width="80" Height="80" Click="Button_Click">
            <Button.RenderTransform>
                <TranslateTransform x:Name="tt" X="0" Y="0"/>
            </Button.RenderTransform>
</Button>

事件处理器如下:

private void Button_Click(object sender, RoutedEventArgs e)
{
	// 创建动画类型为关键帧类型
	DoubleAnimationUsingKeyFrames dakX = new DoubleAnimationUsingKeyFrames();
	dakX.Duration = new Duration(TimeSpan.FromMilliseconds(1000));
	DoubleAnimationUsingKeyFrames dakY = new DoubleAnimationUsingKeyFrames();
	dakY.Duration = new Duration(TimeSpan.FromMilliseconds(1000));
            
	// 创建关键针,和上面的例子差不多
	SplineDoubleKeyFrame kf1 = new SplineDoubleKeyFrame();
	kf1.KeyTime = KeyTime.FromPercent(0.33);
	kf1.Value = 200;
	SplineDoubleKeyFrame kf2 = new SplineDoubleKeyFrame();
	kf2.KeyTime = KeyTime.FromPercent(0.66);
	kf2.Value = 0;
	SplineDoubleKeyFrame kf3 = new SplineDoubleKeyFrame();
	kf3.KeyTime = KeyTime.FromPercent(1);
	kf3.Value = 200;
	SplineDoubleKeyFrame kf4 = new SplineDoubleKeyFrame();
	kf4.KeyTime = KeyTime.FromPercent(0.33);
	kf4.Value = 0;
	SplineDoubleKeyFrame kf5 = new SplineDoubleKeyFrame();
	kf5.KeyTime = KeyTime.FromPercent(0.66);
	kf5.Value = 180;
	SplineDoubleKeyFrame kf6 = new SplineDoubleKeyFrame();
	kf6.KeyTime = KeyTime.FromPercent(1);
	kf6.Value = 180;

	// 设置速率(贝塞尔曲线的速率,s型)
	KeySpline ks = new KeySpline();
	ks.ControlPoint1 = new Point(0, 1);
	ks.ControlPoint2 = new Point(1, 0);
	kf1.KeySpline = ks;
	kf2.KeySpline = ks;
	kf3.KeySpline = ks;
	kf4.KeySpline = ks;
	kf5.KeySpline = ks;
	kf6.KeySpline = ks;

	// 加入到动画之中
	dakX.KeyFrames.Add(kf1);
	dakX.KeyFrames.Add(kf2);
	dakX.KeyFrames.Add(kf3);
	dakY.KeyFrames.Add(kf4);
	dakY.KeyFrames.Add(kf5);
	dakY.KeyFrames.Add(kf6);

	// 为了效果更加清楚,加一个倒退、循环效果
	dakX.AutoReverse = true;
	dakY.AutoReverse = true;
	dakX.RepeatBehavior = RepeatBehavior.Forever;
	dakY.RepeatBehavior = RepeatBehavior.Forever;

	// 关联属性,开始动画
	this.tt.BeginAnimation(TranslateTransform.XProperty, dakX); 
	this.tt.BeginAnimation(TranslateTransform.YProperty, dakY); 
}

我们发现这个事件处理器与上面那个例子的事件处理器并没有什么大的差别,只不过加了一个控制速率而已 . . .

效果图如下:

在这里插入图片描述

他的移动速率是不是变化了呢?

.
.

路径动画

如果我们想让目标沿着一条给定的路径移动,我们可以用 DoubleAnimationUsingPath。

DoubleAnimationUsingPath 需要一个 PathGeometry来指明移动路径,Path不行,
DoubleAnimationUsingPath 有一个重要的属性为 Source,它的取值是 枚举类型 PathAnimationSource的值,X、Y、Angle . . .

下面我们来试一下路径动画,我们让一个Button 沿着一条贝塞尔曲线做来回的运动 . . .

XAML代码如下:

<Grid x:Name="LayoutRoot">
        <Grid.Resources>
            <PathGeometry x:Key="movingPath" 
            	Figures="m 0,150 c 300,-100 300,400 600,120"/>
        </Grid.Resources>
        
        <Button Content="Move" HorizontalAlignment="Left" VerticalAlignment="Top"
                Width="80" Height="80" Click="Button_Click">
            <Button.RenderTransform>
                <TranslateTransform x:Name="tt" X="0" Y="150"/>
            </Button.RenderTransform>
        </Button>
</Grid>

我们将 PathGeometry 作出 LayoutRoot的资源,通过这个名称我们可以查找到这个资源 . . .

事件处理器如下:

private void Button_Click(object sender, RoutedEventArgs e)
{
	// 查找资源
	PathGeometry pathGeometry = this.LayoutRoot.FindResource("movingPath")
	 	as PathGeometry;

	// 设置动画时间
	Duration duration = new Duration(TimeSpan.FromMilliseconds(600));
	
	// 创建路径动画
	DoubleAnimationUsingPath dmpX = new DoubleAnimationUsingPath();
	dmpX.Duration = duration;
	dmpX.PathGeometry = pathGeometry;
	dmpX.Source = PathAnimationSource.X;
	
	DoubleAnimationUsingPath dmpY = new DoubleAnimationUsingPath();
	dmpY.Duration = duration;
	dmpY.PathGeometry = pathGeometry;
	dmpY.Source = PathAnimationSource.Y;
	
	// 设置来回循环效果
	dmpX.AutoReverse = true;
	dmpY.AutoReverse = true;
	dmpX.RepeatBehavior = RepeatBehavior.Forever;
	dmpY.RepeatBehavior = RepeatBehavior.Forever;
	
	// 动画关联属性
	this.tt.BeginAnimation(TranslateTransform.XProperty, dmpX);
	this.tt.BeginAnimation(TranslateTransform.YProperty, dmpY);
}

这个例子比上面的例子简单多了,而且也比较骚气,只要你给出路径,他就能按照你的意愿来 . . .

.
.


场景

之前的关键帧动画是一种串行的动画,而场景则是并行的 . . .

每一个独立的个体都有自己的工作,在某一时刻他们同时工作,就能提高很多的效率,比较拍电影的时候,很多人同时在工作. . .
.
设计WPF的场景时情况也差不多,操作过程为:

  1. 先是把一组独立的动画组织在一个 Storyboard元素中
  2. 指定哪个动画由哪个 UI元素、哪个属性负责完成
  3. Storyboard 拥有一个触发时机(本例放在 Triggers中

下面我们来完成三个小球比赛的小例子 . . .
.

XMAL界面布局如下:

<Grid Background="Black">
        <Grid.RowDefinitions>
            <RowDefinition Height="38"/>
            <RowDefinition Height="38"/>
            <RowDefinition Height="38"/>
        </Grid.RowDefinitions>
        
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="60"/>
        </Grid.ColumnDefinitions>
        
        <Border BorderBrush="Gray" BorderThickness="1" Grid.Row="0">
            <Ellipse x:Name="ballR" Height="36" Width="36" Fill="Red"
                     HorizontalAlignment="Left">
                <Ellipse.RenderTransform>
                    <TranslateTransform x:Name="ttR"/>
                </Ellipse.RenderTransform>
            </Ellipse>
        </Border>
        
        <Border BorderBrush="Gray" BorderThickness="1,0,1,1" Grid.Row="1">
            <Ellipse x:Name="ballG" Height="36" Width="36" Fill="LawnGreen"
                     HorizontalAlignment="Left">
                <Ellipse.RenderTransform>
                    <TranslateTransform x:Name="ttG"/>
                </Ellipse.RenderTransform>
            </Ellipse>
        </Border>
        
        <Border BorderBrush="Gray" BorderThickness="1,0,1,1" Grid.Row="2">
            <Ellipse x:Name="ballB" Height="36" Width="36" Fill="Blue"
                     HorizontalAlignment="Left">
                <Ellipse.RenderTransform>
                    <TranslateTransform x:Name="ttB"/>
                </Ellipse.RenderTransform>
            </Ellipse>
        </Border>
        <Button Content="Go!" Grid.Column="1" Grid.RowSpan="3"/>
</Grid>

我们为三个小球进行了相同的布局,并且 将他们的 TranslateTransform 定义名称 . . .
还有一个触发动画的按钮 . . .

界面效果如下:

在这里插入图片描述

当我们点击按钮时,它们就一起移动.
因为在C#中写 Storyboard会比较麻烦,所以我们直接将代码写在 Button的 Triggers中 . . .

Triggers 代码如下:

<Button.Triggers>
    <EventTrigger RoutedEvent="Button.Click">
        <BeginStoryboard>
            <Storyboard Duration="0:0:5">
                
                <DoubleAnimation Duration="0:0:5" To="685"
                                 Storyboard.TargetName="ttR"
                                 Storyboard.TargetProperty="X"/>
                
                <DoubleAnimationUsingKeyFrames Duration="0:0:5"
                        Storyboard.TargetName="ttG" Storyboard.TargetProperty="X">
                    <SplineDoubleKeyFrame KeyTime="0:0:5" Value="685"
                                          KeySpline="0,1,1,0"/>
                </DoubleAnimationUsingKeyFrames>
                
                <DoubleAnimationUsingKeyFrames Duration="0:0:5"
                        Storyboard.TargetName="ttB" Storyboard.TargetProperty="X">
                    <SplineDoubleKeyFrame KeyTime="0:0:5" Value="685"
                                          KeySpline="1,0,0,1"/>
                </DoubleAnimationUsingKeyFrames>
            
            </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
</Button.Triggers>

有一个是简单动画,其他两是关键帧动画

每一个动画都关联了对应的目标名称与属性,例如下面的代码:

Storyboard.TargetName="ttR"
Storyboard.TargetProperty="X"

它们目标是红色的小球,属性是红色小球的 X座标,所以当我们执行动画时,他会一直移动 . . .

效果图如下所示:

在这里插入图片描述

.
.


作者:浪子花梦

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章