[聚合文章] 【Win 10 应用开发】UI Composition 札记(六):动画

.Net 2017-11-19 1 阅读

动画在 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;
        }

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

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

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

注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。