理解 Windows UI 动画引擎

LarMehaffey 8年前
   <p>2015 年 11 月, <a href="/misc/goto?guid=4959670086958669253" rel="nofollow,noindex">视觉层 (Visual Layer)</a> 作为 Windows.UI.Composition 命名空间中的 <a href="/misc/goto?guid=4959670087048123984" rel="nofollow,noindex">一系列新 API</a> 被引入。这些新 API 标志着开发者首次有机会接触那些支撑着自 Windows 8 以来各种 UI 框架(例如 IE/Edge、XAML 和 Windows Shell)的功能特性。全新视觉层的一个重要方面就是其动画引擎。 但今年在 <a href="/misc/goto?guid=4958978123162195901" rel="nofollow,noindex">//build/</a> 大会上为开发者进行大量讲谈之后,我发现开发者们任然不太清楚动画系统的各部分是如何协同工作的。 为了帮助你理解动画系统的潜力,我们通过两个问题进行理解:</p>    <ul>     <li>谁负责开始动画?</li>     <li>什么驱动动画,改变取值?</li>    </ul>    <h2>隐式 vs. 显式 – 谁负责开始动画?</h2>    <p>显式动画和隐式动画之间的关键区别就在于谁负责触发动画。</p>    <p>长话短说:显式动画你触发;隐式动画你配置。</p>    <h3>显式动画</h3>    <p>提到动画,大部分人想到的都是显式动画,对此你应该很熟悉了。对于显式动画,你进行设置之后,也由作为开发者你的自己进行触发。</p>    <p>例如,在 XAML 中通常用标记语言创建动画,在后台代码中触发动画。</p>    <p>标记语言代码:</p>    <pre>  <code class="language-xml"><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>    </code></pre>    <p>后台代码:</p>    <pre>  <code class="language-cs">private void OnButtonClick(object sender, RoutedEventArgs e)    {      myStoryboard.Begin();  }  </code></pre>    <p>视觉层中的动画系统同样也支持显式动画——尽管只在你的后台代码中。这使你能取用已知动画配置并直接使用。</p>    <p>后台代码:</p>    <pre>  <code class="language-cs">// 获取表示此 UIElement 的 Visual (视觉元素对象)并从中获取 compositor (合成器对象)  Visual tempVisual = ElementCompositionPreview.GetElementVisual(this);    Compositor compositor = tempVisual.Compositor;    // 创建一个简单的 ScalarKeyFrameAnimation (标量关键帧动画)  ScalarKeyFrameAnimation scalarAnimation = compositor.CreateScalarKeyFrameAnimation();    scalarAnimation.Duration = TimeSpan.FromMilliseconds(300);    scalarAnimation.InsertKeyFrame(1f, 200f);    // 显式开始动画  tempVisual.StartAnimation("Offset.X", scalarAnimation);    </code></pre>    <p>以上例子的模式都是相同的。你先定义动画(也就是动画的时长、运动轨迹、目标属性以及取值),然后通过 start/begin 方法显式触发动画。</p>    <h3>隐式动画</h3>    <p>相对于显式动画,隐式动画则是由平台触发的。例如,下列代码演示了如何在 XAML 中为一个按钮附加 EntranceThemeTransition :</p>    <pre>  <code class="language-cs"><Button Content="EntranceThemeTransition Button">        <Button.Transitions>          <TransitionCollection>              <EntranceThemeTransition />          </TransitionCollection>      </Button.Transitions>  </Button>    </code></pre>    <p>这就是实现效果所需的全部代码。当按钮初次呈现时,它会触发 EntranceThemeTransition ,使其以动画形式运动到目标位置。在视觉层出现之前,你只有屈指可数的几个隐式动画可供选择,也就是 <a href="/misc/goto?guid=4959670087282071423" rel="nofollow,noindex">XAML 过渡效果动画 (the XAML Transitions)</a> ,并且几乎无法对其进行配置。而视觉层不仅支持隐式动画,还给了你更大的定制空间:</p>    <pre>  <code class="language-cs">// 创建一个映射表用于储存触发器/动画配对。  ImplicitAnimationMap implicitAnimationCollection = _compositor.CreateImplicitAnimationMap();     // 创建实际要运行的动画。  var _offsetKeyFrameAnimation = _compositor.CreateVector3KeyFrameAnimation();    _offsetKeyFrameAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");    _offsetKeyFrameAnimation.Duration = TimeSpan.FromSeconds(3);     // 设置当 Offest(触发器) 改变时要运行的动画。  implicitAnimationCollection["Offset"] = _offsetKeyFrameAnimation;     // 应用隐式动画。  myVisual.ImplicitAnimations = implicitAnimationCollection;    </code></pre>    <p>根据这段代码,无论何时只要 myVisual 的 Offset 发生改变, _offsetKeyFrameAnimation 都会被平台触发。注意在这一隐式动画的定义中用到了一个 ExpressionKeyFrame ,也就是表达式关键帧。表达式关键帧允许你设置数学表达式,动画系统在播放 <a href="/misc/goto?guid=4959670087372274084" rel="nofollow,noindex">表达式动画 (ExpressionAnimations)</a> 的每一帧时都会计算此表达式的值。在我们的例子里,我们使用了一个简单的表达式 this.FinalValue ,只是对触发动画的条件进行取值。这一动画只是一个非常基本的示例,但通过表达式你能定义任何你想要的动画。</p>    <p>视觉层隐式动画的灵活性使得你能够将应用的逻辑与动效分离,并提供了一种强大的方式定制你的体验。例如,隐式动画一种不错的用法就是对 "Offset" 设置触发器,这样你就能创建从一种布局向另一种布局动画过渡的效果,并且该效果是由 XAML 的布局引擎自动触发的。</p>    <p>想要深入了解隐式动画, <a href="/misc/goto?guid=4958978123162195901" rel="nofollow,noindex">//build/</a> 大会上的 <a href="/misc/goto?guid=4959670087456930284" rel="nofollow,noindex">这一讲谈节目</a> 是个不错的起点。</p>    <h2>专业利器 – 什么驱动动画?</h2>    <h3>时间驱动动画</h3>    <p>时间驱动动画是开发者们熟知并且喜爱的经典动画类型。上文中的代码片段展示了 XAML 的 storyboard 动画以及 composition 的关键帧动画,它们都是时间驱动型动画。关键帧动画背后的思想(实际上是标准)就是你为特定时间的动画都指定目标取值,并描述这些取值之间如何过渡(通常成为插值或缓动函数)。XAML 提供了一大批内建的缓动函数帮助你轻松实现美观的动效。而在视觉层中,负责提供缓动动效的则是 <a href="/misc/goto?guid=4959670087556626111" rel="nofollow,noindex">CubicBezierEasingFunction</a> 类(意为三次贝塞尔缓动函数)。 CubicBezierEasingFunction 通过两个控制点控制运动轨迹。控制点允许你以细粒度方式控制插值。而且鉴于各类动画引擎中广泛使用贝塞尔曲线描述缓动,你能轻松获得很多效果不错的预定义控制点方案。我通常使用 <a href="/misc/goto?guid=4958525660910632533" rel="nofollow,noindex">Easings.net</a> 获取 <a href="/misc/goto?guid=4959670087670909114" rel="nofollow,noindex">标准平纳缓动函数</a> 的控制点。</p>    <h3>引用驱动动画(数学驱动)</h3>    <p>在 Windows 10 的 10586 十一月更新中, <a href="/misc/goto?guid=4959670087372274084" rel="nofollow,noindex">ExpressionAnimation</a> (表达式动画)被引入视觉层的动画引擎。表达式动画允许你在动画系统中创建属性之间在帧间更新的数学关系。视差动画就是一个经典的表达式动画:</p>    <pre>  <code class="language-cs">// 创建驱动视差动画的表达式。  ExpressionAnimation parallaxAnimation = compositor.CreateExpressionAnimation("MyForeground.Offset.Y / MyParallaxRatio");    // 设置我们希望背景进行视差滚动的速度。  parallaxAnimation.SetScalarParameter("MyParallaxRatio", 0.5f);    // 设置前景对象的引用。  parallaxAnimation.SetReferenceParameter("MyForeground", foregroundVisual);    // 在背景对象上开始动画。  backgroundVisual.StartAnimation("Offset.Y", parallaxAnimation);    </code></pre>    <p>这段代码所做的第一件事是创建一个用于描述一些输入与动画结果输出之间关系的数学表达式。表达式中定义了几个参数和稍后赋值的引用。参数帮助你配置数学关系,但引用才是使表达式灵动的重点。一个参数(例如 MyParallaxRatio )是通过调用指定类型的函数(例如 SetScalarParameter )赋值的。此行为通知动画引擎对该参数的所有实例以你传入的取值进行求值。求值动作只在将动画交由引擎处理前发生一次,因此这是一个指定常量取值的好办法。相反,一个引用(例如 MyForeground ) 则是动画引擎在每帧求值的。这正是实际使表达式动画灵动的魔法所在。</p>    <p>此外还有两点需要指出。首先,你会注意到我们能够访问 MyForeground 的成员以及 Y 子通道。表达式的语法允许访问成员以及“混合”或交换一个矢量/矩阵的成分。例如:</p>    <pre>  <code class="language-cs">// 重用 offset 的 X 通道创建一个 Vector2 动画。  ExpressionAnimation vector2Animation = compositor.CreateExpressionAnimation("MyForeground.Offset.X");    // 设置对前景对象的引用。  vector2Animation.SetReferenceParameter("MyForeground", foregroundVisual);    </code></pre>    <p>另一点需要指出的是,视觉层中的所有动画实际上都是模板。这意味着你可以对多个对象使用同一个动画或重用动画的结构,只在下一个对象的动画开始前更新参数和引用。例如,如果我们想要扩展基本视差动画,添加多层景深效果,我们可以只需要一个动画定义即可:</p>    <pre>  <code class="language-cs">// 创建驱动视差滚动的表达式动画。  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);    </code></pre>    <p>表达式动画是一种全新而强大的动画方式,使我们能借以表示物体如何相对运动。表达式动画为我们免去了设置一系列复杂动画的痛苦,使多个不同对象和属性协同运动因而变得更加容易。要深入了解表达式动画,可参见 //build/ 讲谈:</p>    <p><a href="/misc/goto?guid=4959613859338548615" rel="nofollow,noindex">P486: Using Expression Animations to Create Engaging & Custom UI</a></p>    <h3>输入驱动动画</h3>    <p>自大约五年前触摸渐成主流起,创造低延迟体验成为了一种普遍需求。使用手指或笔在屏幕上操作,使得人眼获得了更直观的参照点来辨识操作的延迟和流畅性。为使操作流畅,主流操作系统公司均将更多的操作移交至系统和 GPU (如 <a href="/misc/goto?guid=4959670087795581023" rel="nofollow,noindex">Chrome</a> 、 <a href="/misc/goto?guid=4959670087889018546" rel="nofollow,noindex">IE</a> )执行。在 Windows 上,这由 <a href="/misc/goto?guid=4959670087968690472" rel="nofollow,noindex">DirectManipulation</a> 这一或多或少是针对于触摸构建的动画引擎实现的。它解决了关键的延迟挑战,也就是如何自然地以展示从输入驱动到事件驱动过渡的动效。但另一方面,它也几乎没有提供对定制惯性观感的支持,就像福特 T 型车那样——“只要车是黑色的,你可以把它涂成任意你喜欢的颜色”。</p>    <p>ElementCompositionPreview.GetScrollViewerManipulationPropertySet 是让你能够把玩输入驱动动效的第一步。虽然它仍然没给你任何对内容滚动时观感进行控制的额外能力,但它确实允许你对次级内容应用表达式动画。例如,我们终于能完成我们的基础视差滚动代码:</p>    <pre>  <code class="language-cs">// 创建驱动视差滚动的表达式动画。  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);    </code></pre>    <p>使用这一技巧,你能够实现多种优秀的效果:视差滚动、粘性表头、自定义滚动条等等。唯一缺失的就是定制操作本身的观感……</p>    <p>再来看看 InteractionTracker 。这一全新设计的特性在给予你控制操作观感方方面面的灵活性的同时,保证了手指操作低延迟的体验。在 Windows 的 UI 平台上,我们时常谈到便利性与和可行性之间的权衡。常规 UX 和调用模式通常被包装成简单易用的高级控件和特性。这确实使得他们简单易用,但也在一定程度上损失了灵活操控性。而尺度的另一端则是如图形层(Graphics Layer) 的这类封装。它们使你能够完全控制每个像素在屏幕上的呈现,但也带来了更大的复杂性。在输入处理的设计中, InteractionTracker 更多地倾向于可行性这一侧。如今在 Windows UI 平台上,你首次能够描述性地将输入到输出映射为具体的动效。</p>    <p>这里我们以一个简单的示例,通过修改惯性结束的位置来演示这种全新的灵活性。过去,你通过指定四种 <a href="/misc/goto?guid=4959670088056647760" rel="nofollow,noindex">对齐点(Snap-points)</a> 类型中的一种来修改 XAML 中 ScrollViewer 的惯性表现。现在,有了 InteractionTracker 提供的更多种可能性,你可以使用表达式动画来定义惯性在哪结束。下面是一个例子,基于惯性自然停止的位置创建了三种不同的对齐点方案:</p>    <pre>  <code class="language-cs">// 创建一个惯性端点,其条件与结束点在面板近侧。  // 变量在稍后公有变量更新后填入。  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("0");    // 插入惯性端点表达式引用的属性。  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 });    </code></pre>    <p>实际上你不仅能够如示例中一样修改惯性结束的位置,还能够修改惯性动效的轨迹。 InteractionTracker 使你能够精准定制出体现标志性体验的观感。要了解更多有关 InteractionTracker 潜力与使用的内容,可参见:</p>    <p><a href="/misc/goto?guid=4959670088136851485" rel="nofollow,noindex">P405: Adding Manipulations in the Visual Layer to Create Customized & Responsive Interactive Experiences</a></p>    <h2>如何进一步深入?</h2>    <p>如果你还未查看 <a href="/misc/goto?guid=4959670088221098807" rel="nofollow,noindex">WindowsUIDevLabs 代码仓库</a> ,你绝对应该马上去看看。该仓库的简介是这样的:</p>    <p>欢迎来到 Windows UI 开发实验室的代码仓库,本库包含了最新的示例代码、示例项目以及来自使用 Windows UI 开发各种精美 UWP 应用的开发者的反馈。</p>    <p>作为深入理解学习 Windows UI 的下一站,该代码仓库是获取深入理解平台与各种协助代码的好地方。</p>    <p>译者注:</p>    <ol>     <li> <p>平纳缓动函数(Penner’s Easing Functions):由 Robert Penner 定义的一组流行的缓动函数,被各种动效实现广泛使用。</p> </li>     <li> <p>福特 T 型车是福特于1908年至1927年推出的一款价格低廉广受欢迎的汽车。福特在其自传第四卷中提到他曾对销售人员说“只要车是黑色的,顾客可以把它涂成任何自己喜欢的颜色。”由于黑色涂料廉价耐用,出于提高生产效率的考虑福特作出了只出产黑色车型的决定。但这一决定使得福特后续的份额被竞争对手蚕食。</p> </li>    </ol>    <p>关于作者</p>    <p>本文原作者 Nick Waggoner 供职于微软 native Windows UI platform( <a href="/misc/goto?guid=4959670088308777596" rel="nofollow,noindex">@WindowsUI</a> )。</p>    <p>原作者博客: <a href="/misc/goto?guid=4959613859338548615" rel="nofollow,noindex">http://www.nickwaggoner.com/www.nickwaggoner.com/</a></p>    <p>原作者 推ter: <a href="/misc/goto?guid=4959670088397180497" rel="nofollow,noindex">@nrwaggs</a></p>    <p>本文已获原作者授权进行翻译。我后续会持续翻译 Nick Waggoner 在个人博客或其它位置发表的有关 UWP、 Windows UI 的文章。</p>