事件触发器与动画
本章的案例可以都研究一番.
属性触发器允许某个属性在符合要求时, 会引用特定资源 (样式). 本章提供另一个方面的触发器, 即某个事件触发时调用触发器. 而动画是一系列顺滑的属性变更.
- 属性样式的变更是突发性的, 而动画则是渐变的
- 而且系统提供了丰富的事件可以进行控制, 而属性相对较少
事件触发
首先作者依旧将属性触发器做了一个简要复习, 作为引子, 通过对比来引入事件触发起. 并一次说明了鼠标进入, 离开, 移动事件.
要创建事件触发器, 可以在控件或样式中添加 Triggers
标签属性, 然后在其中添加 EventTrigger
元素.
每一个 EventTrigger
元素都有 RoutedEvent
属性, 用于描述监听什么事件. 其值为 发起事件的控件类型.事件名
.
然后在 EventTrigger
中提供 Actions
标签属性, 在 Actions
中可以编写需要执行的动画 (这里被称为故事板 storyboard).
通常会在 Actions
中使用 BeginStoryboard
元素来执行 Storyboard
对象.
后面会介绍
Storyboard
的细节, 这里假设在Window
资源中已定义好, 命名为sbBigScale
.
例如:
<Button Width="100" Height="50" Content="点击">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard Storyboard="{StaticResource sbBigScale}">
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Button.Triggers>
</Button>
这里的
BeginStoryboard
就是运行故事面板的意思, 逻辑上标签命名为RunStoryboard
更好.
事件触发位置 (Event Trigger Locations)
作者描述了一个点击按钮后, 按钮会旋转一个角度的示例.
其实不明其作用.
Storyboard
无法创建一个变换, 但是可以修改Transformation
属性的值. 因此一般要实现一个变换效果, 都是先定义一个初始值, 然后在Storyboard
中对其进行修改.
故事版 (Storyboard
) 也是一种资源, 目的是简化按钮的定义. 也可以将 EventTrigger
定义在 按钮的 Style
中.
然后作者又演示了一个 demo, 集中来维护事件. 这个案例是左边显示小图标, 点击后会移动放大显示在右侧.
在该示例中有一个 RemoveStoryboard
, 用于移除之前没有执行完的故事板.
本例的代码可以研究一下.
属性元素中的 Storyboard
Storyboard
也是一个属性, 代码逻辑如下:
<Button>
<Button.Triggers>
<EventTrigger RoutedEvent="...">
<EventTrigger.Actions>
</EventTrigger.Actions>
</EventTrigger>
</Button.Triggers>
</Button>
样式中的 Storyboard
将 Storyboard
定义在按钮中, 会使代码难以维护. 所以可以将其定义在 Style
中
<Style x:Key="..." TargetType="...">
<Style.Triggers>
<EventTrigger RoutedEvent="...">
<EventTrigger.Actions>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style>
使用的时候, 直接使用该样式, 同时具备了对应的事件触发器:
<Button Style="{StaticResource ...}"></Button>
属性触发动画
通常, 属性触发器使用 setter 来修改属性值, 但是也可以用来运行 storyboard. 属性触发器运行 storyboard 有别与 setter.
setter 修改属性, 是在属性具备某个值的时候立即进行的. 有点突变的感觉.
而动画的播放是有一个过程的. 类似于渐变. 但是这会存在一个问题. 比如动画播放只需要十秒, 而属性值的变化持续了 20 秒, 那么后 10 秒如何呈现? 重复播放动画. 似乎有点不合适.
jk: 是不是会重复有待验证.
为了合理的应用动画, 属性触发器提供了两个额外的部分, 也可以执行动画: EnterAction
和 ExitAction
.
- setter 依旧是立即执行.
EnterAction
节点中的动画会在属性值变化成指定值时触发.ExitAction
节点中的动画会在属性值编程另一个值的时候触发.
演示代码片段:
<Style x:Key="btnStyle" TargetType="Button">
<Setter Property="Margin" Value="10" />
<Setter Property="Width" Value="100" />
<Setter Property="Height" Value="50" />
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="False" >
<Setter Property="FontStyle" Value="Italic" />
<Trigger.EnterAction >
<BeginStoryboard Storyboard="{StaticResource sbBigSize}" />
</Trigger.EnterAction>
<Trigger.ExitAction>
<BeginStoryboard Storyboard="{StaticResource sbSmallSize}" />
</Trigger.ExitAction>
</Trigger>
</Style.Triggers>
</Style>
然后作者解释了这段代码的执行特征. 包括执行时间等.
STORYBOARD
至此已经知道如何执行 Storyboard 了, 下面看看怎么定义. 一个 Storyboard 就是一个对象, 它定义了一个或多个动画的时间线.
animation
在一段时间内, 让属性从一个值变成另一个值. 动画可以设定从什么时候开始, 持续多久时间.
Animation 类让动画创建变得简单, 你只需要设定要需要在一定时间内, 某些属性需要变化的值即可. frame
的数量决定了动画的速度.
Animation 类让你设定要变成什么, 不用去关心怎么变.
要保证动画的性能, 请尽可能缩小动画影响的范围. 因为某个控件的大小可能会影响到周围控件的变化. 这与一些布局控件有一定关系.
具体的动画内容就是在设置不同类型的属性, 例如 Double
, Color
, String
, Thickness
等.
下面是 5 个简单的 DoubleAnimation
对象:
<Storyboard x:Key="sbImg1">
<DoubleAnimation Duration="0:0:0.5" To="120"
Storyboard.TargetName="img1"
Storyboard.TargetProperty="(Canvas.Left)"/>
<DoubleAnimation Duration="0:0:0.5" To="0"
Storyboard.TargetName="img1"
Storyboard.TargetProperty="(Canvas.Top)"/>
<DoubleAnimation Duration="0:0:0.5" To="485"
Storyboard.TargetName="img1"
Storyboard.TargetProperty="Width"/>
<DoubleAnimation Duration="0:0:0.5" To="400"
Storyboard.TargetName="img1"
Storyboard.TargetProperty="Height"/>
<DoubleAnimation Duration="0:0:0.5" To="1"
Storyboard.TargetName="img1"
Storyboard.TargetProperty="Opacity"/>
</Storyboard>
然后作者介绍了一下这段代码的作用, 在 0.5 秒内更改了属性值.
下面的章节介绍了一些常用的属性. 下一节会介绍常用动画类.
Storyboard 和动画属性
animation 类提供了一些控制动画的属性. 例如 Duration
表示动画持续的时间.
Storyboard 类也提供了部分同名的属性, 其 animation 会继承这些属性. 可以做到统一设定.
下面是 animation 与 Storyboard 的属性:
AccelerationRatio
类似于加速度, 数值在达到目标值前的加速率.AutoReverse
若为 true, 表示数值达到目标后, 反向变化. 可以作为往复运动的控制.BeginTime
启动 storyboard 后多久开始进行动画. 取值格式为 "时:分:秒"
如果动画没有出现, 可以考虑检查该属性是否设置错误.
DecelerationRatio
与AccelerationRatio
相反.Duration
动画持续时间. 格式为 "时:分:秒".FillBehavior
动画结束后属性值该怎么变化:HoldEnd
保留最终值,Stop
恢复默认值.RepeatBehavior
表示动画怎么重复. 可取值有 3 种: 1. 重复次数, 如2x
,5x
; 2. 一个时间, 例如0:0:10
表示动画持续 10 秒; 3.Forever
, 表示保持重复.SpeedRatio
相对父元素的速度比. 例如为2
就表示速度比父元素的快一倍. 可用在一个Storyboard
中有多个动画的时候加以控制.
然后作者设计了一个赛马的案例:
<Storyboard>
<DoubleAnimation Duration="0:0:2" From="5" To="500"
Storyboard.TargetName="img1"
Storyboard.TargetProperty="(Canvas.Left)"/>
<DoubleAnimation Duration="0:0:2" From="5" To="500"
AccelerationRatio="0.4"
DecelerationRatio="0.2"
Storyboard.TargetName="img2"
Storyboard.TargetProperty="(Canvas.Left)"/>
</Storyboard>
Animation 有一些额外的属性, 不从 Storyboard 继承:
From
表示动画中的属性值从什么开始.To
表示动画结束的值.By
用于表示属性值的增量, 即在动画结束时, 属性值与原始值的变化是多少.
似乎
By
与To
是有冲突的.
From
如果不指定, 则表示动当前值开始变化.
动画的类型
动画就是按照一个时间规律修改某个属性的值, 属性是有类型的, 动画的类型依赖于属性的类型.
WPF 包含的类型有:
Boolean
, Byte
, Char
, Color
, Decimal
, Double
, Int16
, Int32
, Int64
, Matrix
, Object
, Point
, Point3D
, Quaternion
, Rect
, Rotation3D
, Single
, Size
, String
, Thickness
, Vector
, Vector3D
.
每一个类型对应了两种动画:
Animation
后缀, 表示线性动画. 数据变化是从某个值开始到另一个值.AnimationUsingKeyFrame
后缀, 表示关键帧动画. 数据从某个值开始到另一个值的过程中, 一定会先到某个指定的值. 这个指定的值即为关键帧, 也可以是各种类型的值. 不同类型决定了不同的变化.
大多数关键帧的动画在数值变化范围内都是线性的, 样条曲线的, 或离散的.
下面逐个说明: 线性的, 样条的, 离散的, 以及 path 的.
简单线性动画
线性动画使用简单的方式将数值从一个值变化到另一个值. 例如:
<Storyboard>
<DoubleAnimation Duration="0:0:2" From="5" To="500"
Storyboard.TargetName="img1"
Storyboard.TargetProperty="(Canvas.Left)">
</DoubleAnimation>
<DoubleAnimation Duration="0:0:2" From="5" To="500"
AccelerationRatio="0.4"
DecelerationRatio="0.2"
Storyboard.TargetName="img2"
Storyboard.TargetProperty="(Canvas.Left)">
</DoubleAnimation>
</Storyboard>
两个动画的区别在于, 第二个动画有一个加速度, 和一个减速度的效果. 而第一个动画是匀速的.
如果指定了
Storyboard.TargetName
, 那么就是独有的, 如果找不到控件会报错. 不设置表示可以用于多个控件.
- 单独的定义
Storyboard
.- 定义
Style
, 在其中定义EventTrigger
, 来启用动画.- 或将
EventTrigger
定义在控件的Triggers
中.
线性关键帧 (Linear Key Frames)
使用方法与线性动画类似:
- 使用
AnimationUsingKeyFrames
后缀的动画类型. - 设置
Storyboard.Target***
来指定修改什么控件的什么属性. - 在标签中提供
LinearDoubleKeyFrame
, 并设置Value
和KeyTime
, 表明在什么时刻要变成什么值.
例如:
<Storyboard>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="btnMover"
Storyboard.TargetProperty="(Canvas.Left)">
<LinearDoubleKeyFrame Value="10" KeyTime="0:0:1.5"></LinearDoubleKeyFrame>
<LinearDoubleKeyFrame Value="100" KeyTime="0:0:2.5"></LinearDoubleKeyFrame>
<LinearDoubleKeyFrame Value="10" KeyTime="0:0:4"></LinearDoubleKeyFrame>
</DoubleAnimationUsingKeyFrame>
</Storyboard>
样条关键帧 (Spline Key Frames)
样条关键帧是一个由点控制的曲线. 其变化与控制点如图所示
有点像贝塞尔曲线. 然后作者解释了这个变化曲线的作用, 以及数据改变的效果特征.
要使用这个效果, 只需要提供 KeySpline
属性, 它有提供两个点 (控制点) 的规范化坐标, 即取值在 0 到 1 之间. 例如:
<Storyboard x:Key="a3">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="Canvas.Left">
<SplineDoubleKeyFrame Value="300" KeyTime="0:0:5"
KeySpline="0.5,0 0.5,1"></SplineDoubleKeyFrame>
<SplineDoubleKeyFrame Value="600" KeyTime="0:0:7"
KeySpline="0.5,0 0.5,1"></SplineDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
经测试 TargetProperty
需要括号.
离散关键帧
线性与样条关键帧使得动画的变换都是连续的, 离散关键帧会使属性的变化是跳跃式的.
使用 DiscreteDoubleKeyFrame
代替 SplineDoubleKeyFrame
即可.
路径动画 (Path Animation)
依照路径的定义与方向来变化.
定义
PathGeometry
, 使用Figures
属性来定义路径.使用
AnimationUsingPath
后缀的动画.需要设置
Source
来确定使用什么分量.
例如:
<PathGeometry x:Key="pathMove"
Figures="M 10,85
A 100,70 0 1 1 210,85
A 100,70 0 1 0 410,85
A 130,70 0 1 0 150,85
A 70, 70 0 1 1 10,85" />
<Storyboard x:key="sbMoveButton" RepeatBehavior="Forever">
<DoubleAnimationUsingPath Duration="0:0:4"
Storyboard.TargetName="btnMover"
Storyboard.TargetProperty="(Canvas.Left)"
Source="X"
PathGeometry="{StaticResource pathMove}" />
<DoubleAnimationUsingPath Duration="0:0:4"
Storyboard.TargetName="btnMover"
Storyboard.TargetProperty="(Canvas.Top)"
Source="Y"
PathGeometry="{StaticResource pathMove}" />
<DoubleAnimationUsingPath Duration="0:0:4"
Storyboard.TargetName="btnMover"
Storyboard.TargetProperty="RenderTransform.Angle"
Source="Angle"
PathGeometry="{StaticResource pathMove}" />
</Storyboard>
混合与匹配关键帧
就是说在一个 Storyboard 中可以根据时间定义不同的动画对象.
特殊情况
WPF 给下列动画类型提供了大多数类型的类
- 简单动画 (线性动画)
- 线性关键帧动画
- 样条关键帧动画
- 离散关键帧动画
除此之外, 对于 String, Boolean, Char 的类型, 仅提供了离散关键帧动画. 很显然, banana 无法线性的变成 time.
控制 Storyboard
BeginStoryboard
, 开始运行.PauseStoryboard
, 暂停.SeekStoryboard
, 将Storyboard
设置为时间线上的一个特殊值.StopStoryboard
停止,逻辑上可以理解为重置.RemoveStoryboard
, 停止并释放资源.
使用
EventTrigger
将控制与 按钮关联起来. 有一个盲区, 就是EventTrigger
的SourceName
属性. 经验证, 定义在与按钮同级别的容器中的Trigger
, 使用 按钮的Name
作为SourceName
, 用来控制是由那个按钮来响应对应的Trigger
.
一般来说动画 Trigger
与样式分开定义.
MEDIA 与 TIMELINE
BeginStoryboard
, PauseStoryboard
等这类控制类都是对动作的包装, 用于执行动作行为. 不同于一些物理对象, 如按钮, 文本等. 还有一个有用的包装动作类为 SoundPlayerAction
, 它用于播放声音.
<Button Width="100" Height="50" Content="播放">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<EventTrigger.Actions>
<SoundPlayerAction Source="002-夜曲 - 周杰伦.wav" />
</EventTrigger.Actions>
</EventTrigger>
</Button.Triggers>
</Button>
需要注意的是资源的属性
这种似乎没办法精细控制.
也可以在 Storyboard 内部播放声音. 只需要在 Window 中添加 MediaElement
控件即可. 然后在 Storyboard 中添加 MediaTimeline
对象, 设置其 TargetName
为 MediaElement
对象, 以及 Source
属性为音频文件 (以内容形式存在).
<Grid>
<Grid.Resources>
<Storyboard x:Key="p" RepeatBehavior="Forever">
<MediaTimeline BeginTime="0:0:1"
Storyboard.TargetName="player"
Source="002-夜曲 - 周杰伦.wav" />
</Storyboard>
</Grid.Resources>
<MediaElement Grid.Row="0" x:Name="player" />
<Button x:Name="btn" Height="50" Width="100" Content="播放2" Grid.Row="1">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<EventTrigger.Actions>
<BeginStoryboard Storyboard="{StaticResource p}" />
</EventTrigger.Actions>
</EventTrigger>
</Button.Triggers>
</Button>
</Grid>
MediaElement
是一个看不见的元素.
注意, 如果 Storyboard
的 AutoReverse
为 true
, 则无法播放媒体文件. 可以将媒体播放单独剥离出来处理, 让其他元素可以 Reverse 即可.
然后作者演示了一个弹球的案例, 利用 AutoReverse
来使得弹球与影子反复变化.
在案例中引入了 ParallelTimeline
组件, 允许在 Storyboard
中同时运行不同的时间线.
不使用 Storyboard 来实现动画
动画都是通过 XAML 来定义的, 如果是后台 C# 代码, 基本逻辑与标签结构一直, 需要手动创建对应的对象, 来执行代码.
很显然不建议这么实现, 太繁琐.
简单动画
即使简单的动画也需要繁琐的代码, 所以推荐使用工具来创建动画.
小结
略