本文译自 Nick Waggoner 的 "Understand what’s possible with the Windows UI Animation Engine",已获原作者授权进行翻译。更多有关 Windows UI、UWP 开发的文章,欢迎访问我的博客源站:http://validvoid.net/


2015 年 11 月,视觉层 (Visual Layer)作为 Windows.UI.Composition 命名空间中的一系列新 API 被引入。这些新 API 标志着开发者首次有机会接触那些支撑着自 Windows 8 以来各种 UI 框架(例如 IE/Edge、XAML 和 Windows Shell)的功能特性。全新视觉层的一个重要方面就是其动画引擎。但今年在 //build/ 大会上为开发者进行大量讲谈之后,我发现开发者们任然不太清楚动画系统的各部分是如何协同工作的。为了帮助你理解动画系统的潜力,我们通过两个问题进行理解:

  • 谁负责开始动画?
  • 什么驱动动画,改变取值?

隐式 vs. 显式 – 谁负责开始动画?

显式动画和隐式动画之间的关键区别就在于谁负责触发动画。

长话短说:显式动画你触发;隐式动画你配置。

显式动画

提到动画,大部分人想到的都是显式动画,对此你应该很熟悉了。对于显式动画,你进行设置之后,也由作为开发者你的自己进行触发。

例如,在 XAML 中通常用标记语言创建动画,在后台代码中触发动画。

标记语言代码:

<Storyboard x:Name="myStoryboard">
<DoubleAnimation From="1" To="6" Duration="00:00:6"
Storyboard.TargetName="rectScaleTransform"
Storyboard.TargetProperty="ScaleY">
<DoubleAnimation.EasingFunction>
<BounceEase Bounces="2" EasingMode="EaseOut"
Bounciness="2" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>

后台代码:

private void OnButtonClick(object sender, RoutedEventArgs e)
{
myStoryboard.Begin();
}

视觉层中的动画系统同样也支持显式动画——尽管只在你的后台代码中。这使你能取用已知动画配置并直接使用。

后台代码:

// 获取表示此 UIElement 的 Visual (视觉元素对象)并从中获取 compositor (合成器对象)
Visual tempVisual = ElementCompositionPreview.GetElementVisual(this);
Compositor compositor = tempVisual.Compositor; // 创建一个简单的 ScalarKeyFrameAnimation (标量关键帧动画)
ScalarKeyFrameAnimation scalarAnimation = compositor.CreateScalarKeyFrameAnimation();
scalarAnimation.Duration = TimeSpan.FromMilliseconds();
scalarAnimation.InsertKeyFrame(1f, 200f); // 显式开始动画
tempVisual.StartAnimation("Offset.X", scalarAnimation);

以上例子的模式都是相同的。你先定义动画(也就是动画的时长、运动轨迹、目标属性以及取值),然后通过 start/begin 方法显式触发动画。

隐式动画

相对于显式动画,隐式动画则是由平台触发的。例如,下列代码演示了如何在 XAML 中为一个按钮附加 EntranceThemeTransition

<Button Content="EntranceThemeTransition Button">
<Button.Transitions>
<TransitionCollection>
<EntranceThemeTransition />
</TransitionCollection>
</Button.Transitions>
</Button>

这就是实现效果所需的全部代码。当按钮初次呈现时,它会触发 EntranceThemeTransition,使其以动画形式运动到目标位置。在视觉层出现之前,你只有屈指可数的几个隐式动画可供选择,也就是 XAML 过渡效果动画 (the XAML Transitions),并且几乎无法对其进行配置。而视觉层不仅支持隐式动画,还给了你更大的定制空间:

// 创建一个映射表用于储存触发器/动画配对。
ImplicitAnimationMap implicitAnimationCollection = _compositor.CreateImplicitAnimationMap(); // 创建实际要运行的动画。
var _offsetKeyFrameAnimation = _compositor.CreateVector3KeyFrameAnimation();
_offsetKeyFrameAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
_offsetKeyFrameAnimation.Duration = TimeSpan.FromSeconds(); // 设置当 Offest(触发器) 改变时要运行的动画。
implicitAnimationCollection["Offset"] = _offsetKeyFrameAnimation; // 应用隐式动画。
myVisual.ImplicitAnimations = implicitAnimationCollection;

根据这段代码,无论何时只要 myVisual 的 Offset 发生改变,_offsetKeyFrameAnimation 都会被平台触发。注意在这一隐式动画的定义中用到了一个 ExpressionKeyFrame,也就是表达式关键帧。表达式关键帧允许你设置数学表达式,动画系统在播放表达式动画 (ExpressionAnimations)的每一帧时都会计算此表达式的值。在我们的例子里,我们使用了一个简单的表达式 this.FinalValue,只是对触发动画的条件进行取值。这一动画只是一个非常基本的示例,但通过表达式你能定义任何你想要的动画。

视觉层隐式动画的灵活性使得你能够将应用的逻辑与动效分离,并提供了一种强大的方式定制你的体验。例如,隐式动画一种不错的用法就是对 "Offset" 设置触发器,这样你就能创建从一种布局向另一种布局动画过渡的效果,并且该效果是由 XAML 的布局引擎自动触发的。

想要深入了解隐式动画,//build/ 大会上的这一讲谈节目是个不错的起点。

专业利器 – 什么驱动动画?

时间驱动动画

时间驱动动画是开发者们熟知并且喜爱的经典动画类型。上文中的代码片段展示了 XAML 的 storyboard 动画以及 composition 的关键帧动画,它们都是时间驱动型动画。关键帧动画背后的思想(实际上是标准)就是你为特定时间的动画都指定目标取值,并描述这些取值之间如何过渡(通常成为插值或缓动函数)。XAML 提供了一大批内建的缓动函数帮助你轻松实现美观的动效。而在视觉层中,负责提供缓动动效的则是CubicBezierEasingFunction 类(意为三次贝塞尔缓动函数)。CubicBezierEasingFunction 通过两个控制点控制运动轨迹。控制点允许你以细粒度方式控制插值。而且鉴于各类动画引擎中广泛使用贝塞尔曲线描述缓动,你能轻松获得很多效果不错的预定义控制点方案。我通常使用 Easings.net 获取标准平纳缓动函数1的控制点。

引用驱动动画(数学驱动)

在 Windows 10 的 10586 十一月更新中,ExpressionAnimation(表达式动画)被引入视觉层的动画引擎。表达式动画允许你在动画系统中创建属性之间在帧间更新的数学关系。视差动画就是一个经典的表达式动画:

// 创建驱动视差动画的表达式。
ExpressionAnimation parallaxAnimation = compositor.CreateExpressionAnimation("MyForeground.Offset.Y / MyParallaxRatio"); // 设置我们希望背景进行视差滚动的速度。
parallaxAnimation.SetScalarParameter("MyParallaxRatio", 0.5f); // 设置前景对象的引用。
parallaxAnimation.SetReferenceParameter("MyForeground", foregroundVisual); // 在背景对象上开始动画。
backgroundVisual.StartAnimation("Offset.Y", parallaxAnimation);

这段代码所做的第一件事是创建一个用于描述一些输入与动画结果输出之间关系的数学表达式。表达式中定义了几个参数和稍后赋值的引用。参数帮助你配置数学关系,但引用才是使表达式灵动的重点。一个参数(例如 MyParallaxRatio)是通过调用指定类型的函数(例如 SetScalarParameter)赋值的。此行为通知动画引擎对该参数的所有实例以你传入的取值进行求值。求值动作只在将动画交由引擎处理前发生一次,因此这是一个指定常量取值的好办法。相反,一个引用(例如 MyForeground) 则是动画引擎在每帧求值的。这正是实际使表达式动画灵动的魔法所在。

此外还有两点需要指出。首先,你会注意到我们能够访问 MyForeground 的成员以及 Y子通道。表达式的语法允许访问成员以及“混合”或交换一个矢量/矩阵的成分。例如:

// 重用 offset 的 X 通道创建一个 Vector2 动画。
ExpressionAnimation vector2Animation = compositor.CreateExpressionAnimation("MyForeground.Offset.X"); // 设置对前景对象的引用。
vector2Animation.SetReferenceParameter("MyForeground", foregroundVisual);

另一点需要指出的是,视觉层中的所有动画实际上都是模板。这意味着你可以对多个对象使用同一个动画或重用动画的结构,只在下一个对象的动画开始前更新参数和引用。例如,如果我们想要扩展基本视差动画,添加多层景深效果,我们可以只需要一个动画定义即可:

// 创建驱动视差滚动的表达式动画。
ExpressionAnimation parallaxAnimation = compositor.CreateExpressionAnimation("MyForeground.Offset.Y / MyParallaxRatio"); // 设置前景对象引用。
parallaxAnimation.SetReferenceParameter("MyForeground", foregroundVisual); // 设置背景对象视差滚动速度。
parallaxAnimation.SetScalarParameter("MyParallaxRatio", 0.5f); // 对背景对象开始动画。
backgroundVisual.StartAnimation("Offset.Y", parallaxAnimation); // 设置远距背景对象的视差滚动速度。
parallaxAnimation.SetScalarParameter("MyParallaxRatio", 0.2f); // 对远距背景对象开始动画。
deepBackgroundVisual.StartAnimation("Offset.Y", parallaxAnimation);

表达式动画是一种全新而强大的动画方式,使我们能借以表示物体如何相对运动。表达式动画为我们免去了设置一系列复杂动画的痛苦,使多个不同对象和属性协同运动因而变得更加容易。要深入了解表达式动画,可参见 //build/ 讲谈:

P486: Using Expression Animations to Create Engaging & Custom UI

输入驱动动画

自大约五年前触摸渐成主流起,创造低延迟体验成为了一种普遍需求。使用手指或笔在屏幕上操作,使得人眼获得了更直观的参照点来辨识操作的延迟和流畅性。为使操作流畅,主流操作系统公司均将更多的操作移交至系统和 GPU (如 ChromeIE)执行。在 Windows 上,这由 DirectManipulation 这一或多或少是针对于触摸构建的动画引擎实现的。它解决了关键的延迟挑战,也就是如何自然地以展示从输入驱动到事件驱动过渡的动效。但另一方面,它也几乎没有提供对定制惯性观感的支持,就像福特 T 型车那样——“只要车是黑色的,你可以把它涂成任意你喜欢的颜色”。2

ElementCompositionPreview.GetScrollViewerManipulationPropertySet 是让你能够把玩输入驱动动效的第一步。虽然它仍然没给你任何对内容滚动时观感进行控制的额外能力,但它确实允许你对次级内容应用表达式动画。例如,我们终于能完成我们的基础视差滚动代码:

// 创建驱动视差滚动的表达式动画。
ExpressionAnimation parallaxAnimation = compositor.CreateExpressionAnimation("MyForeground.Offset.Y / MyParallaxRatio"); // 设置对前景对象的引用。
CompositionPropertySet MyPropertySet = ElementCompositionPreview.GetScrollViewerManipulationPropertySet(MyScrollViewer);
parallaxAnimation.SetReferenceParameter("MyForeground", MyPropertySet); // 设置背景对象视差滚动的速度。
parallaxAnimation.SetScalarParameter("MyParallaxRatio", 0.5f); // 对背景对象开始视差动画。
backgroundVisual.StartAnimation("Offset.Y", parallaxAnimation);

使用这一技巧,你能够实现多种优秀的效果:视差滚动、粘性表头、自定义滚动条等等。唯一缺失的就是定制操作本身的观感……

再来看看 InteractionTracker。这一全新设计的特性在给予你控制操作观感方方面面的灵活性的同时,保证了手指操作低延迟的体验。在 Windows 的 UI 平台上,我们时常谈到便利性与和可行性之间的权衡。常规 UX 和调用模式通常被包装成简单易用的高级控件和特性。这确实使得他们简单易用,但也在一定程度上损失了灵活操控性。而尺度的另一端则是如图形层(Graphics Layer) 的这类封装。它们使你能够完全控制每个像素在屏幕上的呈现,但也带来了更大的复杂性。在输入处理的设计中,InteractionTracker 更多地倾向于可行性这一侧。如今在 Windows UI 平台上,你首次能够描述性地将输入到输出映射为具体的动效。

这里我们以一个简单的示例,通过修改惯性结束的位置来演示这种全新的灵活性。过去,你通过指定四种 对齐点(Snap-points) 类型中的一种来修改 XAML 中 ScrollViewer 的惯性表现。现在,有了 InteractionTracker提供的更多种可能性,你可以使用表达式动画来定义惯性在哪结束。下面是一个例子,基于惯性自然停止的位置创建了三种不同的对齐点方案:

// 创建一个惯性端点,其条件与结束点在面板近侧。
// 变量在稍后公有变量更新后填入。
var snapNearConditionExpression = s_compositor.CreateExpressionAnimation("target.Position.X < - target.CompletionThreshold");
var snapNearValueExpression = s_compositor.CreateExpressionAnimation("-target.CompletedOffset"); var snapNearEndpoint = InteractionTrackerInertiaRestingValue.Create(s_compositor);
snapNearEndpoint.Condition = snapNearConditionExpression;
snapNearEndpoint.RestingValue = snapNearValueExpression; // 创建一个惯性端点,其条件与结束点在面板远侧。
// 变量在稍后公有变量更新后填入。
var snapFarConditionExpression = s_compositor.CreateExpressionAnimation("target.Position.X > target.CompletionThreshold");
var snapFarValueExpression = s_compositor.CreateExpressionAnimation("target.CompletedOffset"); var snapFarEndpoint = InteractionTrackerInertiaRestingValue.Create(s_compositor);
snapFarEndpoint.Condition = snapFarConditionExpression;
snapFarEndpoint.RestingValue = snapFarValueExpression; // 创建一个总惯性控制端点,用于控制如果没有其它惯性修改器生效则归为至静息状态。
var snapHomeEndpoint = InteractionTrackerInertiaRestingValue.Create(s_compositor);
snapHomeEndpoint.Condition = s_compositor.CreateExpressionAnimation("true");
snapHomeEndpoint.RestingValue = s_compositor.CreateExpressionAnimation(""); // 插入惯性端点表达式引用的属性。
s_interactionTracker.Properties.InsertScalar(nameof(CompletedOffset), (float)m_completedOffset);
s_interactionTracker.Properties.InsertScalar(nameof(CompletionThreshold), (float)m_completionThreshold);
s_interactionTracker.ConfigurePositionXInertiaModifiers(
new InteractionTrackerInertiaModifier[] { snapNearEndpoint, snapFarEndpoint, snapHomeEndpoint });

实际上你不仅能够如示例中一样修改惯性结束的位置,还能够修改惯性动效的轨迹。InteractionTracker 使你能够精准定制出体现标志性体验的观感。要了解更多有关 InteractionTracker 潜力与使用的内容,可参见:

P405: Adding Manipulations in the Visual Layer to Create Customized & Responsive Interactive Experiences

如何进一步深入?

如果你还未查看 WindowsUIDevLabs 代码仓库,你绝对应该马上去看看。该仓库的简介是这样的:

欢迎来到 Windows UI 开发实验室的代码仓库,本库包含了最新的示例代码、示例项目以及来自使用 Windows UI 开发各种精美 UWP 应用的开发者的反馈。

作为深入理解学习 Windows UI 的下一站,该代码仓库是获取深入理解平台与各种协助代码的好地方。


译者注:

  1. 平纳缓动函数(Penner’s Easing Functions):由 Robert Penner 定义的一组流行的缓动函数,被各种动效实现广泛使用。 

  2. 福特 T 型车是福特于1908年至1927年推出的一款价格低廉广受欢迎的汽车。福特在其自传第四卷中提到他曾对销售人员说“只要车是黑色的,顾客可以把它涂成任何自己喜欢的颜色。”由于黑色涂料廉价耐用,出于提高生产效率的考虑福特作出了只出产黑色车型的决定。但这一决定使得福特后续的份额被竞争对手蚕食。 


关于作者

本文原作者 Nick Waggoner 供职于微软 native Windows UI platform(@WindowsUI)。

原作者博客:http://www.nickwaggoner.com/

原作者 Twitter:@nrwaggs

本文已获原作者授权进行翻译。我后续会持续翻译 Nick Waggoner 在个人博客或其它位置发表的有关 UWP、 Windows UI 的文章。

[译]理解 Windows UI 动画引擎的更多相关文章

  1. [译]理解Windows消息循环

    出处:http://www.cnblogs.com/zxjay/archive/2009/06/27/1512372.html 理解消息循环和整个消息传送机制对Windows编程来说非常重要.如果对消 ...

  2. 《深入理解Windows Phone 8.1 UI控件编程》基于最新的Runtime框架

    <深入理解Windows Phone 8.1 UI控件编程>本书基于最新的Windows Phone 8.1 Runtime SDK编写,全面深入地论述了最酷的UI编程技术:实现复杂炫酷的 ...

  3. [WP8.1UI控件编程]Windows Phone动画方案的选择

    8.1 动画方案的选择 Windows Phone的动画实现方式有线性插值动画(3种类型).关键祯动画(4种类型)和基于帧动画,甚至还有定时器动画,然后动画所改变的UI元素属性可以是普通的UI元素属性 ...

  4. COCOS2D-X中UI动画导致闪退与UI动画浅析

    前两天和同事一起查一个游戏的闪退问题,log日志显示最后挂在CCNode* ActionNode::getActionNode()函数中的首行CCNode* cNode = dynamic_cast& ...

  5. SpriteSheet精灵动画引擎

    SpriteSheet精灵动画引擎   本文介绍Flash中SpriteSheet精灵序列图与其它渲染方式的性能对比.SpriteSheet的原理及注意实现,最后实现了一个精灵序列图的渲染引擎.本文的 ...

  6. 深入理解windows

    阿猫翻译的,用作备忘 深入理解windows——session.window stations.desktops 翻译自:http://www.brianbondy.com/blog/id/100/ ...

  7. 深入剖析Windows专业版安装Docker引擎和Windows家庭版Docker引擎安装的区别

    原创声明:作者:Arnold.zhao  博客园地址:https://www.cnblogs.com/zh94 公司使用的电脑是Windows专业版,所以配置本机的Docker时会方便许多,后续由于需 ...

  8. Python结合Pywinauto 进行 Windows UI 自动化

    转:Python结合Pywinauto 进行 Windows UI 自动化 https://blog.csdn.net/z_johnny/article/details/52778064 说明:Pyw ...

  9. 春风十里不如你,全新Windows UI 3(WinUI 3) 的第一个实现Project Reunion 0.5

    什么是WinUI Windows UI库 (WinUI) 是适用于 Windows 桌面应用程序和 UWP 应用程序的本机用户体验 (UX) 框架. WinUI is a user interface ...

随机推荐

  1. python merge、concat合并数据集

    数据规整化:合并.清理.过滤 pandas和python标准库提供了一整套高级.灵活的.高效的核心函数和算法将数据规整化为你想要的形式! 本篇博客主要介绍: 合并数据集:.merge()..conca ...

  2. Weekly Contest 118

    970. Powerful Integers Given two non-negative integers x and y, an integer is powerful if it is equa ...

  3. B. Spreadsheets(进制转换,数学)

    B. Spreadsheets time limit per test 10 seconds memory limit per test 64 megabytes input standard inp ...

  4. 洛谷P4526 【模板】自适应辛普森法2(Simpson法)

    题面 传送门 题解 据说这函数在\(x>15\)的时候趋近于\(0\) 据说当且仅当\(a<0\)时积分发散 所以直接套自适应\(simpson\)吧-- //minamoto #incl ...

  5. 洛谷P2764 最小路径覆盖问题(最大流)

    传送门 先说做法:把原图拆成一个二分图,每一个点被拆成$A_i,B_i$,若原图中存在边$(u,v)$,则连边$(A_u,B_v)$,然后$S$对所有$A$连边,所有$B$对$T$连边,然后跑一个最大 ...

  6. root@localhost

    root代表当前的用户 也就是说你使用root的帐号登录的localhost是系统的名字 没有设置系统名字的时候默认名称是localhost/ 代表你当前所处的目录位置 你当前在根目录下# 是用户提示 ...

  7. linux系统安全及应用——账号安全(基本安全措施)

    不开启桌面可以减少受攻击面 一.系统账号清理 1)非登录用户的shell改为/sbin/nologin ~] #usermod -s /sbin/nologin user1 2)锁定长期不用的账号 锁 ...

  8. selenium定位元素提示‘元素不可见’问题解决方法

    最近在使用selenium的过程中发现有元素能够在页面中查找到,但是pycharm中运行时始终报错element not visible,于是使用如下方法成功解决问题. 1.driver.find_e ...

  9. java10:基于时间的版本控制

    功能发布 从Java 10开始,采用了一种新的严格的基于时间的发布模式. 在这个新模型中,Java平台的主要版本(现称为功能版本)将每6个月(3月和9月)发布一次. 功能版本将包含语言功能,JVM功能 ...

  10. 【离散数学】 SDUT OJ 1.1联结词真值运算

    1.1联结词真值运算 Time Limit: 1000 ms Memory Limit: 65536 KiB Submit Statistic Problem Description 已知命题变元p和 ...