动画类型回顾

  创建动画面临的第一个挑战是为动画选择正确的属性。期望的结果(例如,在窗口中移动元素)于需要使用的属性(在这种情况下是Canvas.Left和Canvas.Top属性)之间的关系并不总是很直观。西面是一些知道原则:

  • 如果希望使用动画来使元素显示和消失,不要使用 Visibility 属性(该属性只能在完全可见和完全不可见之间进行切换)。应改用 Opacity 属性淡入或淡出元素
  • 如果希望动态改变元素的位置,可考虑使用Canvas面板。它提供了最直接的属性(Canvas.Left 及 Canvas.Top),而且开销最小。此外,也可使用动画属性在其他布局容器中获得类似效果。例如,可通过使用 ThicknessAnimation 类动态改变 Margin 和 Padding 等属性,还可动态改变 Grid 控件中的 MinWidth 或 MinHeight 属性、一列或一行。
  • **动画最常用的属性是渲染变换。**可使用变换移动或翻转元素(TranslateTransform)、旋转元素(RotateTransform)、缩放或扭曲元素(ScaleTransform)等。通过仔细地使用变换,有时可避免在动画中硬编码尺寸和位置。它们也绕过了WPF布局系统,比直接作用于元素大小或位置的其他方法速度更快。
  • 动态改变元素表面的较好方法是修改画刷属性。可使用ColorAnimation 改变颜色或其他动画对象来变换更复杂画刷的属性,如渐变中的偏移。

动态变换

  变换提供了自定义元素的最强大方式之一。当使用变换时,不只是改变元素的边界,而且会移动、翻转、扭曲、拉伸、放大、缩小或旋转元素的整个可视化外观。

  每个元素都能以两种不同的方式使用变换:RenderTransform属性和 LayoutTransorm 属性RenderTransform 效率更高,因为是在布局之后应用变换并且用于变换最终的渲染输出LayoutTransform在布局前应用,从而其他控件需要重新排列以适应变换。改变 LayoutTransform属性会引发新的布局操作(除非在 Canvas面板上使用元素,在这种情况下RenderTransform和LayoutTransform 的效果相同)。

为在动画中使用变换,第一部是定义变换(动画可改变已经存在的变换,当不能创建新的变换)。例如,假设希望使按钮旋转,此时需要使用RotateTransform对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<Window.Resources>
<Style TargetType="{x:Type Button}">
<Setter Property="HorizontalAlignment"
Value="Center" />
<Setter Property="RenderTransformOrigin"
Value="0.5,0.5" />
<Setter Property="Padding"
Value="20,15" />
<Setter Property="Margin"
Value="2" />
<Style.Triggers>
<EventTrigger RoutedEvent="Button.MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="RenderTransform.Angle"
To="360"
Duration="0:0:0.8"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent="Button.MouseLeave">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="RenderTransform.Angle"
Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<Button Width="100"
Height="45"
Content="Button">
<Button.RenderTransform>
<RotateTransform />
</Button.RenderTransform>
</Button>
</Grid>

动态改变多个变换

  可很容易地组合使用变换。实际上这很容易——只需要使用TransformGroup对象设置LayoutTransformRenderTransform属性即可。可根据需要在TransformGroup对象中嵌套任意多个变换。

通过指定数字偏移值(0用于首先显示的RotateTransform对象,1要评语接下来显示的ScaleTransform对象),动画可与这两个交换对象进行交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<Window.Resources>
<Style TargetType="{x:Type Button}">
<Setter Property="HorizontalAlignment"
Value="Center" />
<Setter Property="RenderTransformOrigin"
Value="0.5,0.5" />
<Setter Property="Padding"
Value="20,15" />
<Setter Property="Margin"
Value="2" />
<Style.Triggers>
<EventTrigger RoutedEvent="Button.MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="RenderTransform.Children[0].Angle"
To="360"
Duration="0:0:0.8"
RepeatBehavior="Forever" />
<DoubleAnimation Storyboard.TargetProperty="RenderTransform.Children[1].ScaleX"
By="1.2"
Duration="0:0:0.8"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent="Button.MouseLeave">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="RenderTransform.Children[0].Angle"
Duration="0:0:0.2" />
<DoubleAnimation Storyboard.TargetProperty="RenderTransform.Children[1].ScaleX"
By="0.8"
Duration="0:0:0.8"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<Button Width="100"
Height="45"
Content="Button">
<Button.RenderTransform>
<TransformGroup>
<RotateTransform />
<ScaleTransform />
</TransformGroup>
</Button.RenderTransform>
</Button>
</Grid>

动态改变画刷

  动态改变画刷是WPF动画中的另一种常用技术,和动态变换同样容易。同样,这种技术使用恰当的动画类型,深入到希望改变的特定子属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<Window.Triggers>
<EventTrigger RoutedEvent="Window.Loaded">
<BeginStoryboard>
<Storyboard>
<PointAnimation Storyboard.TargetName="ellipse"
Storyboard.TargetProperty="Fill.GradientOrigin"
From="0.7,0.3"
To="0.3,0.7"
Duration="0:0:10" />
<ColorAnimation Storyboard.TargetName="ellipse"
Storyboard.TargetProperty="Fill.GradientStops[1].Color"
To="Black"
Duration="0:0:10"
AutoReverse="True"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Window.Triggers>
<Grid>
<Ellipse Name="ellipse"
Margin="5"
Stretch="Uniform">
<Ellipse.Fill>
<RadialGradientBrush RadiusX="1"
RadiusY="1"
GradientOrigin="0.7,0.3">
<GradientStop Color="White"
Offset="0" />
<GradientStop Color="Blue"
Offset="1" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
</Grid>

  为实现这个动画,需要使用两种尚未分析过的动画类型。ColorAnimation 动画在两个颜色之间逐渐混合,创建一种微妙的颜色转移效果。PointAnimation动画可将点从一个位置移到另一个位置(本质上与使用独立的DoubleAnimation,通过线性插值同时修改X坐标和Y坐标是相同的)。可使用 PointAnimation 动画改变使用点构造的图形,或者就像这个示例中那样,改变径向渐变中心点的位置。

动态改变像素着色器

  像素着色器(可为任意元素应用位图风格效果的低级例程,如模糊、辉光以及弯曲效果),就自身而言,像素着色器是一些有趣并且偶尔有用的工具。当通过结合使用动画,它们可变得更通用。可使用它们设计吸引眼球的过渡效果。也可使用像素着色器创建给人留下深刻印象的用户交互效果。最好为像素着色器的属性应用动画,就像为其他内容应用动画一样容易

  下例包含一系列按钮,并且当用户将鼠标移动到其中某个按钮上时,关联并开始动画。区别在于这个示例中的动画不是旋转按钮,而将模糊半径减少至 0。结果是移动鼠标时,最近的控件骤然间轻快地变得清。

  • EventTrigger.SourceName : 触发事件的元素
  • Storyboard.TargetName : 应用动画的元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<Window.Triggers>
<EventTrigger SourceName="button"
RoutedEvent="UIElement.MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="button"
Storyboard.TargetProperty="(UIElement.Effect).(BlurEffect.Radius)"
To="0"
Duration="0:0:0.4" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger SourceName="button"
RoutedEvent="UIElement.MouseLeave">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="button"
Storyboard.TargetProperty="(UIElement.Effect).(BlurEffect.Radius)"
To="10"
Duration="0:0:0.4" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Window.Triggers>
<Grid>
<Button x:Name="button"
Content="A Button"
Width="100"
Height="40">
<Button.Effect>
<BlurEffect Radius="10" />
</Button.Effect>
</Button>
</Grid>

关键帧动画

您到目前为止看到的所有动画都使用线性插值从起点移到终点。但如果需要创建具有多个分段的动画和不规则移动的动画,该怎么办呢?

  例如,可能希望创建一个动画,快速地将一个元素滑入到视图中,然后慢慢地将它移到正确位置。可通过创建两个连续的动画,并使用BeginTime 属性在第一个动画之后开始第二个动画来实现这种效果。然而,还有更简单的方法——可使用关键帧动画。

  关键帧动画是由许多较短的段构成的动画。每段表示动画中的初始值、最终值或中间值。当运行动画时,它平滑地从一个值移到另一个值

线性的关键帧动画

例如,分析下面的将 RadialGradientBrush 画刷的中心点从一个位置移到另一个位置的 Point动画:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<Window.Triggers>
<EventTrigger RoutedEvent="Window.Loaded">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<PointAnimationUsingKeyFrames Storyboard.TargetName="ellipse"
Storyboard.TargetProperty="Fill.GradientOrigin"
Duration="0:0:10"
RepeatBehavior="Forever">
<LinearPointKeyFrame Value="0.7,0.3"
KeyTime="0:0:0:0" />
<LinearPointKeyFrame Value="0.3,0.7"
KeyTime="0:0:0:5" />
<LinearPointKeyFrame Value="0.5,0.9"
KeyTime="0:0:0:8" />
<LinearPointKeyFrame Value="0.9,0.6"
KeyTime="0:0:0:10" />
<LinearPointKeyFrame Value="0.8,0.2"
KeyTime="0:0:0:12" />
<LinearPointKeyFrame Value="0.7,0.3"
KeyTime="0:0:0:14" />
</PointAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Window.Triggers>
<Grid>
<Ellipse Name="ellipse"
Margin="5"
Stretch="Uniform">
<Ellipse.Fill>
<RadialGradientBrush RadiusX="1"
RadiusY="1"
GradientOrigin="0.7,0.3">
<GradientStop Color="White"
Offset="0" />
<GradientStop Color="Blue"
Offset="1" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
</Grid>

  PointAnimationUsingKeyFrames 对象执行线性插值,从第一个关键帧平滑地移到第二个关键帧,就像 PointAnimation 对象对 From 和 To 值执行的操作一样。

每个关键帧动画都使用各自的关键帧对象(如LinearPointKeyFrame)。对于大部分内容,这些类是相同的——它们包含用于存储目标值的 Value 属性和用于指示帧何时到达目标值的 KeyTime属性。唯一的区别在于 Value 属性的数据类型。在LinearPointKeyFrame 类中是 Point 类型,在DoubleKeyFrame 类中是double 类型

使用关键帧动画不如使用多个连续的动画功能强大。最重要的区别是不能为每个关键帧应用不同的 AccelerationRatioDecelerationRatio 值,而只能为整个动画应用单个值。

离散的关键帧动画

  线性的关键帧动画在关键帧值之间平滑地过渡,另一种选择是使用离散的关键帧。对于这种情况,不尽兴插值,当到达关键时间时,属性突然改变为新值

  线性关键帧类使用“Linear + 数据类型 + KeyFrame”的形式进行命名。离散关键帧类使用“Discrete + 数据类型 + KeyFrame”的形式命名。下面是RadialGradientBursh画刷示例的修改版本,在该修改版本中使用的是离散关键帧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<Window.Triggers>
<EventTrigger RoutedEvent="Window.Loaded">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<PointAnimationUsingKeyFrames Storyboard.TargetName="ellipse"
Storyboard.TargetProperty="Fill.GradientOrigin"
Duration="0:0:10"
RepeatBehavior="Forever">
<DiscretePointKeyFrame Value="0.7,0.3"
KeyTime="0:0:0:0" />
<DiscretePointKeyFrame Value="0.3,0.7"
KeyTime="0:0:0:5" />
<DiscretePointKeyFrame Value="0.5,0.9"
KeyTime="0:0:0:8" />
<DiscretePointKeyFrame Value="0.9,0.6"
KeyTime="0:0:0:10" />
<DiscretePointKeyFrame Value="0.8,0.2"
KeyTime="0:0:0:12" />
<DiscretePointKeyFrame Value="0.7,0.3"
KeyTime="0:0:0:14" />
</PointAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Window.Triggers>
<Grid>
<Ellipse Name="ellipse"
Margin="5"
Stretch="Uniform">
<Ellipse.Fill>
<RadialGradientBrush RadiusX="1"
RadiusY="1"
GradientOrigin="0.7,0.3">
<GradientStop Color="White"
Offset="0" />
<GradientStop Color="Blue"
Offset="1" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
</Grid>

  当运行这个动画时,中心点会在适当的时间从一个位置跳到下一个位置。这是戏剧性的(但是不平稳的)效果。

  所有关键帧动画类都支持离散关键帧,但只有一部分关键帧动画类支持线性关键帧。这完全取决于数据类型。支持线性关键帧的数据类型也支持线性插值,并提供了相应的DataTypeAnimation类,如PointColor以及double。不支持线性插值的数据类型包括字符串(StringAnimationUsingKeyFrames)和对象(ObjectAnimationUsingKeyFrames)

可在同一个关键帧动画中组合使用两种类型的关键帧——线性关键帧和离散关键帧。

缓动关键帧

  您看到了如何使用缓动函数改进普通的动画。尽管关键帧动画被分割成多段,但每段仍使用普通的、令人厌烦的线性插值。
  如果这不是您希望的结果,可使用动画缓动为每个关键帧添加加速或减速效果。然而,普通的线性插俏关键帧类和离散关键帧类不支持该特征。相反,需要使用缓动关键帧,如EasingDoubleKeyFrame、EasingColorKeyFrame或EasingPointKeyFrame。每个缓动关键帧类和对应的线性插值关键帧类的工作方式相同,但是额外提供了EasingFunction 属性。

  下面的示例使用动画缓动为前5秒的关键帧动画应用加速效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<Window.Triggers>
<EventTrigger RoutedEvent="Window.Loaded">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<PointAnimationUsingKeyFrames Storyboard.TargetName="ellipse"
Storyboard.TargetProperty="Fill.GradientOrigin"
Duration="0:0:10"
RepeatBehavior="Forever">
<LinearPointKeyFrame Value="0.7,0.3"
KeyTime="0:0:0:0" />
<!--缓动关键帧,也可为每帧加上关键帧-->
<EasingPointKeyFrame Value="0.3,0.7"
KeyTime="0:0:0:5">
<EasingPointKeyFrame.EasingFunction>
<CircleEase/>
</EasingPointKeyFrame.EasingFunction>
</EasingPointKeyFrame>
<DiscretePointKeyFrame Value="0.5,0.9"
KeyTime="0:0:0:8" />
<DiscretePointKeyFrame Value="0.9,0.6"
KeyTime="0:0:0:10" />
<DiscretePointKeyFrame Value="0.8,0.2"
KeyTime="0:0:0:12" />
<DiscretePointKeyFrame Value="0.7,0.3"
KeyTime="0:0:0:14" />
</PointAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Window.Triggers>
<Grid>
<Ellipse Name="ellipse"
Margin="5"
Stretch="Uniform">
<Ellipse.Fill>
<RadialGradientBrush RadiusX="1"
RadiusY="1"
GradientOrigin="0.7,0.3">
<GradientStop Color="White"
Offset="0" />
<GradientStop Color="Blue"
Offset="1" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
</Grid>

样条关键帧动画

  还有一种关键帧类型:样条关键帧。每个支持线性关键帧的类也支持样条关键帧,它们使用“Spline+数据类型+KeyFrame”的形式进行命名。

  与线性关键帧一样,样条关键帧使用插值从一个键值平滑地移到另一个键值。区别是每个样条关键帧都有 KeySpline 属性。可使用该属性定义能影响插值方式的三次贝塞尔曲线。尽管为了得到希望的效果这样做有些繁琐(至少还没有高级的设计工具可辅助您工作),但这种技术能创建更加连贯的加速和减速以及更逼真的动画效果。

  贝塞尔曲线由起点、终点以及两个控制点定义。对于关键样条,起点总是(0,0),终点总是(1,1)。用户只需要提供两个控制点。创建的曲线描述了时间(X轴)和动画值(Y 轴)之间的关系。

  下面的示例通过对比 Canvas 面板上两个椭圆的移动,演示了一个关键样条动画。第一个椭圆使用DoubleAnimation动画缓慢匀速地在窗口上移动。第二个圆使用具有两个SplineDoubleKeyFrame 对象的 DoubleAnimationUsingKeyFrames 动画。两个椭圆同时到达目标位置(10 秒后),但第二个椭圆在运动过程中会有明显的加速和减速,加速时会超过第一个椭圆,而减速时又会落后于第一个椭圆。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<Window.Triggers>
<EventTrigger RoutedEvent="Window.Loaded">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="ellipse1"
Storyboard.TargetProperty="(Canvas.Left)"
To="500"
Duration="0:0:10" />
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ellipse2"
Storyboard.TargetProperty="(Canvas.Left)">
<SplineDoubleKeyFrame KeyTime="0:0:5"
Value="250"
KeySpline="0.25,0 0.5,0.7" />
<SplineDoubleKeyFrame KeyTime="0:0:10"
Value="500"
KeySpline="0.25,0.8 0.2,0.4" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Window.Triggers>
<Canvas>
<Ellipse Name="ellipse1"
Margin="5"
Width="20"
Height="20"
Canvas.Top="100"
Canvas.Left="0">
<Ellipse.Fill>
<RadialGradientBrush RadiusX="1"
RadiusY="1"
GradientOrigin="0.7,0.3">
<GradientStop Color="White"
Offset="0" />
<GradientStop Color="Blue"
Offset="1" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse Name="ellipse2"
Margin="5"
Width="20"
Height="20"
Canvas.Top="300"
Canvas.Left="0">
<Ellipse.Fill>
<RadialGradientBrush RadiusX="1"
RadiusY="1"
GradientOrigin="0.7,0.3">
<GradientStop Color="White"
Offset="0" />
<GradientStop Color="Red"
Offset="1" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
</Canvas>

  最快的加速发生在5秒后不久,也就是当进入第二个 SplineDoubleKeyFrame 关键帧时。贝赛尔曲线的第一个控制点将较大的表示动画进度(0.8)的Y轴值与较小的表示时间的X轴值相匹配。所以,在再次减慢速度前,椭圆在一小段距离内会增加速度。

  以图形方式显示了两条控制椭圆运动的曲线。为理解这些曲线,请记住它们从顶部到底部描述了动画过程。观察第一条曲线可以发现,它相对均地下降,在开始处有较短的暂停,在末尾处平缓下降。然而第二条曲线快速下降,运动了一大段距离,然后对于剩余的动画部分,曲线缓缓下降。

使用图形显示关键样条动画的过程

基于路径的动画

  基于路径的动画使用PathGeometry对象设置属性。尽管原则上基于路径的动画也能用于修改任何适当数据类型的属性,但当动态改变与位置相关的属性时最有用。实际上,基于路径的动画类主要用于沿着一条路径移动可视化对象。

  PathGeometry对象描述可包含直线、弧线以及曲线的图形,下图显示的示例具有一个PathGeometry对象,该对象包含两条弧线以及一条将最后定义的点连接到起点的直线段。这样就创建了一条闭合的路线,一个小的矢量图像以恒定不变的速度在这条路径上运动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<Window.Resources>
<!-- 路径资源 -->
<PathGeometry x:Key="path">
<PathFigure IsClosed="True">
<ArcSegment Point="100,200"
Size="15,10"
SweepDirection="Clockwise" />
<ArcSegment Point="400,50"
Size="5,5" />
</PathFigure>
</PathGeometry>
</Window.Resources>
<Window.Triggers>
<EventTrigger RoutedEvent="Window.Loaded">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimationUsingPath Storyboard.TargetName="image"
Storyboard.TargetProperty="(Canvas.Left)"
PathGeometry="{StaticResource path}"
Duration="0:0:5"
RepeatBehavior="Forever"
Source="X" />
<DoubleAnimationUsingPath Storyboard.TargetName="image"
Storyboard.TargetProperty="(Canvas.Top)"
PathGeometry="{StaticResource path}"
Duration="0:0:5"
RepeatBehavior="Forever"
Source="Y" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Window.Triggers>
<Canvas>
<!--路径-->
<Path Stroke="Red"
StrokeThickness="1"
Data="{StaticResource path}"
Canvas.Top="10"
Canvas.Left="10" />
<!--图形-->
<Image x:Name="image">
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<GeometryDrawing Brush="LightSteelBlue">
<GeometryDrawing.Geometry>
<GeometryGroup>
<EllipseGeometry Center="10,10"
RadiusX="9"
RadiusY="4" />
<EllipseGeometry Center="10,10"
RadiusX="4"
RadiusY="9" />
</GeometryGroup>
</GeometryDrawing.Geometry>
<GeometryDrawing.Pen>
<Pen Thickness="1"
Brush="Black" />
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</Canvas>

  正如您可能看到的,当创建基于路径的动画时,不是提供开始值和结束值,而是通过PathGeometry 属性指定希望使用的 PathGeometry 对象。一些基于路径的动画类,如 PointAnimationUsingPath 类,可同时为目标属性应用X和Y组件。但 DoubleAnimationUsingPath 类不具备这一能力,因为它只能设置双精度值。结果,还需要将 Source 属性设置为X或Y,以指示是使用路径的 X坐标还是了坐标

  尽管基于路径的动画可使用包含贝塞尔曲线的路径,但它与上一节中介绍的关键样条动画区别很大。在关键样条动画中,贝塞尔曲线描述动画进度和时间之间的关系,从而可以创建变速动画。但在基于路径的动画中,由直线和曲线的集合构成的路径决定了将用于动画属性的值。

基于路径的动画始终以恒定的速度运行。WPF 通过分析路径的总长度和指定的持续时间来确定速度。

基于帧的动画

  **除基于属性的动画系统外,WPF提供了一种创建基于帧的动画的方法,这种方法只使用代码。需要做的全部工作是响应静态的 CompositionTarget.Rendering 事件,触发该事件是为了给每帧获取内容。**这是一种非常低级的方法,除非使用标准的基于属性的动画模型不能满足需要(例如,构建简单的侧边滚动游戏、创建基于物理的动画或构建粒子效果模型(如火焰、雪花以及气泡)),否则不会希望使用这种方法。

  **构建基于帧的动画的基本技术很容易。只需要为静态的CompositionTarget.Rendering 事件关联事件处理程序。**一旦关联事件处理程序,WPF就开始不断地调用这个事件处理程序(只要渲染代码的执行速度足够快,WPF每秒将调用60次)。在染事件处理程序中,您需要在窗口中相应地创建或调整元素。换句话说,需要自行管理全部工作。当动画结束时,分离事件处理程序

  下面的示例中,随机数量的圆从Canvas面板的顶部向底部下落。它们(根据随机生成的开始速度)以不同速度下降。当所有的圆到达底部时,动画结束。
每个下落的圆由 Ellipse 元素表示。使用自定义的EllipseInfo 类保存椭圆的引用,并跟踪对于物理模型而言十分重要的一些细节。在这个示例中,只有如下信息很重要–椭圆沿 X轴的移动速度(可很容易地扩展这个类,使其包含沿着Y轴运动的速度、额外的加速信息等)。

1
2
3
4
5
6
7
8
9
10
11
public class EllipseInfo
{
public Ellipse Ellipse { get; set; }
public double VelocityY { get; set; }

public EllipseInfo(Ellipse ellipse,double velocityY)
{
Ellipse = ellipse;
VelocityY = velocityY;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal"
Margin="0 10">
<Button Margin="10 0"
Click="cmdStart_Clicked">Start</Button>
</StackPanel>
<Canvas Grid.Row="1"
x:Name="canvas"></Canvas>
</Grid>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
public partial class MainWindow : Window
{
private List<EllipseInfo> ellipses = new List<EllipseInfo>();

private double accelerationY = 0.1;
private int minStartingSpeed = 1;
private int maxStartingSpeed = 50;
private double speedRatio = 0.1;
private int minEllipses = 20;
private int maxEllipses = 100;
private int ellipseRadius = 10;

public MainWindow()
{
InitializeComponent();
}

private bool rendering = false;

private void cmdStart_Clicked(object sender, RoutedEventArgs e)
{
if (!rendering)
{
ellipses.Clear();
canvas.Children.Clear();

CompositionTarget.Rendering += RenderFrame;
rendering = true;
}
}

private void RenderFrame(object? sender, EventArgs e)
{
if (ellipses.Count == 0)
{
int halfCanvasWidth = (int)canvas.ActualWidth / 2;

Random rand = new Random();
int ellipseCount = rand.Next(minEllipses, maxEllipses + 1);
for (int i = 0; i < ellipseCount; i++)
{
Ellipse ellipse = new Ellipse();
ellipse.Fill = Brushes.LimeGreen;
ellipse.Width = ellipseRadius;
ellipse.Height = ellipseRadius;

Canvas.SetLeft(ellipse, halfCanvasWidth + rand.Next(-halfCanvasWidth, halfCanvasWidth));
Canvas.SetTop(ellipse, 0);
canvas.Children.Add(ellipse);

// Track the ellipse
EllipseInfo info = new EllipseInfo(ellipse, speedRatio * rand.Next(minStartingSpeed, maxStartingSpeed));
ellipses.Add(info);
}
}
else
{
for (int i = ellipses.Count - 1; i >= 0; i--)
{
EllipseInfo info = ellipses[i];
double top = Canvas.GetTop(info.Ellipse);
Canvas.SetTop(info.Ellipse, top + 1 * info.VelocityY);

if (top >= (canvas.ActualHeight - ellipseRadius * 2))
{
// This circle has reached the bottom.
// Stop animating it.
ellipses.Remove(info);
}
else
{
// Increase the velocity
info.VelocityY += accelerationY;
}

}

if (ellipses.Count == 0)
{
CompositionTarget.Rendering -= RenderFrame;
rendering = false;
}

}
}
}

  在这个示例中,每个下落的圆由 Ellipse 元素表示。使用自定义的Ellipselnfo 类保存椭圆的引用,并跟踪对于物理模型而言十分重要的一些细节。在这个示例中,只有如下信息很重要–椭圆沿 X轴的移动速度(可很容易地扩展这个类,使其包含沿着Y轴运动的速度、额外的加速信息等)。

  当构建基于帧的动画时需要注意如下问题:它们不依赖于时间。换句话说,动画可能在能好的计算机上运动得更快,因为帧率会增加,会更频繁地调用CompositionTarget.Renderin事件。为补偿这种效果,需要编写考虑当前时间的代码

使用代码创建故事板

  当需要处理多个动画并且预先不知道将有多少个动画或不知道如何配置动画时,就会用到代码创建动画。如果希望在不同的窗口中使用相同的动画,或者只是希望从标记中灵活地分离出所有与动画相关的细节以方便重用,也会遇到这种情况。

  通过编写代码创建、配置和启动故事板并不难。只需要创建动画和故事板对象,并将动画添加到故事板中,然后启动故事板即可。在动画结束后可响应 Storyboard.Completed 事件以执行所有清理工作。

  在接下来的示例中,您将看到如何创建实现一个投炸弹💣的游戏。在该例中,投下的一系列炸弹的速度始终不断增加。玩家必须单击每个炸弹以逐一拆除。当达到设置的极限时——默认情况下是落下5个炸弹-游戏结束

  在这个示例中,投下的每颗炸弹都有自己的包含两个动画的故事板。第一个动画使炸弹下落(通过为 Canvas.Top 属性应用动画),而第二个动画稍微前后旋转炸弹,使其具有逼真的摆动效果。如果用户单击一颗下落的炸弹,这些动画就会停止,并且会发生另外两个动画,使炸弹倾斜,悄然间离开 Canvas 面板的侧边。最后,每次结束一个动画,应用程序都会进行检查,以查看该动画是表示炸弹被拆除了还是落下了,并相应地更新计数。

创建Bomb控件

  用于炸弹的标记包含 RotateTransfomm 变换,动画代码可使用该变换为下落中的炸弹应用摆动效果。尽管可通过编写代码创建并添加这个 RotateTransfom 变换,但在炸弹的 XAML 文件中定义该变换更加合理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<UserControl x:Class="WpfApp.Controls.Bomb"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp.Controls"
mc:Ignorable="d">
<UserControl.RenderTransform>
<TransformGroup>
<RotateTransform Angle="20"
CenterX="50"
CenterY="50" />
<ScaleTransform ScaleX="0.5"
ScaleY="0.5" />
</TransformGroup>
</UserControl.RenderTransform>
<Canvas>
<!--The Path elemebts that draw the bomb graphic are defined here-->
<TextBlock Text="💣" FontSize="50"/>
</Canvas>
</UserControl>

1
2
3
4
5
6
7
8
9
10
11
12
public partial class Bomb : UserControl
{
public Bomb()
{
InitializeComponent();
}

/// <summary>
/// 跟踪炸弹当前是否正在下落
/// </summary>
public bool IsFalling { get; set; }
}

创建主窗口

  为了投弹,应用程序使用DispatcherTimer,这是一种能很好地用于WPF用户界面的计时器,因为它在用户界面线程触发事件。选择时间间隔,伺候DispatcherTimer会在该时间间隔内引发周期性的Tick事件。

在投弹游戏中,计时器最初被设置为每隔1.3秒引发一次。当用户单击按钮开始游戏时,计时器随之启动,每次引发计时器事件时,代码创建一个新的 Bomb 对象并设置其在 Canvas 面板上的位置。炸弹放在 Canvas 面板的顶部边缘,使其可以无缝地落入视图。炸弹的水平位置是随机的,位于Canvas 面板的左侧和右侧之间:

  随着游戏的进行,游戏难度加大。更频繁地引发计时器事件,从而炸弹之间的距离越来越近,并且减少了下落时间。为实现这些变化,每经过一定的时间间隔就调整一次计时器代码。默认情况下,BombDropper 每隔 15 秒调整一次。下面是控制调整的字段

拦截炸弹

  用户通过在炸弹到达 Canvas 面板底部之前单击炸弹来进行拆除。因为每个炸弹都是单独的Bomb 用户控件实例,所以拦截鼠标单击很容易–需要做的全部工作就是处理MouseLeft-ButtonDown 事件,当单击炸弹的任意部分时会引发该事件(但如果单击背景上的某个地方,例如炸弹圈边缘的周围,不会引发该事件)。

  当单击炸弹时,第一步是获取适当的炸弹对象,并设置其 IsFalling 属性以指示不再下降(在处理动画完成的事件处理程序中会使用IsFalling 属性)。

  单击炸弹后,使用另一个动画集将炸弹移出屏幕,将炸弹抛向上方、抛向左侧或右侧(取决于距离哪一侧最近)。尽管可创建全新的故事板以实现该效果,但BombDropper 游戏清空用于炸弹的当前故事板并为其添加新动画。处理完毕后,启动新的故事板:

统计炸弹和清理工作

  为下落的炸弹应用动画以及为拆除的炸弹应用动画。可使用不同的事件处理程序处理这些故事板的结束事件,但为使代码保持简单,BombDropper 只使用一个事件处理程序。通过检査 Bomb.IsFalling属性来区分爆炸的炸弹和拆除的炸弹。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<Window x:Class="WpfApp.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp"
mc:Ignorable="d"
Title="MainWindow"
Height="450"
Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.6*" />
<ColumnDefinition Width="0.4*" />
</Grid.ColumnDefinitions>

<Border Grid.Column="0"
BorderBrush="SteelBlue"
BorderThickness="1"
Margin="5">
<Grid>
<Canvas x:Name="canvasBackground"
SizeChanged="canvasBackground_SizeChanged"
MinWidth="50">
<Canvas.Background>
<RadialGradientBrush>
<GradientStop Color="AliceBlue"
Offset="0" />
<GradientStop Color="White"
Offset="0.7" />
</RadialGradientBrush>
</Canvas.Background>
</Canvas>
</Grid>
</Border>

<Border Grid.Column="1"
BorderBrush="SteelBlue"
BorderThickness="1"
Margin="5">
<Border.Background>
<RadialGradientBrush GradientOrigin="1,0.7"
Center="1,0.7"
RadiusX="1"
RadiusY="1">
<GradientStop Color="Orange"
Offset="0" />
<GradientStop Color="White"
Offset="1" />
</RadialGradientBrush>
</Border.Background>
<StackPanel Margin="15"
VerticalAlignment="Center"
HorizontalAlignment="Center">
<TextBlock FontFamily="Impact"
FontSize="35"
Foreground="LightSteelBlue">Bomb Dropper</TextBlock>
<TextBlock x:Name="lblRate"
Margin="0 30 0 0"
TextWrapping="Wrap"
FontFamily="Georgia"
FontSize="14" />
<TextBlock x:Name="lblSpeed"
Margin="0 30"
TextWrapping="Wrap"
FontFamily="Georgia"
FontSize="14" />
<TextBlock x:Name="lblStatus"
TextWrapping="Wrap"
FontFamily="Georgia"
FontSize="20">
No bombs have dropped.
</TextBlock>
<Button x:Name="cmdStart"
Padding="5"
Margin="0 30"
Width="80"
Content="Start Game"
Click="cmdStart_Click" />
</StackPanel>
</Border>

</Grid>
</Window>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
public partial class MainWindow : Window
{
private DispatcherTimer bombTimer = new DispatcherTimer();

public MainWindow()
{
InitializeComponent();

bombTimer.Tick += BobmTimer_Tick;
}

// Keep track of how many bombs are dropped and stopped
private int droppedCount = 0;
private int savedCount = 0;

// Initially, bombs fall every 1.3 seconds, and hit the ground after 3.5 seconds.
private double initialSecondsBetweenBombs = 1.3;
private double initialSecondsToFall = 3.5;
private double secondsBetweenBombs;
private double secondsToFall;

// Make it possible to look up a storyboard based on a bomb.
private Dictionary<Bomb, Storyboard> bombs = new Dictionary<Bomb, Storyboard>();

private void BobmTimer_Tick(object? sender, EventArgs e)
{
// Create the bomb
Bomb bomb = new Bomb();
bomb.IsFalling = true;

// Position the bomb.
Random random = new Random();
bomb.SetValue(Canvas.LeftProperty, (double)(random.Next(0, (int)(canvasBackground.ActualWidth - 50))));
bomb.SetValue(Canvas.TopProperty, -100.0);

// Add the bomb to the Canvas.
canvasBackground.Children.Add(bomb);

// Attach mouse click event (for defusing the bomb).
bomb.MouseLeftButtonDown += Bomb_MouseLeftButtonDown;

// Creatye the animation for the falling bomb.
Storyboard storyboard = new Storyboard();
DoubleAnimation fallAnimation = new DoubleAnimation();
fallAnimation.To = canvasBackground.ActualHeight;
fallAnimation.Duration = TimeSpan.FromSeconds(secondsToFall);

Storyboard.SetTarget(fallAnimation, bomb);
Storyboard.SetTargetProperty(fallAnimation, new PropertyPath("(Canvas.Top)"));
storyboard.Children.Add(fallAnimation);

// Create the animation for the bomb "wiggle"
DoubleAnimation wiggleAnimation = new DoubleAnimation();
wiggleAnimation.To = 30;
wiggleAnimation.Duration = TimeSpan.FromSeconds(0.2);
wiggleAnimation.RepeatBehavior = RepeatBehavior.Forever;
wiggleAnimation.AutoReverse = true;

Storyboard.SetTarget(wiggleAnimation, ((TransformGroup)bomb.RenderTransform).Children[0]);
Storyboard.SetTargetProperty(wiggleAnimation, new PropertyPath("Angle"));
storyboard.Children.Add(wiggleAnimation);

bombs.Add(bomb, storyboard);

storyboard.Duration = fallAnimation.Duration;
storyboard.Completed += Storyboard_Completed;
storyboard.Begin();

// Perform and "adjustment" when needed.
if ((DateTime.Now.Subtract(lastAdjustmentTime).TotalMinutes > secondsBetweenAdjustments))
{
lastAdjustmentTime = DateTime.Now;

secondsBetweenBombs -= secondsBetweenBombsRaduection;
secondsToFall -= secondsToFallReduction;

// (Technically, you should check for 0 or negative values)
// However, in paratice these won't occur because the game will always end first.)

// Set the timer to drop the next bomb at the appropriate time.
bombTimer.Interval = TimeSpan.FromSeconds(secondsBetweenBombs);

// Update the status message.
lblRate.Text = String.Format("A bomb is released every {0} seconds.", secondsBetweenBombs);
lblSpeed.Text = String.Format("Each bomb takes {0} seconds to faill.", secondsToFall);
}

}

// 随着游戏的进行,游戏难度加到。很频繁地引发计时器时间,从而炸弹之间的距离越来越近,并且减少了下落时间
// Perform an adjustment every 15 seconds.
private double secondsBetweenAdjustments = 15;
private DateTime lastAdjustmentTime = DateTime.MinValue;

// After every adjustment, shave 0.1 seconds off both.
private double secondsBetweenBombsRaduection = 0.1;
private double secondsToFallReduction = 0.1;


// End the game when 5 bombs have fallen.
private int maxDropped = 5;

private void Storyboard_Completed(object? sender, EventArgs e)
{
ClockGroup clockGroup = (ClockGroup)sender;

// Get the first animation in the storyboard, and use it to find the bomb that's being animated.
DoubleAnimation completedAnimation = (DoubleAnimation)clockGroup.Children[0].Timeline;
Bomb completedBomb = (Bomb)Storyboard.GetTarget(completedAnimation);

// Determine if a bomb fell or flew off the Canvas after begin clicked.
if (completedBomb.IsFalling)
{
droppedCount++;
}
else
{
savedCount++;
}

// Update the display
lblStatus.Text = String.Format("You have dropped {0} bombs and saved {1}.", droppedCount, savedCount);

// Check if it's game over.
if (droppedCount >= maxDropped)
{
bombTimer.Stop();
lblStatus.Text += "\r\n\r\nGame over.";

// Find all the storyboards that are underway
foreach (KeyValuePair<Bomb, Storyboard> item in bombs)
{
Storyboard storyboard = item.Value;
Bomb bomb = item.Key;

storyboard.Stop();
canvasBackground.Children.Remove(bomb);
}

// Empty the tracking collection.
bombs.Clear();

// Allow the user to start a new game.
cmdStart.IsEnabled = true;
}
else
{
// Clean up just this bomb, and let the game continue.
Storyboard stopryboard = (Storyboard)clockGroup.Timeline;
stopryboard.Stop();

bombs.Remove(completedBomb);
canvasBackground.Children.Remove(completedBomb);
}
}

private void Bomb_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// Get the bomb.
Bomb bomb = sender as Bomb;
bomb.IsFalling = false;

// Record the bomb's current (animated) position.
double curentTop = Canvas.GetTop(bomb);

// Stop the bomb from falling.
Storyboard storyboard = bombs[bomb];
storyboard.Stop();

storyboard.Children.Clear();

DoubleAnimation riseAnimation = new DoubleAnimation();
riseAnimation.From = curentTop;
riseAnimation.To = 0;
riseAnimation.Duration = TimeSpan.FromSeconds(2);

Storyboard.SetTarget(riseAnimation, bomb);
Storyboard.SetTargetProperty(riseAnimation, new PropertyPath("(Canvas.Top)"));
storyboard.Children.Add(riseAnimation);

DoubleAnimation slideAnimation = new DoubleAnimation();
double currentLeft = Canvas.GetLeft(bomb);

// Throw the bomb off the closest side.
if (currentLeft < canvasBackground.ActualWidth / 2)
{
slideAnimation.To = -100;
}
else
{
slideAnimation.To = canvasBackground.ActualWidth + 100;
}
slideAnimation.Duration = TimeSpan.FromSeconds(1);
Storyboard.SetTarget(slideAnimation, bomb);
Storyboard.SetTargetProperty(slideAnimation, new PropertyPath("(Canvas.Left)"));
storyboard.Children.Add(slideAnimation);

// Start the new animation.
storyboard.Duration = slideAnimation.Duration;
storyboard.Begin();

}

private void canvasBackground_SizeChanged(object sender, SizeChangedEventArgs e)
{
// Set the clipping region to match the current display region of the Canvas.
RectangleGeometry rect = new RectangleGeometry();
rect.Rect = new Rect(0, 0, canvasBackground.ActualWidth, canvasBackground.ActualHeight);
canvasBackground.Clip = rect;
}

private void cmdStart_Click(object sender, RoutedEventArgs e)
{
cmdStart.IsEnabled = false;

// Reset the game
droppedCount = 0;
savedCount = 0;
secondsBetweenBombs = initialSecondsBetweenBombs;
secondsToFall = initialSecondsToFall;

// Start the bomb-dropping timer.
bombTimer.Interval = TimeSpan.FromSeconds(secondsBetweenBombs);
bombTimer.Start();
}
}

  现在已经完成了 BombDropper 游戏的代码。然而,可进行诸多改进。例如,可执行如下改进:

  • 为炸弹添加爆炸动画效果。这种效果使炸弹周围的火焰闪耀或发射在 Canvas 面板上四处飞溅的炸弹碎片。
  • 为背景添加动画。此改进易于实现,可添加精彩的可视化效果。例如,可创建上移的线性渐变,产生移动感,或创建在两种颜色之间过渡的效果。
  • 添加深度。实现这一改进比您想象得要容易。基本技术是为炸弹设置不同尺寸。更大的炸弹应当具有更高的 ZIndex 值,确保大炸弹重叠在小炸弹之上,而且应为大炸弹设置更短的动画时间,从而确保它们下落得更快。还可使炸弹半透明,从而当一个炸弹下落时仍能看到它背后的其他炸弹。
  • 添加音效。可使用准确计时的声音效果以强调炸弹爆炸或拆除。
  • 使用动画缓动。如果希望炸弹在下落、弹离屏幕时加速,或更自然地摆动,可为此处使用的动画添加缓动函数。并且,正如您所期望的,可使用代码构造缓动函数,就像在 XAML中构造缓动函数一样容易。
  • 调整参数。可为修改行为提供更多细节(例如,当游戏运行时设置如何修改炸弹运动时间、轨迹以及投放频率的变量),还可插入更多随机因素(例如,使拆除的炸弹以稍有不同的方式弹离 Canvas 面板)。