【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>
            <Image Name="img" Height="200" Source="Assets/1.png"/>
        </Canvas>
        <StackPanel Grid.Row="1" Margin="8" Orientation="Horizontal" HorizontalAlignment="Center">
            <Button Content="开始" Click="OnStart"/>
            <Button Content="停止" Margin="20,0,0,0" Click="OnStop"/>
        </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();

            // 获取可视化对象
            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 Vector3(20f, 20f, 60f));
        }
复制代码

 

老周在前面的博文中说过,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;
            // 创建颜色关键帧动画
            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;
            // 创建颜色画刷
            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);
            // 启动动画
            brush.StartAnimation(nameof(CompositionColorBrush.Color), animat);
        }
复制代码

代码比较长,但有些我前面文章中已经介绍过,我们重点看这段。

复制代码
            // 创建颜色关键帧动画
            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;
            var springAnmt = compositor.CreateSpringScalarAnimation();
            springAnmt.InitialValue = 0f;
            springAnmt.FinalValue = 400f;
            springAnmt.Period = TimeSpan.FromMilliseconds(60d);
            springAnmt.DampingRatio = 0.2f;
            springAnmt.StopBehavior = AnimationStopBehavior.SetToInitialValue;

            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();
            opacityAnmt.InsertExpressionKeyFrame(0f, "this.StartingValue");
            opacityAnmt.InsertExpressionKeyFrame(1f, "this.FinalValue");
            opacityAnmt.Duration = TimeSpan.FromSeconds(1d);
            opacityAnmt.Target = nameof(Visual.Opacity);

            Vector3KeyFrameAnimation offsetAnmt = compos.CreateVector3KeyFrameAnimation();
            offsetAnmt.InsertExpressionKeyFrame(0f, "this.StartingValue");
            offsetAnmt.InsertExpressionKeyFrame(1f, "this.FinalValue");
            offsetAnmt.Duration = TimeSpan.FromSeconds(1d);
            offsetAnmt.Target = nameof(Visual.Offset);

            implicitAnmts.Add(nameof(Visual.Offset), offsetAnmt);
            implicitAnmts[nameof(Visual.Opacity)] = opacityAnmt;

            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;
        }
复制代码

 

好,现在可以看效果了。运行应用,分别点四个按钮,看看它们这样修改对象的属性会不会更生动。

 

 

好了,本文就讲到这里吧。

你一定会记得,还有一个表达式动画,那个咱们留到下一篇文章再聊,本篇就先聊到这里。示例的代码我都基本贴上了,所以我就不上传示例了。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @   东邪独孤  阅读(1117)  评论(7编辑  收藏  举报
编辑推荐:
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示