【Win 10 应用开发】UI Composition 札记(六):动画
动画在 XAML 中也有,而且基本上与 WPF 中的用法一样。不过,在 UWP 中,动画还有一种表现方式—— 通过 UI Composition 来创建。
基于 UI Composition 的动画,相对于 XAML 动画,有以下优点:
1、不使用 UI 线程,XAML 动画是共享 UI 线程的,而 Composition 中的动画是使用辅助线程的。
2、Composition 动画支持表达式(计算公式)来产生动画,相对灵活。
老周的建议是:两者都用,因为基于 XAML 和基于 Composition 的动画各有特点,在应用程序中都可以混合来用。我们不要被一些不健康的思想所毒害,世界上没有什么技术可以取代和不取代,只要用得上,哪怕是 1000 年前的技术也同样适用(事实也表明有些东西我们现在科技这么发达竟然做不到,可咱们祖先在 N 千万年前反而能做到)。所以,我们应该向庄子先生学习,思维要灵活,合理应用一切可用的资源。
对于动画,不管是啥类型的,其实基本要素都一样,首先,动画是基于时间变化而产生的“眼球欺骗”技术,只是一个个帧随着时间变化不断改变,利用人眼的视觉延时误差,让我们觉得目标好像在动。其实,人看着在动,但是猫的眼睛看就不见得是这样了。故,动画会有时间线,可以说是动画的时长。
其次是值,比如,你要让绿色变成红色,那么在特定的时间点上,你就应该给一个颜色值;再比如,一只猪从屏幕左边滑到右边,那么在对应的时间上,你要给出一个坐标值,表明这头猪滑行了多长距离。
然后就是动画的作用目标,就是你要把动画应用到哪个对象的哪个属性上,要是想改变不透明度,就会选择应用到 K 对象的 Opacity 属性上。
在 Composition API 中,Visual 类的属性都支持动画,如 Offset,Size 等属性。
下面我们先介绍一种最经典的动画类型——关键帧。
所谓关键帧动画,就是在时间线上添加 N 个(N 肯定是有效数字)时间点,这些时间点会与一个目标值对应,当动画播放到这个关键帧时,会改变目标值。而关键帧之间的部分,就交给某些算法去计算过度动画。
举个例子,用关键帧动画改变某对象的 Opacity 属性(不透明度),时间线总长为 10 秒,在第 0 秒时设定值为 0,即全透明,然后,在第 5 秒时设定值为 0.5,即半透明,最后在第 10 秒处将值设定为 1,表示完全不透明。
下面咱们玩一个例子。
XAML 代码如下。
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="auto"/> </Grid.RowDefinitions> <Canvas> <strong> <Image Name="img" Height="200" Source="Assets/1.png"/> </strong> </Canvas> <StackPanel Grid.Row="1" Margin="8" Orientation="Horizontal" HorizontalAlignment="Center"> <strong> <Button Content="开始" Click="OnStart"/> <Button Content="停止" Margin="20,0,0,0" Click="OnStop"/> </strong> </StackPanel> </Grid>
由于老周比较穷,所以界面放的东西不多。Image 控件用来显示多拉 B 梦的照片,然后,对,下面两个按钮,一个用来启动播放动画,另一个用来停止动画。
Image 为啥要放到 Canvas 容器中呢,因为这个容器,你懂的,它是绝对定位。如果是一个 Grid,可能会受到对齐方式的影响,这样后面我们要对这个对象的位置进行修改时就很不好弄。
切换到代码视图,在页面类中声明两个变量。
Vector3KeyFrameAnimation Animation = null; Visual imageVs = null;
之所以在类级别声明它们,因为稍后要用。这里,Vector3KeyFrameAnimation 表示关键帧动画是针对 Vector3 这种值进行处理的,待会我们要让 Image 控件中的 多拉 B 梦 移动。通过老周前面的介绍,大伙应该记得,Offset 属性表示对象的位置,它有三个值:X、Y、Z,所以,我们要用 Vector3 而不是 Vector2,Vector2 只有两个值,适用于 Size 属性。
如果你要对颜色做动画处理,那就用ColorKeyFrameAnimation,道理一样,它使用的值就是 Color 结构类型。如果你进行动画处理的目标属性只有一个值,比如 Opacity ,只是一个 float 值,那么,你就可以选用ScalarKeyFrameAnimation。
在页面的构造函数中,我们初始化一下各个对象。
public MainPage() { this.InitializeComponent(); <strong>// 获取可视化对象 imageVs = ElementCompositionPreview.GetElementVisual(img); var compos = imageVs.Compositor; // 创建关键帧动画 Animation = compos.CreateVector3KeyFrameAnimation(); // 时长为 4 秒 Animation.Duration = TimeSpan.FromSeconds(4d); // 插入关键帧 Animation.InsertKeyFrame(0f, new Vector3(0f, 0f, 0f)); Animation.InsertKeyFrame(0.5f, new Vector3(500f, 360f, 30f)); Animation.InsertKeyFrame(0.7f, new Vector3(260f, 125f, 45f)); Animation.InsertKeyFrame(1f, new</strong><strong> Vector3(20f, 20f, 60f)); </strong> }
老周在前面的博文中说过,Composition 要用到的各种资源,都可以通过Compositor 实例的 CreateXXX 方法来创建,动画也是如此。关键帧动画一定要记得添加关键帧,InsertKeyFrame 方法的第一个参数是关键帧在时间线上的位置,注意,它采用的是相对值(百分比),从 0.0 到 1.0,如果是 1 则表示关键帧在时间线 100% 处,如果是 0.5,关键帧正好位于时间线中央。
插入关键帧时要记得,它是用百分比来计算的。另外,不要忘了设置一下Duration 属性,就是动画时间线的长度。
接下来,处理一下那两个按钮的 Click 事件,分别启动和停止动画。
private void OnStart(object sender, RoutedEventArgs e) { imageVs?.StartAnimation(nameof(Visual.Offset), Animation); } private void OnStop(object sender, RoutedEventArgs e) { imageVs?.StopAnimation(nameof(Visual.Offset)); }
要让动画对象与目标属性关联,可以调用可视化对象的StartAnimation 方法,第一个参数要指定要应用到的属性名字,本示例是应用到Offset 属性上。要停止正在播放的动画,只需要把属性名传给StopAnimation 方法即可。
一起来看看效果,多拉B梦在家里经常这样锻炼身体的。
由于 gif 动画的帧率问题,所以你看到截图上的动画是不流畅的,想实际体验就自己动手吧。
下面,老周再给大伙伴们演示一个基于颜色值的动画。
XAML 代码很简单,就放一个 Canvas 就行了。
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Canvas Name="cvs"/> </Grid>
然后转到页面代码,初始化一下动画。
public MainPage() { this.InitializeComponent(); Visual cvsv = ElementCompositionPreview.GetElementVisual(cvs); Compositor compos = cvsv.Compositor; <strong>// 创建颜色关键帧动画 ColorKeyFrameAnimation animat = compos.CreateColorKeyFrameAnimation(); // 时间长度 animat.Duration = TimeSpan.FromSeconds(6d); // 让它永远循环播放 animat.IterationBehavior = AnimationIterationBehavior.Forever; // 插入关键帧 animat.InsertKeyFrame(0f, Colors.Red); animat.InsertKeyFrame(0.6f, Colors.Blue); animat.InsertKeyFrame(1f, Colors.Yellow); // 颜色变化模式 animat.InterpolationColorSpace = CompositionColorSpace.Rgb; </strong>// 创建颜色画刷 CompositionColorBrush brush = compos.CreateColorBrush(Colors.Black); // 创建可视化对象 SpriteVisual sv = compos.CreateSpriteVisual(); // 设置大小和位置 sv.Size = new Vector2(360f, 250f); sv.Offset = new Vector3(150f, 140f, 0f); // 关联画刷 sv.Brush = brush; // 把可视化对象插入 XAML 可视化树 ElementCompositionPreview.SetElementChildVisual(cvs, sv); // 启动动画 <strong> brush.StartAnimation(nameof(CompositionColorBrush.Color), animat); </strong> }
代码比较长,但有些我前面文章中已经介绍过,我们重点看这段。
// 创建颜色关键帧动画 ColorKeyFrameAnimation animat = compos.CreateColorKeyFrameAnimation(); // 时间长度 animat.Duration = TimeSpan.FromSeconds(6d); // 让它永远循环播放 animat.IterationBehavior = AnimationIterationBehavior.Forever; // 插入关键帧 animat.InsertKeyFrame(0f, Colors.Red); animat.InsertKeyFrame(0.6f, Colors.Blue); animat.InsertKeyFrame(1f, Colors.Yellow); // 颜色变化模式 animat.InterpolationColorSpace = CompositionColorSpace.Rgb;
首先,当然要创建基于颜色的关键帧动画对象,然后设置一下参数,插入关键帧相信你都会了,跟前面那个多拉B梦移动的例子差不多,只是值的类型变成 Color 值而已。
IterationBehavior 属性用来设置动画的循环次数,如果你设置为Count,那么,就要为动画的IterationCount 属性指定一个数值,比如3表示播放三次。这里我设置为Forever,表示动画永久循环播放。
InterpolationColorSpace 属性是个很好玩的东西,主要设置颜色在进行动画过程如何过度。它用CompositionColorSpace 枚举来规范几个值。经过测试发现,貌似使用 RGB 形式动画比较正常, RgbLinear 会发生错误,但 Rgb 是正常的,所以我就选用 Rgb 模式了。
最后在启动动画时要注意,动画的作用是改变颜色,所以它的应用对象应该是画刷CompositionColorBrush 的 Color 属性,所以,调用StartAnimation 方法应该在画刷对象上,而不是SpriteVisual 对象。
来,看看效果吧。
接下来,我们看一下跳跃式动画。所谓跳跃式动画,就是它可以模仿弹簧的物理特性,在动画停止之前有一个回弹的动作。这个动画用在控件特效很不错。
下面我们来个弹球球的实验。
首先,我们在 XAML 中放一个蓝色的球。
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Canvas> <Ellipse Name="ell" Width="100" Height="100" Fill="Blue" Canvas.Top="40" Canvas.Left="20"/> </Canvas> </Grid>
随后,转到代码视图,输入以下代码。
public MainPage() { this.InitializeComponent(); Visual ellVisual = ElementCompositionPreview.GetElementVisual(ell); var compositor = ellVisual.Compositor; <strong>var springAnmt = compositor.CreateSpringScalarAnimation(); springAnmt.InitialValue = 0f; springAnmt.FinalValue = 400f; springAnmt.Period = TimeSpan.FromMilliseconds(60d); springAnmt.DampingRatio = 0.2f; springAnmt.StopBehavior =</strong><strong> AnimationStopBehavior.SetToInitialValue; </strong> Windows.System.Threading.ThreadPoolTimer.CreateTimer(timer => { ellVisual?.StartAnimation("Offset.X", springAnmt); }, TimeSpan.FromSeconds(3d)); }
InitialValue 和FinalValue 属性分别用于指定动画的初始值和最终值。如果不指定初始值,那就默认使用当前的值作为初始值。这里有两个属性我们要重点关注的。第一个是DampingRatio ,它是一个大于 0 的值,它表示对象在完成动画时振动的衰减程度,就像一个球,它落到地面上会弹起来,可是,它不可能永远都在那里弹,可能弹几下它就落地不动了。弹性势能会不断地衰减。
如果你把DampingRatio 属性设置为 0 ,那么,物体就会不停地在弹,而且振幅很大,这是不符合现实物理现象的,因此,这个值你不能用0,一般是用大于0小于1之间,如果大于/等于1,物体几乎不会振动,非但不振动,反而速度会逐渐变慢。所以,这个DampingRatio 属性值,当值小于 1 时,就像在弹簧上弹起来,而当其大于或等于 1 时,就等同于用手按弹簧,越往下按,阻力越大。
还有一个属性,是配合DampingRatio 使用的,它就是Period,它表示每一轮振动的时间,时间越短,物体振动就越快。
本例的设置如下。
springAnmt.Period = TimeSpan.FromMilliseconds(60d); springAnmt.DampingRatio = 0.2f;
表示振动周期为 60 毫秒,振动衰减系数为 0.2,这个值振感明显,但不会振个不停。
看看效果吧。
其他的跳跃式动画的用法也一样,本例所针对的值是可视化对象的Offset 属性的 X 值,所以是单个 float 值,因此使用SpringScalarNaturalMotionAnimation。如果处理动画的目标是其他复杂的值,可以用SpringVector2NaturalMotionAnimation 或SpringVector3NaturalMotionAnimation,用法都是一样的,我就不废话了,有兴趣的伙伴可以试试。
本文最后,我们看一下隐式动画。啥叫隐式动画?就是你不必调用StartAnimation 方法来启动动画,当一些支持动画的属性更改时,会自动产生动画。比如,Visual 类的属性基本支持动画,像Opacity、Offset、Orientation、Size 等。
Composition 对象都从CompositionObject 类上继承了一个叫ImplicitAnimations属性,它是一个集合,我们可以将多个动画对象加进去,然后,当指定的对象属性更改时,会自动产生动画。
ImplicitAnimationCollection 集合是以字典数据形式来存储的,Key 是要进行动画处理的属性名,Value 是对应的动画实例。这里你可以用关键帧动画,或者上面讲到过的跳跃式动画都可以。为了最大限度保证动画的兼容性,隐式动画会存在一定的自动转换功能。比如,一个针对 Vector3 的动画可以用于 Vector2 值的属性,它会从X,Y,Z中取两个值来填充 Vector2 值。
下面,我们还是用示例来说明吧。我们在 XAML 中放一个物体。
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="auto"/> </Grid.RowDefinitions> <Canvas> <Ellipse Name="ell" Fill="Green" Width="150" Height="150"/> </Canvas> <StackPanel Grid.Row="1" Margin="2,12" Orientation="Horizontal" HorizontalAlignment="Center"> <Button Content="动作 1" Margin="0,0,24,0" Click="OnClick1"/> <Button Content="动作 2" Margin="0,0,24,0" Click="OnClick2"/> <Button Content="动作 3" Margin="0,0,24,0" Click="OnClick3"/> <Button Content="动作 4" Click="OnClick4" /> </StackPanel> </Grid>
下面的四个按钮的作用是修改上面那个圆的 Offset,Opacity 属性,说白了,就是修改它的位置和不透明度。
现在,我们转到代码视图,先在类级别声明一个 Visual 类型的变量,它表示上面的 Ellipse 对象的可视化对象引用,应用我们在四个按钮的 Click 事件处理代码中要访问它,所以把其作为类级别的字段。
Visual ell_vs;
然后,在页面类的构造函数中初始化。
public MainPage() { this.InitializeComponent(); // 设置动画 ell_vs = ElementCompositionPreview.GetElementVisual(ell); Compositor compos = ell_vs.Compositor; ImplicitAnimationCollection implicitAnmts = compos.CreateImplicitAnimationCollection(); ScalarKeyFrameAnimation opacityAnmt = compos.CreateScalarKeyFrameAnimation(); <strong> opacityAnmt.InsertExpressionKeyFrame(0f, </strong><strong>"this.StartingValue"); opacityAnmt.InsertExpressionKeyFrame(1f, "this.FinalValue"</strong><strong>); </strong> opacityAnmt.Duration = TimeSpan.FromSeconds(1d); <strong>opacityAnmt.Target </strong><strong>=</strong><strong> nameof(Visual.Opacity); </strong> Vector3KeyFrameAnimation offsetAnmt = compos.CreateVector3KeyFrameAnimation(); <strong> offsetAnmt.InsertExpressionKeyFrame(0f, </strong><strong>"this.StartingValue"); offsetAnmt.InsertExpressionKeyFrame(1f, "this.FinalValue"</strong><strong>); </strong> offsetAnmt.Duration = TimeSpan.FromSeconds(1d); <strong>offsetAnmt.Target </strong><strong>=</strong><strong> nameof(Visual.Offset); </strong> <strong>implicitAnmts.Add(nameof(Visual.Offset), offsetAnmt); implicitAnmts[nameof(Visual.Opacity)] </strong><strong>=</strong><strong> opacityAnmt; </strong> ell_vs.ImplicitAnimations = implicitAnmts; }
请注意,在为动画插入关键帧时,使用的是表达式的方法,因为我们后面是对对象的不透明度和位置进行动态调整,所以,这里的代码并不能准确知道动画的最终值是什么,所以,使用了这两个关键字:
this.StartingValue:表示动画的初始值,它会根据实际情况自动填充值。
this.FinalValue:指的是动画的最终值,它会自动填充。
在这个例子中,StartingValue 就是对象上一次被修改后的值,比如,第一次把 Opacity 改为 0.5,那么下一轮动画时的初始就是这个 0.5。FinalValue就是属性的最新值,比如Opacity 原来是 1,现在你改为 0.6,那么对本次动画来说,StartingValue 就是 1,FinalValue 就是 0.6 了。
对了,还有一点,你得为动画的 Target 属性赋值,比如动画是作用于 Opacity 属性上的,就赋 Opacity 。这个隐式动画比较特殊,一定要这样赋值。
然后,我们给四个按钮弄弄 Click 事件。
private void OnClick1(object sender, RoutedEventArgs e) { ell_vs.Opacity = 0.2f; ell_vs.Offset = new Vector3(300f, 250f, -30f); } private void OnClick2(object sender, RoutedEventArgs e) { ell_vs.Opacity = 0.8f; ell_vs.Offset = new Vector3(400f, 320f, 130f); } private void OnClick3(object sender, RoutedEventArgs e) { ell_vs.Opacity = 1f; ell_vs.Offset = new Vector3(150f, 60f, -70f); } private void OnClick4(object sender, RoutedEventArgs e) { ell_vs.Offset = new Vector3(20f, 200f, 50f); ell_vs.Opacity = 0.5f; }
好,现在可以看效果了。运行应用,分别点四个按钮,看看它们这样修改对象的属性会不会更生动。
好了,本文就讲到这里吧。
你一定会记得,还有一个表达式动画,那个咱们留到下一篇文章再聊,本篇就先聊到这里。示例的代码我都基本贴上了,所以我就不上传示例了。