正如我在本系列书籍的第一部分中提到的,从 Windows 运行时迁移到通用 Windows 平台并不是一个很大的挑战:大多数 API 和核心特性都是一样的。然而,在创建用户界面时,情况就不同了:Windows 10 最显著的特点是它运行在多种类型的设备上,屏幕大小不同:智能手机、平板电脑、台式电脑、游戏机等。
这种灵活性在视窗 8.1 中已经是一个挑战,因为在市场上你可以找到具有多种屏幕分辨率和尺寸的手机和平板电脑,所以创建一个可以适应不同屏幕的布局的概念并不是什么新鲜事。然而,在 Windows 10 中,这个概念变得更加重要,因为在过去,8.1 的通用应用基于不同的项目(一个用于 Windows,一个用于 Windows Phone),因此,很容易创建不同的 XAML 页面、不同的资源、不同的用户控件等。
相反,在 Windows 10 中,我们看到我们有一个在每个平台上运行的单一项目,因此我们需要能够将相同的 XAML 页面适配到不同的设备。在这一章的第一部分,我们将探索所有内置的 Windows 10 功能,这些功能使实现这一目标变得更加容易。
为运行在多台设备上的应用设计用户界面可能是一个挑战,因为设计使用真实像素的界面是不可能的,因为有一系列因素(分辨率、屏幕大小和观看距离)使体验难以处理。使用真实像素将使设计师创造出在手机上完美呈现的元素,但这在 Xbox One 上可能几乎看不到,因为它是一种与大屏幕一起使用的设备,观看距离更远。因此,Windows 10 引入了有效像素的概念:当你在 XAML 页面中设计一个元素并设置一个大小时(比如 14pt 字体的TextBlock
控件或 200 px 宽度的Rectangle
控件),你的目标不是真实的屏幕像素,而是有效像素。
该大小将自动乘以窗口的比例因子,比例因子是根据分辨率、屏幕大小和观看距离分配给设备的 100%到 400%之间的值。这样,作为开发人员,您就不必担心元素是太大还是太小:由 Windows 根据运行应用的设备自动调整它,以保持观看体验的一致性。这是可能的,因为 XAML 是一种标记语言,可以操纵矢量元素:如果你放大或缩小,你不会失去质量。

图 1:有效像素有助于为元素设置固定大小,同时为用户保持一致
有效像素方法最重要的结果是,由于像素独立于设备,您可以定义一组断点,这是一系列捕捉点,您可以从这些点开始考虑更改应用的布局,因为您已经切换到屏幕更大或更小的设备。
下图显示了断点使用的一个很好的例子,取自 Windows 10 中包含的本机 Mail 应用:根据屏幕大小,您可以获得三种不同的体验。
改变布局
使用有效像素的最大优势在于,您可以使用断点来区分各种设备系列:
- 320 到 720: 电话
- **720-1024:**片&片
- 超过 1024: 大屏幕,如桌面显示器、电视屏幕或 Surface Hub

图 3:不同设备如何处理有效像素
如你所见,这些像素与设备的真实分辨率无关,而是与有效像素的概念有关。因此,例如,如果屏幕宽于 1024 有效像素,我们可以将其视为台式机/笔记本电脑或 Xbox,无论显示器的真实分辨率或 DPIs 如何。
正如我们刚刚看到的,XAML 框架帮助我们创建自适应布局体验:由于它是一种基于矢量的技术,它能够自动适应屏幕的大小和分辨率,而不会损失质量。然而,这并不意味着没有任何注意事项要记住。最重要的一点是避免给我们的控件分配固定的大小。事实上,当您给控件一个固定的大小时,它不能自动填充可用空间。因此,在定义布局时,避免使用像Canvas
和Border
这样的控件是很重要的,因为它们使用绝对定位:内容不能自动适应容器,但是它们使用像Top
和Left
这样的属性被放置在固定位置。相反,Grid
控件是您可以用来定义流畅布局的最佳容器:正如我们在上一本书中看到的,您可以定义大小可以自动适应内容的行和列。
图 4:流体布局示例:在更大的屏幕上,应用可以比在更小的屏幕上显示更多的内容。
然而,在某些情况下,这种方法会导致一些问题,尤其是在游戏中。让我们以一个象棋游戏为例:棋盘上的方块数是固定的,不管装置的大小如何。在这种情况下,如果屏幕更大,我们不需要显示更多的内容:我们只需要以更大的尺寸显示内容。对于这些情况,我们可以使用ViewBox
控件,它可以根据屏幕大小自动缩放内容:在更大的设备上,内容只会看起来更大,但内容的密度总是相同的。
使用该控件非常简单:只需在其中包装您想要自动缩放的 XAML 控件,如下例所示。
<Viewbox>
<StackPanel>
<TextBlock Text="Some
text" />
<TextBlock Text="Some
other text" />
</StackPanel>
</Viewbox>
如果你曾经接触过现代网络技术,比如 HTML5 和 CSS,你应该已经熟悉了响应布局的概念:一个网页可以根据窗口的大小来调整它的布局,这样无论用户是从个人电脑还是从手机浏览网站,它都可以始终提供出色的用户体验。调整布局并不仅仅意味着将东西变大或变小,更常见的是,深度改变内容的显示方式:例如,我们可以通过利用GridView
控件在宽屏幕上水平扩展内容,而在手机上使用ListView
控件会更好,因为它是一种通常用于人像模式的设备。
同样的概念也适用于通用视窗应用:根据窗口的大小,您可以调整应用的布局,以便内容始终能够适合可用空间。在 XAML 实现这一目标的最佳方式是使用视觉状态。我们已经在该系列的第一本书中看到了这个概念:视觉状态是控件在特定状态下应该是什么样子的定义。它们的强大之处在于,您不必为每个状态重新定义描述控件的整个模板,只需重新定义差异即可。你还记得上一本书中我们举的关于Button
控制的例子吗?它可以有多种状态(按下、禁用、突出显示),但每种视觉状态都不会从头重新定义模板,而只是与基本模板相比的差异。
Windows 10 允许您对整个页面使用相同的方法:您可以指定基本状态之间的差异,而不是定义多个页面,每个断点一个页面。这个目标可以通过 Universal Windows 平台中引入的一个新功能来实现,这个新功能叫做AdaptiveTrigger
:你可以创建一个可视状态,让 Windows 根据窗口的大小自动应用。
以下是使用自适应布局的页面的定义:
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="AdaptiveVisualStateGroup">
<VisualState x:Name="VisualStateNarrow">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="HeroImage.Height" Value="100" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="VisualStateNormal">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="720" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="HeroImage.Height" Value="200" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="VisualStateWide">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="1024" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="HeroImage.Height" Value="400" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<!-- content of the page -->
</Grid>
我们在VisualStateManager.VisualStateGroups
属性中创建一个VisualStateGroup
,它由每个控件公开。通常,当我们谈论控制整个页面的视觉状态时,我们将它们作为外部容器的子容器放置(就像默认的Grid
包含在包含所有其他控件的每个页面中)。
在VisualStateGroup
内部,我们创建多个VisualState
对象,一个用于我们想要处理的每个页面布局。在典型的 UWP 应用中,我们将为每个断点设置一个可视状态,这样无论屏幕大小如何,我们都可以真正优化体验。
Windows 10 在可视状态处理中引入了两个新功能,使创建自适应布局体验变得更加容易:
StateTriggers
,这是一种基于特定条件自动应用视觉状态的方式。通用视窗平台带有一个名为AdaptiveTrigger
的内置触发器,允许我们指定窗口的大小。当窗口大小达到该值时,VisualStateManager
将自动应用该视觉状态。在 Windows 10 之前,仅仅使用 XAML 是无法实现这个目标的,但是我们需要编写一些 C# 代码。作为开发人员,我们也有机会通过利用StateTriggerBase
类来创建自定义触发器。微软 Morten Nielsen 创建了一个伟大的自定义触发器开源集合,以处理许多常见场景(方向、设备系列等)。)可以在 GitHub 上找到:https://github.com/dotMorten/WindowsStateTriggers- 一个更简单的语法来改变属性的值:要指定我们想要在应用视觉状态时改变的属性,我们只需将控件的名称(点)设置为
Target
属性的名称,并将新值设置为Value
。例如,要更改名为HeroImage
的控件的高度,我们只需将HeroImage.Height
设置为Target
,并将新尺寸设置为Value
。
需要提醒的是,在每个视觉状态下,我们只描述了与基础状态相比的差异:页面中的所有控件都将继续看起来相同,无论窗口大小如何,但名为HeroImage
的控件除外。在这种情况下,我们根据窗口的大小来改变图像的Height
。Windows 10 将自动应用正确的视觉状态,不需要我们用 C# 写任何一行代码,只需要使用 XAML。
毫无疑问,我们在上一节中看到的自适应触发器方法是在应用中实现自适应布局的最佳方式:事实上,这种方法在桌面(用户有机会根据自己的喜好调整窗口大小)和其他平台上都很有效,在其他平台上,由于断点,我们可以为每种设备提供优化的用户体验。
然而,在某些极端情况下,这种方法可能太复杂而无法实现,因为两个不同设备之间的用户界面可能太不相同。或者,例如,当应用运行在像树莓皮这样的特殊设备上时,我们希望提供一个最小的用户界面,与运行在桌面上的版本相比,该界面具有一部分功能。
为了处理这些场景,通用视窗平台引入了 XAML 视图的概念,这是连接到类后面相同代码的不同 XAML 页面。采用这种方法,我们的项目将具有:
- 一个通用的 XAML 页面,类后面有一个代码,将用于每个设备(除非另有说明)
- 特定的 XAML 页面,与通用页面同名,类后没有代码,将用于特定类别的设备。XAML 页面将引用类后面的原始代码,这样您就可以重用所有的逻辑、控件引用、事件处理程序等。
下图显示了使用这种方法的项目的外观:
图 5:对同一页面使用不同 XAML 视图的示例项目
如您所见,项目的根包含一个 MainPage.xaml 文件及其类后面的相应代码, MainPage.xaml.cs 。XAML 文件包含默认情况下将使用的布局,除非该应用运行在有特定布局的设备上。相反,类背后的代码将包含所有的逻辑,并处理与用户的交互。
你可以注意到有两个文件夹分别叫做设备家族-团队和设备家族-Xbox ,并且每个文件夹都包含另一个 MainPage.xaml 文件。与主类的区别在于,在这种情况下,您可以注意到类后面的代码丢失了:XAML 中的控件将引用原始的 MainPage.xaml.cs 文件来处理所有关于逻辑、事件处理等的内容。
特定的布局通过一组命名约定来处理,这些命名约定应用于包含特定 XAML 文件的文件夹:
- DeviceFamily-Desktop 适用于台式电脑。
- 设备家族-移动为移动设备。
- 地面枢纽设备系列-团队。
- Xbox One 的 DeviceFamily-Xbox 。
- 面向 Windows 10 物联网核心的 DeviceFamily-IoT 。
要添加新的 XAML 视图,只需在项目中创建具有适当命名约定的文件夹,然后右键单击该文件夹并选择添加- >新项目。在可用模板列表中,选择 XAML 视图,给文件命名,然后按添加。
图 6:在 Visual Studio 中创建 XAML 视图的模板
在某些情况下,前面的两个选项都不适合您的场景。例如,我们可能要求根据屏幕的大小有两个完全不同的页面,不仅从用户界面来看,而且从逻辑角度来看。在这种情况下,我们既不能利用自适应触发器,也不能利用 XAML 视图。然而,我们有一个最后的办法,那就是在Windows.Graphics.Display
命名空间中定义的一个 API,它被称为DisplayInformation
,是在 11 月的更新中引入的。这个应用编程接口允许您检索许多关于显示器的有用信息,比如屏幕的大小,这是您想要定制用户体验时可以考虑的关键因素之一。
例如,在使用GetForCurrentView()
方法检索到当前视图的 API 引用后,您可以利用DiagonalSizeInInches
属性获取屏幕的大小(以英寸为单位)。这样,例如,您可以决定使用两种不同的导航流程:一种用于较大的设备,另一种用于较小的设备,布局针对单手体验进行了优化。下面的代码利用此属性将用户重定向到不同的页面,以防屏幕小于 6 英寸:
public void
NavigateToDetail(object sender, RoutedEventArgs e)
{
double size = DisplayInformation.GetForCurrentView().DiagonalSizeInInches.Value;
if (size < 6.0)
{
Frame.Navigate(typeof(OneHandedPage));
}
else
{
Frame.Navigate(typeof(StandardPage));
}
}
另一种方法是利用AnalyticsInfo
应用编程接口,这是Windows.System.Profile.
命名空间的一部分,由于DeviceFamily
属性,除了其他信息之外,还允许您检索运行该应用的设备系列。以下示例代码显示了如何根据设备类型更改导航流:
public void
NavigateToDetail(object sender, RoutedEventArgs e)
{
if (AnalyticsInfo.VersionInfo.DeviceFamily == "Windows.Mobile")
{
Frame.Navigate(typeof(MobilePage));
}
else
{
Frame.Navigate(typeof(StandardPage));
}
}
在这个示例中,我们创建了一个为移动设备定制的特定页面,如果我们检测到应用正在手机上运行,我们将重定向用户。
但是,必须强调的是,最后两种方法应该作为最后手段使用,因为与实现真正的自适应布局体验相比,它们有许多缺点:
- 您必须维护同一个页面的多个版本,这些版本可能会以不同的方式显示相同的数据。像 MVVM 或 XAML 视图这样的模式可以帮助减少这个问题的影响,但是使用单个页面来处理这个问题仍然更加复杂和耗时。
- 有时,设备类型之间的差异可能非常小。例如,在市场上,你可以找到搭载 Windows 10 移动版的小型平板电脑,而不是传统的完整版。在这种情况下,依靠设备系列检测可能会给用户带来不适当的体验。
- 它们不太适合桌面世界,因为用户可以根据自己的喜好调整窗口大小,无论屏幕大小还是设备类型(传统桌面、平板电脑、二合一等)。)
最终,Windows 10 引入了一个名为 Continuum 的功能,该功能在一些 Windows 10 手机(如 Lumia 950 和 Lumia 950 XL)上可用,当它们通过专用坞站连接到更大的屏幕或使用 Miracast 标准无线连接时,可以将它们变成桌面。在这种情况下,当你在支持连续体的设备上在大屏幕上启动应用时,你会获得与桌面应用相同的用户体验,即使它仍然在移动设备上运行。以前的技术可能无法提供最佳的用户体验,因为屏幕大小(检测为宽,就像是台式电脑)和运行应用的设备(手机)之间可能不匹配。
有许多技术可以在应用中实现自适应布局体验。让我们不要从技术的角度来看它们(因为它们都是基于我们之前看到的概念和特性),而是用一种更具描述性的方法。
在自适应布局中,调整大小的方法意味着改变页面中元素的大小,以便它们能够适当地适应所有可用的空间。
图 7:调整大小的方法
在大多数情况下,如果您按照上一节管理布局中描述的建议正确创建了页面,这种方法会自动为您实现:例如,当您使用像Grid
、GridView
或ListView
这样的控件时,它们都被设计为自动填充可用空间,无论屏幕大小如何。但是,在某些情况下,您可以利用自适应触发器来手动调整某些元素的大小,以便以更好的方式调整它们,例如图像。
图 8:一个需要手动处理调整大小方法的应用示例
上图显示了一个应用在两个不同大小的窗口中运行的例子:在这两个窗口中,您可以看到实现了自动和手动方法。在收集图像的情况下,我们不必担心屏幕的大小,因为GridView
控件可以自动将项目分割成多列,以防有更多的空间(在第一张图像中,我们只有一列项目,在第二张图像中,它们自动变成两列)。然而,我们不能对标题图像说同样的话:在宽屏幕上,与小屏幕相比,它变得不那么有意义,因为照片中的大多数字符都被剪切了。在这种情况下,您应该利用自适应触发器来根据屏幕的大小更改图像的大小。
重新定位技术包括在不同的地方移动应用的部分,以更好地利用可用空间。以下图为例:在大屏幕上,有更多的空间,因此两个部分(标记为 A 和 B)可以一个向右放置。相反,在较小的屏幕上,比如在手机上,我们可以将它们一个接一个地移动,因为手机可以享受垂直滚动的体验。

图 9:使用重定位技术,您可以根据窗口的大小移动页面中的控件
这种方法通常是通过将自适应触发器与我们在本系列第一本书中学习使用的RelativePanel
控件相结合来实现的:根据屏幕的大小,您可以更改RelativePanel
内子控件之间的关系。
回流意味着应用的布局应该是流畅的,这样用户就可以从应用的内容中获得最佳效果,无论屏幕大小如何。根据运行应用的设备,内容的密度应该始终适当。
图 10:应用的内容在一个屏幕更大的设备上流入两个不同的列
大多数情况下,这种方法可以自动实现,这要归功于像GridView
这样的控件,它可以自动回流内容。否则,您也可以通过利用自适应触发器来手动实现它:例如,您可以根据屏幕的大小决定将不同的ItemTemplate
分配给GridView
或ListView
控件。
Rearchitect 意味着我们处于一种情况,即相同的布局不能同时应用于小屏幕和宽屏幕,移动部分或调整它们的大小是不够的:我们需要基于运行应用的设备重新思考用户体验。这个场景最好的例子之一是主细节:我们有一个项目列表,用户可以点击其中一个来查看更多的细节。当我们在一个宽屏幕的设备上时,我们可以并排显示两者。相反,当我们在一个带有小屏幕的设备上时,我们会退回到基于两个不同页面的体验:一个是列表页面,一个是细节页面。有许多 Windows 10 内置应用利用了这种方法,如邮件或人。
图 11:主细节场景是重新构建场景的一个很好的例子
与其他方法相比,这种方法实现起来更复杂。它可以使用自适应触发器来实现,通过创建多个控件并隐藏或显示它们,不仅基于屏幕的大小,还基于页面状态(如果我们显示的是页面的母版或详细信息)。另一种方法是利用设备系列或屏幕尺寸检测技术:在这种情况下,您可以根据您的场景将用户重定向到不同的页面。
显示技术包括根据窗口的大小隐藏或显示新信息。
图 12:当在更大的设备上使用 Pivot 控件时,它会显示更多的元素
有些控件会自动实现这种行为:例如,如上图所示,Pivot
控件可以根据屏幕大小自动隐藏或显示不同数量的部分。在其他情况下,由我们的场景来定义我们想要显示哪些元素,而不是隐藏哪些元素:使用这种方法,您通常会利用自适应触发器来更改控件的Visibility
属性。
下图显示了我们在本书第一部分中学到的应用于SplitView
控件的这种技术的一个例子。在这个场景中,我们根据屏幕的大小改变控件的DisplayMode
属性:
- 如果是小屏幕,我们使用
CompactOverlay
模式,面板完全隐藏,只需点击汉堡按钮即可显示。此外,面板与页面内容重叠,不占用空间。 - 如果是中屏(如平板电脑或平板电脑),我们使用
CompactInline
模式,面板仍然覆盖页面内容,这样可以节省一些空间,但它并没有完全隐藏:部分图标的预览总是可见的。 - 如果是宽屏幕,我们使用
Inline
模式,面板总是可见的,占据他在页面上的空间。
图 13:应用于拆分视图控件的显示技术
替换技术应该被认为是“最后的手段”,因为它不能完全满足“自适应布局”的体验,事实上,它利用了我们之前描述的方法,比如在代码中检测屏幕或设备系列的大小。
实际上,替换意味着您将完全替换用户界面的某些部分,以便更好地针对屏幕大小或设备类型进行优化。
Windows 10 中第一方 Photos app 的原始版本利用了这一技术,为用户提供了针对每台设备量身定制的良好导航体验。事实上,在照片应用中,应用的各个部分都得到了处理:
- 在电脑上有一个汉堡菜单,因为这种导航方法在用鼠标和键盘控制的大型设备上运行良好。
- 在手机上有一个支点,因为汉堡菜单的方法可能不是最好的移动设备。事实上,在大屏幕设备上,用拇指很难够到汉堡按钮。
图 14:照片应用基于设备系列实现了两种完全不同的导航技术
当使用图像时,我们没有 XAML 方法提供的灵活性:事实上,图像被渲染为位图,而不是矢量,因此图像尺寸越大,质量损失越大。为了管理图像,通用视窗平台提供了一种命名约定,极大地帮助开发人员支持所有设备:您需要添加不同版本的图像(分辨率不同),视窗将根据设备的比例因子自动选择最佳版本。
| 比例因子 | One hundred | One hundred and twenty-five | One hundred and fifty | Two hundred | Two hundred and fifty | Three hundred | four hundred |
上表显示了 Windows 支持的所有不同比例因子:当然,最好的方法是为它们中的每一个提供一个图像,但是,如果您没有这个机会,重要的是您至少要为粗体突出显示的那些(100、200 和 400)提供一个图像
例如,假设您有一个分辨率为 100x100(对应于比例因子 100)的图像:为了正确支持所有可能的屏幕大小和分辨率,我们必须向项目中添加至少相同的分辨率为 200x200(对于 200 比例因子)和 400x400(对于 400 比例因子)的图像。有两种方法可以管理这种情况。它们都产生相同的结果;由你来选择哪一个适合你的需求和你的编码习惯。
第一种方法是将图像包含在同一个文件夹中,但名称以不同的后缀结尾。例如,如果原始图像名为logo.png,则应添加以下文件:
- logo.scale-100.png为 100 比例因子。
- logo.scale-200.png为 200 比例因子。
- logo.scale-400.png为 400 比例因子。
相反,第二种方法要求始终使用相同的文件名,但存储在不同的文件夹中。根据前面的示例,您应该使用以下文件夹来组织项目:
- /scale-100/logo.png 为 100 比例因子。
- /scale-200/logo.png 为 200 比例因子。
- /scale-400/logo.png 为 400 比例因子。
需要强调的最重要的一点是,这种方法对开发人员来说是完全透明的:您只需将图像的基本名称分配给控件,Windows 就会为您挑选最好的图像。例如,要使用Image
控件显示上一张名为logo.png的图像,您只需声明以下代码:
<Image Source="/Assets/logo.png" />
该应用将根据分配给运行该应用的设备的比例因子,自动使用正确版本的图像。
当然,前面的方法只适用于作为 Visual Studio 项目一部分的图像:如果图像是从网络上下载的,您将不得不手动管理图像的不同版本。您可以依靠我们之前看到的 类提供的ResolutionScale
属性来实现这个目标:您将能够检索当前的比例因子,并为您的设备下载适当的图像。
protected override void OnNavigatedTo(NavigationEventArgs e)
{
string url = string.Empty;
ResolutionScale scale = DisplayInformation.GetForCurrentView().ResolutionScale;
switch (scale)
{
case ResolutionScale.Scale100Percent:
url = "http://www.mywebsite.com/image100.png";
break;
case ResolutionScale.Scale200Percent:
url = "http://www.mywebsite.com/image200.png";
break;
case ResolutionScale.Scale400Percent:
url = "http://www.mywebsite.com/image200.png";
break;
}
MyImage.Source = new BitmapImage(new
Uri(url));
}
我们刚才看到的关于图像的方法也适用于任何通用视窗平台应用所需的标准视觉资产,如图标、图块等。如果您已经阅读了本系列第一本书的第 2 章,您会记得应用的标准可视资产是在清单文件中定义的,在一个名为可视资产的特定部分中。您可以注意到,对于该部分中请求的每个图像,您可以加载它们的多个版本,以支持不同的比例因子。当您定义要使用的图像时,可视化清单编辑器将帮助您理解要使用的正确分辨率。例如,如果您查看清单文件中的闪屏部分,您会注意到,在每个图像下,它报告了每个特定比例因子所需的正确分辨率,例如:
- 比例因子为 100 的基础图像的分辨率应为 620x300。
- 比例因子为 125 的图像应该具有 775x375 的分辨率。
- 比例因子为 150 的图像应该具有 930x450 的分辨率。
- 比例因子为 200 的图像应该具有 1240x600 的分辨率。
- 比例因子为 400 的图像应该具有 2480x1200 的分辨率。
让我们详细看看清单文件中需要哪些不同类型的图像。
此部分用于定义应用的徽标。需要多种格式:每种格式对应一个特定的用例。让我们详细看看。
- Square71x71 Logo 指的是小瓷砖图像。
- 正方形 150x150 标志是用于标准正方形瓷砖的图像。
- Wide310x150 Logo 是用于宽矩形瓷砖的图像。
- 方形 310x310 徽标是用于大方形图块的图像,但是在 Windows Mobile 上不可用。
- 在操作系统中,有些页面需要较小的徽标(如应用列表)。该图像在正方形 44x44 标志部分定义。
- 店铺标识是店铺使用的形象。
通用视窗平台应用也可以在锁定屏幕上与用户交互,当用户不积极使用设备时会显示锁定屏幕。最常见的场景是通知:我们可以提醒用户应用中发生了一些事情(例如,他们收到了一封新邮件),而无需强制他解锁设备。在本节中,您将能够定义用于显示此类通知的图像。这个图像的特点是它必须是单色的,背景是透明的。
当应用正在加载时,闪屏图像会显示给用户:一旦加载完成,闪屏就会隐藏,并显示应用的第一页。闪屏图像显示在屏幕中央,它不会填满所有可用空间(实际上,请求的分辨率为 620x300,低于任何 Windows 设备支持的任何分辨率)。因此,您还必须设置一种背景颜色,以填充剩余的空间。为了获得最佳效果,颜色与用作闪屏的图像的背景颜色相匹配是很重要的。
测试您是否正确管理了应用的布局和图像,以使其能够良好运行,无论应用运行在哪个设备上,这可能都很棘手:您需要访问许多设备,每个设备都有不同的分辨率和屏幕大小。幸运的是,Visual Studio 2015 提供了一些工具,可以帮助开发人员模拟不同的比例因子。
第一个是集成设计器,当您打开任何 XAML 页面时,都可以访问它。如果使用左下角的适当选项卡切换到设计视图,Visual Studio 将显示应用布局的预览。在右上角,您会发现一个下拉列表,您可以使用它来模拟不同类型的设备,每个设备都有自己的分辨率和比例因子。
图 15:Visual Studio 设计器
此外,您还可以注意到,在下拉列表的右侧,您可以选择更改方向和一个标签,该标签以有效像素显示设备的当前分辨率(因此已经应用了比例因子)。此外,Visual Studio 设计器可以实时应用自适应触发器:如果您创建了多个视觉状态,连接到不同大小的屏幕,它们将自动应用,并且您将看到结果的预览,而无需运行应用。
但是,有时,您需要在应用的实际执行过程中测试不同的缩放因子,因此您需要有效地启动它。在这种情况下,您可以使用我们在第一本书中描述的模拟器:事实上,它在工具栏中提供了一个选项,可以更改模拟器的当前分辨率和屏幕大小。
Windows Mobile 模拟器也包含此功能,它提供了多个不同屏幕大小和分辨率的版本,如下图所示:
图 16:Visual Studio 2015 中不同的移动仿真器
前面描述的管理屏幕尺寸调整的方法也可以应用于方向管理。在 Windows 的早期版本中,方向管理在某些情况下是可选的:例如,如果您在一个仅使用 Windows Phone 的项目中工作,管理横向方向不一定是必需的,因为大多数情况下手机是在纵向模式下使用的。然而,请记住,通用视窗平台应用可以在各种设备上运行:其中一些主要用于人像(如手机),一些用于风景(如传统桌面),一些用于两种方式(如平板电脑)。
因此,重要的是实现自适应布局体验,不仅是在屏幕大小的处理上,而且在方向上。
默认情况下,通用视窗应用会自动处理方向:当您旋转设备时,页面内容会旋转。在清单文件中的应用选项卡中,您会发现一个名为**支持旋转的部分。**然而,如果你读了描述,你会明白它并没有真正强制执行一个要求,但它更像是一种表明方向偏好的方式。事实上,如果当前平台不支持,Windows 10 总是能够覆盖清单中描述的行为。例如,假设您已经将清单配置为仅支持纵向模式,但随后应用在仅支持横向模式的桌面上启动。在这种情况下,Windows 将忽略清单设置,并且无论如何都会轮换应用。
自动方向处理可能是一个很好的起点,但它并不总是提供好的结果:处理视觉状态是管理方向变化的最佳方式,这样我们就可以根据用户握持设备的方式手动更改应用的布局。
从 XAML 的角度来看,该代码与我们在讨论实现具有视觉状态的自适应布局时看到的代码相同:您可以简单地定义两种视觉状态,一种用于纵向,另一种用于横向,在这种情况下,您将根据方向设置控件的外观。
您可以决定通过利用Page
类公开的SizeChanged
事件来管理代码中的方向更改,如下例所示。
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
this.SizeChanged += MainPage_SizeChanged;
}
private void MainPage_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (e.NewSize.Width > e.NewSize.Height)
{
VisualStateManager.GoToState(this, "DefaultLayout", true);
}
else
{
VisualStateManager.GoToState(this, "PortraitLayout", true);
}
}
}
除其他情况外,当设备的方向改变时,会触发SizeChanged
事件:在这种情况下,我们可以使用NewSize
属性提供的Width
和Height
属性来确定当前方向。如果Width
高于Height
,则表示该设备正在景观模式下使用;否则,它将在纵向模式下使用。使用VisualStateManager
,我们根据这个条件触发合适的视觉状态。
但是,如果您更喜欢只使用 XAML 而不编写 C# 代码,您可以利用已经提到的StateTriggerBase
类,它允许您创建自己的可视状态触发器。名为窗口状态触发器(https://github.com/dotMorten/WindowsStateTriggers)的社区库已经包含了一个触发器,您可以轻松使用它来处理方向更改,如下例所示:
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup >
<VisualState x:Name="landscape">
<VisualState.StateTriggers>
<triggers:OrientationStateTrigger Orientation="Landscape" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="orientationStatus.Text" Value="Landscape mode" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="portrait">
<VisualState.StateTriggers>
<triggers:OrientationStateTrigger Orientation="Portrait" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="orientationStatus.Text" Value="Portrait mode" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<TextBlock x:Name="orientationStatus"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
这个 XAML 页面只是在一个Grid
中包含一个TextBlock
控件:使用库中包含的OrientationStateTrigger
,我们根据设备的方向更改Text
属性的值。
Windows 模拟器和 Windows Mobile 模拟器都可以通过提供旋转设备的选项来帮助我们测试这个场景。
能够使应用的用户界面适应不同的屏幕和设备并不足以提供出色的用户体验。该应用还应该令人愉快地使用,并吸引用户返回使用它,不仅因为它有用,而且因为它令人愉快地使用。
实现这一目标的最佳方法是设计一个出色的用户界面,动画和效果在其中发挥着重要作用:它们有助于创造应用流畅、快速和响应迅速的感觉。
过去,开发人员倾向于过度使用这种技术。很多时候,应用只是为了动画和效果而包含动画和效果,从而获得相反的效果:减慢用户的工作流程,用户需要等待动画(就像从一个页面到另一个页面的转换)完成后才能继续前进。
相反,大多数平台目前采用的方法是仅在动画和效果有意义时才利用它们:在页面和另一个页面之间添加过渡动画有助于创建出色的用户体验,如果它流畅而快速,但如果用户每次在应用中导航时都需要等待 10 秒钟,他可能很快就会停止使用它。
复合应用编程接口是在 Windows 10 中添加的一组新的应用编程接口,随着每次更新都有所扩展(11 月和周年更新都带来了新的功能)。它们有助于添加动画和效果,与我们在上一本书中看到的用故事板实现的 XAML 动画相比,它们提供了更多的机会和更好的性能。
让我们看看下面的图片:
图 17:视窗 10 中的用户界面框架分层
在使用 Windows 应用的用户界面时,在 Windows 10 之前,我们有两种选择:
- XAML ,我们在之前的书里已经开始知道了。这是创建用户界面最简单的方法,因为它提供了一组内置的控件、属性和特性,比如数据绑定。然而,它并不是性能最好的堆栈,因为在幕后,Windows 必须负责将所有 XAML 代码转换成用户界面元素并呈现它们。
- DirectX ,这本书不会涉及,游戏通常会用到它。你可以想到它是否像一个空画板:你有能力从头开始创建一切,包括形状、复杂的 3d 对象等。而不受平台内置的 XAML 控件的“限制”。此外,该层直接与渲染引擎对话,因此您可以获得最佳性能。然而,缺点是你必须从头开始做所有的事情:即使是一个简单的文本也需要手动渲染,因为你不能像在 XAML 那样访问内置控件。
Windows.UI.Composition
是 Windows 10 中新增的一个命名空间,作为另外两个命名空间之间的中间层:它提供的功能和性能更接近 DirectX 层提供的功能和性能,但在逻辑和代码编写方面没有相同的复杂性,使得编码体验更类似于 XAML 层。
合成 API 可以用来实现两个目标:创建动画和渲染效果。让我们简单地看看这两种情况。
有四种类型动画可以用合成 API 创建:
- 关键帧动画
- 隐式动画
- 表情动画
- 连接的动画
合成动画可以应用于Visual
类的大部分属性,该类表示在视觉树中渲染的基本 XAML 对象。这些属性的一个例子是Opacity
、Offset
、Orientation
、Scale
、Size
等。此外,您还有机会将它们应用于这些属性之一的子组件。例如,当您要将动画应用于元素的Size
属性时,您可以决定只使用x
属性,而忽略y
属性。
合成 API 是一个复杂的话题,因为它们提供了很多机会和特性。因此,我们不会在这本书里讨论所有不同的类型。如果您想了解更多信息,您可以参考官方文档https://msdn . Microsoft . com/en-us/windows/uwp/graphics/composition-animation以及 GitHub 上的官方示例应用,该程序演示了所有可用功能https://github.com/Microsoft/WindowsUIDevLabs
让我们看看使用这些 API 可以实现的一些最重要的动画和效果。它们都属于命名空间Windows.UI.Composition
。
关键帧动画就像你可以用 XAML 故事板实现的动画一样,让你定义需要在特定时间点执行的动画。因此,我们讨论的是时间驱动的动画,开发者可以在特定的时间控制控件的属性需要具有的确切值。关键帧动画的一个最重要的特性是放松功能支持(也称为插值器),这是一种描述一帧和另一帧之间的过渡(也可能相当复杂)的简单方法。由于插值器,您将能够只配置动画的一些关键帧(比如您希望在开始、中间和最后将哪个值应用于属性),并且 API 将负责为您生成所有中间帧。
让我们看一个真实的例子,通过制作我们之前提到的 XAML 控件的一个属性的动画:我们想要改变Rectangle
的Opacity
,这样它就慢慢消失了,从可见变为隐藏。
首先,我们需要在页面中添加控件,并使用x:Name
属性为其分配名称:
<StackPanel>
<Rectangle Width="400" Height="400"
Fill="Blue" x:Name="MyRectangle" />
<Button Content="Start
animation" Click="OnStartAnimation" />
</StackPanel>
此外,我们还添加了一个Button
控件,它将触发动画。下面是按下按钮时调用的代码:
private void
OnStartAnimation(object sender, RoutedEventArgs e)
{
Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
var visual = ElementCompositionPreview.GetElementVisual(MyRectangle);
visual.Opacity = 1;
var animation =
compositor.CreateScalarKeyFrameAnimation();
animation.InsertKeyFrame(0,
1);
animation.InsertKeyFrame(1, 0);
animation.Duration =
TimeSpan.FromSeconds(5.0);
animation.DelayTime
= TimeSpan.FromSeconds(1.0);
visual.StartAnimation("Opacity", animation);
}
我们需要的第一件事是对合成器的引用,这是一个允许我们与合成 API 交互并应用动画和效果的对象。为了得到它,我们需要调用ElementCompositionPreview
对象的GetElementVisual()
,作为参数传递对我们想要制作动画的控件的父容器的引用(在这种情况下,它是当前的 XAML 页面,由this
关键字标识)。结果将包含一个名为Compositor
的属性,这是我们需要处理的属性。
第二步是获取对我们想要制作动画的 XAML 控件的Visual
属性的引用:在这种情况下,它是Rectangle
1,所以我们再次使用ElementCompositionPreview
对象,但是,这一次,我们调用GetElementVisual()
方法,将控件的名称作为参数传递(在我们的示例中,它是MyRectangle
)。
现在我们有了合成器和视觉,这是我们需要处理的两个元素。Compositor
类根据我们需要制作动画的属性类型,提供了许多制作动画的方法。例如,如果您需要激活控件的Size
属性(由一个包含 x 和 y 两个分量的向量构成),您可以使用CreateVector2KeyFrameAnimation()
方法。或者如果你想改变控件的颜色,可以使用CreateColorKeyFrameAnimation()
控件。在本例中,我们使用的是Opacity
属性,它是一个标量值(0 到 1 之间的十进制数):因此,我们必须使用CreateScalarKeyFrameAnimation()
方法来创建动画。
现在,我们可以通过以下方式开始自定义动画:
- 对每一个你想要操纵的时间点使用
InsertKeyFrame()
方法。该方法需要第一个参数,即帧号,以及第二个参数,即当我们到达该帧时要分配给属性的值。在这个示例中,我们只创建了两个帧:一个开始帧,控件可见(因此Opacity
等于1
),一个结束帧,控件隐藏(因此Opacity
等于0
)。 - 使用需要
TimeSpan
值的Duration
属性设置动画的持续时间(在前面的示例中,动画需要 5 秒钟才能完成)。 - 也可以选择设置一个
DelayTime
,这是另一个TimeSpan
:这种情况下,如果不想马上开始动画,但是过一会儿(这种情况下,我们按下按钮 1 秒后动画就会开始)就可以利用它。
最后,我们通过调用Compositor
对象公开的StartAnimation()
方法开始动画,将一个字符串作为参数传递给我们想要更改的属性名称(在本例中为Opacity
)和我们刚刚创建的动画对象。
仅此而已:现在,通过按下按钮,1 秒后Compositor
将负责生成所有中间关键帧,给用户的印象是Rectangle
控件正在慢慢消失。
我们本可以使用故事板 XAML 实现相同的目标,但是在一个同时有更多对象要制作动画的真实场景中,合成应用编程接口允许我们以更好的性能和更低的 CPU 使用率实现相同的结果。
例如,关键帧动画可能有用的另一种情况是,当您处理用类似ListView
或GridView
的控件显示的集合时。由于复合应用编程接口,您可以将入口效果应用于页面中的每个项目,而不会影响性能,即使集合是由成千上万个元素组成的。
为了实现这个目标,你可以利用一个由控制所暴露的事件,比如ListView
或者GridView
,叫做ContainerContentChanging
:每次控制在视觉上呈现列表中的一个新项目时,它都会被触发,因此,我们可以使用它来激活入口效果。
下面是实现该功能的GridView
控件的外观:
<GridView ItemsSource="{x:Bind TopSeries, Mode=OneWay}"
x:Name="TvSeries"
ItemTemplate="{StaticResource GridTemplate}"
ContainerContentChanging="GridView_ContainerContentChanging" />
以下是ContainerContentChanging
事件的事件处理程序是如何实现的:
private void
GridView_ContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
{
Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
var visual = ElementCompositionPreview.GetElementVisual(args.ItemContainer);
visual.Opacity = 0;
var animation =
compositor.CreateScalarKeyFrameAnimation();
animation.InsertKeyFrame(0, 0);
animation.InsertKeyFrame(1, 1);
animation.Duration =
TimeSpan.FromSeconds(4);
animation.DelayTime
= TimeSpan.FromMilliseconds(args.ItemIndex *
200);
visual.StartAnimation("Opacity", animation);
}
如您所见,代码与我们之前看到的相同。唯一的区别是:
- 由于事件处理程序参数的
ItemContainer
属性,我们不是获取对页面中特定控件的引用(像前面的Rectangle
控件),而是获取对正在呈现的项的可视容器的引用。 DelayTime
不是固定的时间,而是根据正在渲染的项目的索引来计算的(感谢ItemIndex
属性)。原因是我们希望列表中的每一项都按顺序动画化,一个接一个,以给出更好的结果。
这段代码的结果是,我们将看到集合中的所有项一个接一个地慢慢出现在页面中。这是一个动画的例子,使用 XAML 的标准故事板很难完成。
隐式动画背后的基本概念与我们刚刚看到的关键帧动画相同:不同之处在于,在前面的场景中,动画是以显式方式定义的,由开发人员决定动画何时开始和结束(如点击按钮或渲染GridView
中的项目)。
相反,当开发人员控制之外的 XAML 控件的属性发生更改时,会自动触发隐式动画。
让我们通过重用之前的 XAML 代码来看一个例子,在这个例子中,我们有一个Rectangle
控件,我们想要激活它:
<StackPanel>
<Rectangle Width="400" Height="400"
Fill="Blue" x:Name="MyRectangle" />
<Button Content="Start
animation" Click="OnStartAnimation" />
</StackPanel>
由于在这种情况下,动画不是由用户手动触发的,我们将在页面的OnNavigatedTo()
方法中定义它:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
var visual = ElementCompositionPreview.GetElementVisual(MyRectangle);
var offsetAnimation =
compositor.CreateVector3KeyFrameAnimation();
offsetAnimation.InsertExpressionKeyFrame(1, "this.FinalValue");
offsetAnimation.Duration = TimeSpan.FromSeconds(1);
offsetAnimation.Target = "Offset";
var implicitMap =
compositor.CreateImplicitAnimationCollection();
implicitMap.Add("Offset", offsetAnimation);
visual.ImplicitAnimations =
implicitMap;
}
代码的大部分类似于我们在关键帧动画中看到的部分:我们获得了对Compositor
和连接到Rectangle
控件的Visual
对象的引用。然而,在这种情况下,我们不想再隐藏或显示Rectangle
,但我们想移动它:因此,我们需要使用Offset
属性,该属性由三个轴 X,Y,z 上的向量表示。因此,我们使用CreateVector3KeyFrameAnimation()
创建动画。
同样在这种情况下,我们使用Duration
属性设置持续时间,但是与关键帧动画相比,有两个重要的区别:
- 我们设置一个
Target
属性,通过指定哪个属性将在被改变时触发动画。在本例中,我们将Offset
属性设置为Target
,这意味着每当有人试图更改Rectangle
控件的位置时,我们正在创建的动画都会自动触发。 - 我们使用
InsertExpressKeyFrame()
方法,将帧号和一个固定的字符串this.FinalValue
作为参数传递。当使用隐式动画时,您不需要为动画的起点设置关键帧,因为它是隐式的(在这种情况下,当Offset
属性因矩形移动而改变时,动画将开始)。然而,我们需要指定动画的结尾。this.FinalValue
是一个特殊的表达式,用于标识属性的最终值(请记住,我们正在设置的动画将会运行,因为目标属性是由其他人设置的)
最后一步是通过调用Compositor
对象上的CreateImplicitAnimationCollection()
方法来创建隐式动画的集合(因为您可以为同一个控件分配多个动画)。这个集合是一个字典,每个条目都由一个键(T2 属性)和一个值(我们刚刚创建的动画)组成。
最后,我们通过将刚刚创建的集合设置为控件视觉的ImplicitAnimations
属性(在本例中为Rectangle
控件的视觉)来连接拼图的所有部分。
现在,如果我们想测试这个动画,我们需要一些如何改变Rectangle
控件的偏移量。最简单的方法是将此操作委托给Button
控件,如下例所示:
private void
OnStartAnimation(object sender, RoutedEventArgs e)
{
var visual = ElementCompositionPreview.GetElementVisual(MyRectangle);
visual.Offset = new System.Numerics.Vector3(350,
0, 0);
}
仅此而已。现在,如果你按下按钮,你会看到 350 像素的矩形从右向左移动。但是,由于我们添加了一个隐式动画,Compositor
对象会为我们创建一组关键帧动画,所以矩形会从一个点慢慢移动到另一个点,而不仅仅是从一个地方消失,出现在另一个地方。
您可能想知道隐式动画在哪个场景中有用:最后,前面的示例代码也可以通过关键帧动画来实现,方法是在按下按钮时直接设置各种关键帧。然而,请记住,并不是每个动作都可以由开发人员直接控制:其中一些动作是用户在我们的应用控制之外所做的事情的结果。
为了更好地解释这个场景,让我们再次使用GridView
控件,并再次订阅ContainterContentChanging
事件:
<GridView ItemsSource="{x:Bind TopSeries, Mode=OneWay}"
x:Name="TvSeries"
ItemTemplate="{StaticResource GridTemplate}"
ContainerContentChanging="GridView_ContainerContentChanging" />
下面是如何将事件处理程序配置为使用隐式动画:
private void
GridView_ContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
{
Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
var visual = ElementCompositionPreview.GetElementVisual(args.ItemContainer);
var offsetAnimation =
compositor.CreateVector3KeyFrameAnimation();
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
offsetAnimation.Duration = TimeSpan.FromMilliseconds(450);
offsetAnimation.Target = "Offset";
var implicitMap =
compositor.CreateImplicitAnimationCollection();
implicitMap.Add("Offset", offsetAnimation);
visual.ImplicitAnimations =
implicitMap;
}
我们添加了与之前相同的动画(基于Offset
属性):不同的是,这一次,它已经应用于当前正在渲染的GridView
控件项的容器。通过这段代码,我们将在每次集合中的一个项目要改变他的位置时应用一个动画。你能想到这种情况会发生吗?当我们谈到自适应布局和重排体验时,我们看到了一个例子:当应用在桌面上运行,用户开始调整窗口大小时,GridView
控件将自动开始在新的行和列中来回移动项目,这样内容将始终适合可用空间。与以前的方法相比,不同之处在于,得益于隐式动画,现在的回流将被动画化:每次用户开始调整应用的窗口大小时,GridView
控件中的项目将慢慢移动到新的位置,而不是简单地从一行或一列中消失,然后重新出现在另一行或一列中,从而创造出更加流畅的用户体验。
这是隐式动画的完美场景:由于GridView
控件的每一项的Offset
都可以在开发人员的控制之外进行更改,所以我们不可能用关键帧动画获得相同的结果。
合成应用编程接口还提供了连接到同一个控件多个动画的机会,无论它们是隐式的还是基于关键帧的。我们再来看看通常的Rectangle
样本:
<StackPanel>
<Rectangle Width="400" Height="400"
Fill="Blue" x:Name="MyRectangle" />
<Button Content="Start
animation" Click="OnStartAnimation" />
</StackPanel>
这一次,对于Rectangle
控件的视觉,我们将应用我们之前创建的两个动画:关键帧一,作用于Opacity
属性,隐式一,作用于Offset
属性。以下是页面的OnNavigatedTo()
方法代码:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
var visual = ElementCompositionPreview.GetElementVisual(MyRectangle);
var offsetAnimation =
compositor.CreateVector3KeyFrameAnimation();
offsetAnimation.InsertExpressionKeyFrame(1, "this.FinalValue");
offsetAnimation.Duration
= TimeSpan.FromSeconds(1);
offsetAnimation.Target = "Offset";
var implicitMap =
compositor.CreateImplicitAnimationCollection();
implicitMap.Add("Offset", offsetAnimation);
var rotationAnimation = compositor.CreateScalarKeyFrameAnimation();
rotationAnimation.Target = "Opacity";
rotationAnimation.InsertKeyFrame(0,
1);
rotationAnimation.InsertKeyFrame(1, 0);
rotationAnimation.Duration = TimeSpan.FromSeconds(1);
var animationGroup = compositor.CreateAnimationGroup();
animationGroup.Add(offsetAnimation);
animationGroup.Add(rotationAnimation);
var implicitAnimations =
compositor.CreateImplicitAnimationCollection();
implicitAnimations.Add("Offset", animationGroup);
visual.ImplicitAnimations
= implicitAnimations;
}
如您所见,该代码是我们之前看到的两个示例的混合:这两个动画是以完全相同的方式创建的。不同之处在于代码的最后一部分,我们调用Compositor
对象的CreateAnimationsGroup()
方法来访问我们想要应用的动画集合。在这种情况下,通过使用Add()
方法,我们将两者相加:关键帧(通过隐藏矩形作用于Opacity
)和隐含的关键帧(通过设置矩形运动的动画作用于Offset
)。
最后,我们仍然使用Compositor
对象的CreateImplicitAnimationCollection()
方法创建一组隐式动画,并将其绑定到Offset
属性(因为我们仍然希望当矩形改变其位置时触发动画):不同的是,这一次,我们不再传递单个动画作为第二个参数,而是传递我们刚刚创建的一组动画。
最后一段代码和之前一样:当按下页面上的Button
时,我们改变Rectangle
的Offset
,这样就触发了隐式动画。
private void
OnStartAnimation(object sender, RoutedEventArgs e)
{
var visual = ElementCompositionPreview.GetElementVisual(MyRectangle);
visual.Offset = new System.Numerics.Vector3(350,
0, 0);
}
然而,在这种情况下,Offset
属性的改变将触发两个动画:结果是矩形将从右向左慢慢移动,同时它将慢慢消失。
合成 API 不仅可以用来触发动画,还可以应用模糊、阴影、遮罩不透明度等效果。实现它们最简单的方法是利用微软创建的库 Win2D 来应用二维效果。这一要求的原因是,为了提高整个 UWP 的一致性,合成效果管道被设计为在 Win2D 中重用效果描述类,而不是创建一组并行的类。
因此,第一步是右键单击您的项目,选择管理 NuGet 包并搜索和安装名为 Win2d.uwp 的包。
图 18:使用合成 API 应用效果所需的 Win2d NuGet 包
让我们考虑以下 XAML 代码,带有一个Image
和一个Button
控件:
<StackPanel>
<Image Source="Assets/image.jpg" Width="400" x:Name="BackgroundImage" />
<Button Content="Apply
effect" Click="OnApplyEffect" />
</StackPanel>
当按钮被按下时,我们可以通过调用下面的代码来使用合成应用接口给图像应用模糊效果:
private void
OnApplyEffect(object sender, RoutedEventArgs e)
{
var graphicsEffect = new GaussianBlurEffect
{
Name = "Blur",
Source = new CompositionEffectSourceParameter("Backdrop"),
BlurAmount = 7.0f,
BorderMode = EffectBorderMode.Hard
};
var blurEffectFactory =
_compositor.CreateEffectFactory(graphicsEffect,
new[] { "Blur.BlurAmount" });
_brush = blurEffectFactory.CreateBrush();
var destinationBrush = _compositor.CreateBackdropBrush();
_brush.SetSourceParameter("Backdrop", destinationBrush);
var blurSprite = _compositor.CreateSpriteVisual();
blurSprite.Size = new Vector2((float)BackgroundImage.ActualWidth, (float)BackgroundImage.ActualHeight);
blurSprite.Brush =
_brush;
ElementCompositionPreview.SetElementChildVisual(BackgroundImage,
blurSprite);
}
Microsoft.Graphics.Canvas.Effects
命名空间包含可应用于 XAML 控件的多种效果。在这种情况下,我们使用GaussianBlurEffect
,我们使用它来创建模糊效果。当我们创建它时,我们配置一组参数来定义效果,像Name
(这是唯一的标识符)、BlurAmount
(这是效果的强度)和Source
,这是将应用效果的属性(在这种情况下,这是Image
控件的Backdrop
)。
代码的其余部分有点“冗长”:
- 首先,我们需要创建一个效果工厂,通过调用
CreateEffectFactory()
方法,将我们想要控制的效果和属性(在本例中,效果的BlurAmount
属性由Blur
名称标识)作为参数传递。 - 然后我们需要从工厂创建一个笔刷,通过调用
CreateBrush()
方法。 - 由于在这种情况下,效果将应用于
Image
控件的Backdrop
,我们需要在Compositor
对象上调用CreateBackdropBrush()
方法,并使用SetSourceParameter()
方法将结果指定为我们之前创建的笔刷的来源。 - 最后,我们需要把这个笔刷应用到一个精灵上,这个精灵是通过调用
Compositor
对象的CreateSpriteVisual()
方法创建的。我们为这个Sprite
对象分配一个Size
(与原始图像相同)和一个Brush
(这是我们之前创建的笔刷)。 - 最后,通过调用
ElementCompositionPreview
类的SetElementChildVisual()
方法,通过将我们在 XAML 页面中放置的Image
控件的名称和我们之前创建的Sprite
作为参数传递,我们终于能够将所有的拼图拼在一起。
仅此而已:如果我们做的一切都正确,当我们按下按钮时,我们的图像应该会有模糊效果,如下图所示。
图 19:由于合成应用接口,应用了模糊效果
使用合成 API 的效果的好处是它们可以与动画相结合。让我们用以下代码更改连接到Button
的事件处理程序:
private void
OnApplyEffect(object sender, RoutedEventArgs e)
{
var graphicsEffect = new GaussianBlurEffect
{
Name = "Blur",
Source = new CompositionEffectSourceParameter("Backdrop"),
BlurAmount =
0.0f,
BorderMode = EffectBorderMode.Hard
};
var blurEffectFactory =
_compositor.CreateEffectFactory(graphicsEffect,
new[] { "Blur.BlurAmount" });
_brush =
blurEffectFactory.CreateBrush();
var destinationBrush = _compositor.CreateBackdropBrush();
_brush.SetSourceParameter("Backdrop", destinationBrush);
var blurSprite = _compositor.CreateSpriteVisual();
blurSprite.Size = new Vector2((float)BackgroundImage.ActualWidth, (float)BackgroundImage.ActualHeight);
blurSprite.Brush =
_brush;
ElementCompositionPreview.SetElementChildVisual(BackgroundImage,
blurSprite);
ScalarKeyFrameAnimation blurAnimation =
_compositor.CreateScalarKeyFrameAnimation();
blurAnimation.InsertKeyFrame(0.0f, 0.0f);
blurAnimation.InsertKeyFrame(0.5f, 7.0f);
blurAnimation.InsertKeyFrame(1.0f, 12.0f);
blurAnimation.Duration = TimeSpan.FromSeconds(4);
_brush.StartAnimation("Blur.BlurAmount", blurAnimation);
}
用黄色突出显示,您可以看到与上一个示例相比,我们添加的代码行。我们已经创建了一个标准的关键帧动画,在本例中是一个标量动画,因为BlurAmount
属性是由一个数字定义的。我们定义了三个关键帧动画:
- 开始时,模糊强度应为 0(因此图像完全清晰)。
- 在动画的一半,模糊强度应该是 7。
- 在动画的最后,模糊强度应该是 14。
你可以注意到一个笔刷的行为就像一个标准的Visual
对象,所以它提供了和我们之前讨论动画时看到的相同的StartAnimation()
方法。为了触发动画,我们简单地将这种传递称为参数、标识我们想要制作动画的属性的字符串(Blur.BlurAmount
)以及我们刚刚创建的动画。
现在,当用户按下按钮时,我们将获得与以前相同的结果(应用于图像的模糊效果),但平滑过渡将持续 4 秒钟。
在本书的前一部分,我们提到了 UWP 社区工具包,这是一个开源的控件、服务和助手集合,由微软在社区的帮助下创建和维护。当涉及到利用合成 API 将效果和动画应用到控件时,UWP 社区工具包可以成为一个很好的朋友。
让我们以我们在前面的代码中应用的模糊效果为例:如您所见,这不是一个简单的操作,因为有很多代码要按照正确的顺序编写。
UWP 社区工具包包括一组内置的行为,它们是特殊的 XAML 元素,可以应用于控件,并且可以在幕后执行一系列操作,或者,您将有机会只在代码中执行这些操作。
UWP 社区工具包包含一个名为微软的特定 NuGet 包。工具包。Uwp.UI .动画,当涉及到使用合成 API 的一些特性时,它可以让你的生活变得更容易。
图 20:我们需要的 NuGet 包以一种更简单的方式使用组合 API 效果
例如,让我们看看,在我们的项目中安装了这个包之后,我们如何使用不同的方法对相同的Image
控件应用模糊效果:
<Page
x:Class="SampleApp.MainPage"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:SampleApp"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:behaviors="using:Microsoft.Toolkit.Uwp.UI.Animations.Behaviors"
mc:Ignorable="d">
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<Image Source="Assets/image.jpg" Width="400" x:Name="BackgroundImage">
<interactivity:Interaction.Behaviors>
<behaviors:Blur x:Name="BlurBehavior"
AutomaticallyStart="True"
Duration="0"
Delay="0"
Value="7"/>
</interactivity:Interaction.Behaviors>
</Image>
</Grid>
</Page>
如您所见,我们不需要在代码隐藏中编写任何代码。我们只需要给Image
控件分配一个行为(由于Interaction.Behaviors
属性):在这种情况下,行为的名称是Blur
。
您可以注意到,这两个对象都不是标准通用 Windows 平台的一部分,因此,您必须在Page
定义中声明它们的 XAML 命名空间:Interaction.Behaviors
集合的Microsoft.Xaml.Interactivity
和Blur
行为的Microsoft.Toolkit.Uwp.UI.Animations.Behaviors
。
要配置行为,我们可以依赖一组简单的属性,如:
AutomaticallyStart
,这是一个bool
,定义了我们是否要立即应用效果。Duration
,这是可选动画的持续时间。Delay
,也就是过了多少时间动画才会开始。Value
,这是分配给效果的值(在这种情况下,模糊强度将为 7)。
正如您所注意到的,我们可以使用这个行为来应用效果(因为我们已经将 0 指定为Duration
,模糊将被立即应用)或者包括动画(通过简单地将Duration
属性设置为不同的值)。例如,下面是我们如何实现之前在代码中创建的相同动画,该动画在 4 秒内将模糊强度从 0 更改为 12:
<Page
x:Class="SampleApp.MainPage"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:SampleApp"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:behaviors="using:Microsoft.Toolkit.Uwp.UI.Animations.Behaviors"
mc:Ignorable="d">
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<Image Source="Assets/image.jpg" Width="400" x:Name="BackgroundImage">
<interactivity:Interaction.Behaviors>
<behaviors:Blur x:Name="BlurBehavior"
AutomaticallyStart="True"
Duration="0"
Delay="4"
Value="12"/>
</interactivity:Interaction.Behaviors>
</Image>
</Grid>
</Page>
Microsoft.Toolkit.Uwp.UI.Animations.Behaviors
包含许多其他行为来应用不同的效果,如Fade
、Rotate
、Scale
等。您可以很容易地注意到,得益于 UWP 工具包,我们实现了两个重要目标:
- 我们已经能够在 XAML 保留所有与用户界面相关的代码。
- 要编写的 XAML 代码比我们不得不手动编写的 C# 代码要简单得多,也更容易记住。
正如我们之前在本系列书中提到的,与传统桌面应用不同,通用视窗平台应用是基于页面的。每个页面都显示一些内容,用户可以从一个页面导航到另一个页面来浏览应用。因此,通用视窗平台应用基于Frame
概念,这是所有应用页面的容器。一个Frame
可以包含一个或多个Page
对象,这些对象用类似于网站提供的层次结构来管理:用户有机会在不同的页面上来回移动。
正如我们已经看到的,每个应用的页面都继承自Page
类,该类提供了一组对管理页面生命周期很重要的事件。在这本书里,我们会经常用到其中的两个:OnNavigatedTo()
和OnNavigatedFrom()
。第一个是当用户导航到当前页面时触发的:它是初始化需要在页面中显示的数据的最佳入口点之一(例如,从数据库或 web 服务中检索一些数据)。其中一个主要原因是,通常情况下,通过利用async
和await
模式,数据加载最好使用异步代码来完成。然而,页面的构造函数(通常是开发人员尝试包含数据加载逻辑的第一个地方)不能是异步的。这是 C# 的一个普遍限制:创建一个对象的新实例应该是一个立即的操作,因为在大多数情况下,构造函数不能执行异步代码。而OnNavigatedTo()
方法,连接到一个事件,没有这个限制,可以无限制的使用async
和await
关键词。相反,当用户从当前页面导航到另一个页面时,会触发第二个。这两个入口点对于保存和恢复页面状态也非常有用,这样我们就可以正确地管理应用的生命周期。
protected override void OnNavigatedTo(NavigationEventArgs e)
{
//load the data
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
//save the data
}
Frame
类,因为它是页面的容器,提供了从一个页面导航到另一个页面的基本方法。最基本的一个叫做Navigate()
,它接受一个类型作为参数,这个类型标识了你想要重定向用户的页面。
例如,如果您想将用户重定向到名为 MainPage.xaml 的页面,类型为MainPage
,您可以使用以下代码:
private void
OnGoToMainPageClicked(object sender, RoutedEventArgs e)
{
this.Frame.Navigate(typeof(MainPage));
}
Navigate()
方法还接受第二个参数,这是一个您希望从一个页面传递到另一个页面的对象:它在常见的主-细节场景中很有用,在这种场景中,用户点击一个页面中的元素,然后他被重定向到另一个页面,以查看关于所选项目的更多信息。
以下示例代码从ListView
控件中检索所选项,并将其传递到另一页:
private void
OnGoToMainPageClicked(object sender, RoutedEventArgs e)
{
Person person = People.SelectedItem as Person;
this.Frame.Navigate(typeof(MainPage), person);
}
然后,由于导航参数中存储的Parameter
属性,我们能够在目标页面的OnNavigateTo()
事件处理程序中检索参数,如下例所示:
protected override async void
OnNavigatedTo(NavigationEventArgs e)
{
Person person = e.Parameter as Person;
MessageDialog dialog = new MessageDialog(person.Name);
await dialog.ShowAsync();
}
由于Parameter
属性可以包含一个泛型object
,我们需要首先对期望的类型执行强制转换。然而,重要的是要强调作为参数传递的对象应该是可序列化的。我们将在下一章中再次讨论这个重要的概念。
通用视窗平台应用在导航方面遵循分层方法,这与网络应用提供的方法非常相似:通常,用户从主页面开始,然后移动到应用的其他页面。但是,他也可以决定向后导航并返回到前面的页面。
页面层次结构像堆栈一样管理:每次导航到一个新页面时,都会在堆栈顶部添加一个新项目;相反,当您向后导航时,堆栈顶部的页面将被移除。这两个平台都要求开发人员通过使用Frame
类提供的GoBack()
按钮来正确管理向后导航。事实上,默认情况下,每个 Windows 10 设备中包含的后退按钮会将用户重定向到以前打开的应用,而不是上一页。因此,如果我们想让我们的应用的行为与系统的用户体验和用户的期望保持一致,我们需要手动管理向后导航。
与 Windows 8.1 相比,Windows 10 在处理后退按钮方面引入了一个重要的区别。过去,您只需要在 Windows Phone 应用中处理它,因为它是唯一一个带有集成硬件后退按钮的平台。由于台式机和平板电脑没有专用的按钮,所以开发人员可以直接将其集成到应用的用户界面中。
相反,Windows 10 引入了统一的后退按钮管理,根据应用运行的平台,以不同的方式实现:
- 在手机上,后退按钮是操作系统界面的一部分。在一些设备上,它是一个物理按钮,在另一些设备上,它是虚拟的,是用户界面的一部分。在这两种情况下,它都放在手机外壳的底部。
图 21:Windows 10 移动设备上的后退按钮
- 在平板电脑(或已激活平板模式的电脑)上,后退按钮包含在设备底部的导航栏中,位于开始按钮和搜索/ Cortana 按钮之间。
图 22:Windows 10 平板电脑上的后退按钮
- 在桌面上,后退按钮包含在窗口的铬合金中,并放置在左上角。
图 23:Windows 10 电脑上的后退按钮
- 在 Surface Hub 上,这种体验就像平板电脑一样:底部导航栏中有一个虚拟按钮,唯一的区别是它被放在屏幕的中间。
图 Surface Hub 上的后退按钮
- 在 Xbox One 上,向后导航是通过按下游戏手柄上的 B 按钮来触发的
图 Xbox One S 控制器
无论运行应用的设备是哪一个,Universal Windows Platform 都提供了一个专用的 API 来检测和处理用户按下了后退按钮,因此,我们需要将他重定向到应用的上一页(除非后面的堆栈是空的,这通常意味着我们在主页面上)。
该应用编程接口由SystemNavigationManager
类(包含在Windows.UI.Core
命名空间中)公开,该类提供了一个名为BackRequested
的事件,每当用户按下后退按钮时都会调用该事件,无论该应用运行在哪个设备上:
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
SystemNavigationManager.GetForCurrentView().BackRequested
+= MainPage_BackRequested;
}
private void MainPage_BackRequested(object sender, BackRequestedEventArgs e)
{
//perform back navigation
}
}
正如您所注意到的,在订阅BackRequested
事件之前,我们需要通过调用GetCurrentView()
方法来获取对当前页面的SystemNavigationManager
实现的引用。
此外,SystemNavigationManager
类提供了一个名为AppViewBackButtonVisibility
的属性,该属性仅适用于桌面。事实上,默认情况下,窗口的 chrome 中包含的后退按钮是不可见的。如果我们想要显示它,我们需要将该属性设置为true
。
然而,我们刚才描述的方法维护起来会非常昂贵,因为我们需要编写相同的代码来处理应用每个页面中的后退按钮。因此,处理这一需求的最佳方式是将后退按钮管理集中在页面的App
类中:这样,预期的行为(将用户重定向到应用的上一页)将自动应用于应用的每个页面。
第一步是打开App
类(存储在 App.xaml.cs 文件中)并寻找OnLaunched()
方法。目前,重要的是要知道这是应用从一开始启动时调用的方法:我们将在本章后面讨论应用的生命周期时看到更多细节。
默认方法是这样的:
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
Frame rootFrame = Window.Current.Content as Frame;
// Do not repeat app initialization
when the Window already has content,
// just ensure that the window is
active
if (rootFrame == null)
{
// Create a Frame to act as the
navigation context and navigate to the first page
rootFrame = new Frame();
rootFrame.NavigationFailed += OnNavigationFailed;
if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
{
//TODO: Load state from previously suspended
application
}
// Place the frame in the current
Window
Window.Current.Content = rootFrame;
}
if (e.PrelaunchActivated == false)
{
if (rootFrame.Content == null)
{
// When the navigation stack isn't
restored navigate to the first page,
// configuring the new page by
passing required information as a navigation
// parameter
rootFrame.Navigate(typeof(MainPage), e.Arguments);
}
// Ensure the current window is
active
Window.Current.Activate();
}
}
我们需要稍微改变一下之前的代码,以实现几个目标:
- 我们需要检查后栈是否为空。如果应用在桌面上运行,事实上,只有当有有效的页面可以返回时,我们才会在窗口的 chrome 中显示后退按钮。
- 我们需要在每次导航时重复前面的检查,因为情况可能会改变(例如,如果我们在主页面中,按钮可能会被隐藏,但是如果我们在其中一个内页中,按钮必须再次显示)。
- 我们需要全局处理后退按钮,这样,无论用户访问哪个页面,他都可以返回到上一个页面。
以下是新方法的外观,用黄色突出显示了我们所做的更改:
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
Frame rootFrame = Window.Current.Content as Frame;
// Do not repeat app initialization
when the Window already has content,
// just ensure that the window is
active
if (rootFrame == null)
{
// Create a Frame to act as the
navigation context and navigate to the first page
rootFrame = new Frame();
rootFrame.NavigationFailed += OnNavigationFailed;
rootFrame.Navigated += OnNavigated;
if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
{
//TODO: Load state from previously suspended
application
}
// Place the frame in the current
Window
Window.Current.Content = rootFrame;
SystemNavigationManager.GetForCurrentView().BackRequested
+= OnBackRequested;
SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility
=
rootFrame.CanGoBack ?
AppViewBackButtonVisibility.Visible :
AppViewBackButtonVisibility.Collapsed;
}
if (e.PrelaunchActivated == false)
{
if (rootFrame.Content == null)
{
// When the navigation stack isn't
restored navigate to the first page,
// configuring the new page by
passing required information as a navigation
// parameter
rootFrame.Navigate(typeof(MainPage), e.Arguments);
}
// Ensure the current window is
active
Window.Current.Activate();
}
}
我们做的第一个改变是订阅应用根框架的Navigated
事件,这意味着每次用户从一个页面移动到另一个页面时,我们都会收到通知。我们使用这种方法来了解当应用在桌面上运行时,我们是否需要显示或隐藏后退按钮。下面是事件处理程序的实现:
private void
OnNavigated(object sender, NavigationEventArgs e)
{
SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility
=
((Frame)sender).CanGoBack ?
AppViewBackButtonVisibility.Visible :
AppViewBackButtonVisibility.Collapsed;
}
这个目标很容易实现,得益于名为CanGoBack
的bool
属性:如果是true
,说明栈中还有其他页面,所以按钮应该可见;否则,我们就藏起来。我们通过更改当前页面的SystemNavigationManager
的AppViewBackButtonVisibility
属性的值来实现这个目标。
我们对OnLaunched()
方法所做的第二个更改是订阅SystemNavigationManager
类的BackRequested
事件,就像我们在前面的示例中看到的那样,但是在这种情况下,它被应用于单个页面,而不是整个应用。下面是事件处理程序的实现:
private void
OnBackRequested(object sender, BackRequestedEventArgs e)
{
Frame rootFrame = Window.Current.Content as Frame;
if (rootFrame.CanGoBack)
{
e.Handled = true;
rootFrame.GoBack();
}
}
同样在这种情况下,我们利用应用根框架的CanGoBack
属性。只有当它是true
的时候,才意味着在后面的堆栈中还有其他页面,所以我们通过调用GoBack()
方法来触发向后导航。重要:我们还需要将方法参数的Handled
属性设置为true
,以防止窗口管理返回按钮(并强制打开以前使用的应用)。
我们添加到OnLaunched()
方法中的最后一段代码与我们在Navigated
事件的处理程序中看到的代码相同:原因是,当应用第一次启动时,Navigated
事件尚未被触发,因此我们需要手动检查后堆栈上是否有页面,因此,我们需要在桌面上显示或显示后退按钮。
当您使用页面堆栈时,需要管理的一件真正重要的事情是,当您想要将用户重定向到上一个页面时,请始终使用Frame
类的GoBack()
方法,而不要使用Navigate()
方法。
这是必需的,因为正如我们已经提到的,页面是用堆栈管理的:GoBack()
方法从堆栈中移除顶部页面,而Navigate()
方法在顶部添加一个新页面。结果是,如果我们使用Navigate()
方法返回到上一页,我们创建了一个循环导航,用户在相同的两个页面之间不断移动。
让我们看一个真实的例子:你有一个应用,它有一个显示新闻列表的主页面。应用提供了一个设置按钮,将用户重定向到可以配置应用的页面。在这个页面的底部,我们添加了一个确认按钮:点击后,设置被保存,用户被重定向回主页。
假设我们使用Navigate()
方法执行这个到主页面的向后导航:发生的事情是,我们没有从堆栈中移除设置页面,而是在它的顶部添加了主页面。结果是,如果现在用户按下后退按钮,而不是返回开始菜单(这是预期的行为,因为他在主页面上),他将被重定向回设置页面,因为它已经存在于堆栈中。
管理这种场景的正确方法是当用户按下确认按钮时调用GoBack()
方法:这样,设置页面将从堆栈中移除,留下主页面作为堆栈中唯一可用的页面。这样,再次按下后退按钮将正确地将用户重定向到开始屏幕,退出应用。
如果您已经使用过 Windows Phone 8.0 和 Silverlight,您会记得,在页面从堆栈中移除之前,它的状态会保留在内存中。这意味着,如果用户按下后退按钮返回到上一页,他会发现它处于与他先前离开时相同的状态。
Windows Runtime 已经改变了这种行为,它仍然适用于通用 Windows 平台:每当用户被重定向到一个页面时(无论是向前导航到新页面还是向后导航到已经在堆栈中的页面),都会创建一个新的实例。这意味着状态永远不会被保持:例如,如果一个页面包含一个TextBox
控件,并且用户在其中写了一些东西,那么一旦他离开该页面,内容就会丢失。
如果想避免这个问题,保持之前的行为,可以在页面构造器中将页面的NavigationCacheMode
属性设置为Required
或者Enabled
,或者通过设置Page
类在 XAML 提供的属性:这样,页面状态将一直保持。需要强调的是,在这种情况下,您需要正确管理数据加载,避免在页面构造函数中加载东西,因为只有在第一次请求页面时才会调用它。最好使用像OnNavigatedTo()
这样的方法,每次用户导航到页面时都会触发。这两个值的区别是什么?它们都保持页面的状态,但是Required
使用更多的内存,因为它将总是缓存页面,不管已经缓存了多少其他页面。使用Enabled
,页面将被缓存,但是如果达到缓存大小限制,该状态将被删除。
以下示例显示了如何在后面的代码中设置NavigationCacheMode
属性:
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
this.NavigationCacheMode = NavigationCacheMode.Required;
}
}
通用视窗平台应用和传统视窗桌面应用最大的区别之一是生命周期,这意味着应用在运行时可以呈现不同的状态。通常,传统桌面应用的生命周期非常简单,因为它们只受到运行应用的硬件的限制。用户始终可以控制应用的状态:应用被启动,它一直保持活动状态,直到他关闭它,没有任何限制执行后台操作的机会。
然而,这种方法并不适合在有电池和性能限制的设备上运行的应用,如手机或平板电脑:性能、小电池影响和响应能力是这些平台的关键因素,标准桌面应用提供的自由不尊重这些要求。
通用视窗平台应用并不总是运行的:当用户切换到另一个活动时(比如打开另一个应用,或者回到开始屏幕),应用会被暂停。它的状态保存在内存中,但不再运行,因此不使用任何资源(CPU、网络等)。).因此,当一个应用被挂起时,它不能执行后台操作(即使有异常,这要归功于一个名为扩展执行的功能,我们将在后面详细介绍):为此,通用视窗平台引入了后台任务,这将在本系列的另一本书中详细介绍。在大多数情况下,挂起管理对开发人员来说是透明的:当用户恢复我们的应用时,它将被简单地恢复,同时恢复其状态。这样,用户会发现应用处于与他之前离开时相同的状态。
然而,一些设备(尤其是平板电脑和智能手机)没有无限的内存:因此,操作系统可以在资源耗尽的情况下终止旧的应用。作为开发人员,在挂起期间保存应用的状态非常重要,这样我们就可以在应用被系统终止的情况下恢复它。目标是为用户提供流畅的体验:无论应用是刚刚暂停还是终止,用户都应该始终以他离开时的状态找到应用。
重要的是不要将应用的状态(例如,用户正在填写的表单内容和他不想丢失的内容,即使切换到另一个任务)与应用的数据(例如数据库)混淆:正如我们将在本书的下一章中学习的那样,应用的数据应该在更改后立即保存,以最大限度地减少数据丢失,以防出现问题(例如应用意外崩溃)。
让我们详细看看应用生命周期的不同状态。
所有的通用视窗平台应用都是从一个叫做NotRunning
的基础状态开始的,这意味着应用还没有启动。当应用从这个状态启动时,启动事件被触发,它负责初始化框架和主页。一旦应用被初始化,它将进入Running
状态。
通用视窗平台应用能够管理App
类中的生命周期事件,该类在 App.xaml.cs 文件中定义:具体来说,启动事件称为OnLaunched()
。只有当应用从零开始初始化时才会触发,因为它还没有运行或挂起。
下面的代码显示了一个典型的启动管理,它与我们在讨论后退按钮管理时已经看到的一样:
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
Frame rootFrame = Window.Current.Content as Frame;
// Do not repeat app initialization
when the Window already has content,
// just ensure that the window is
active
if (rootFrame == null)
{
// Create a Frame to act as the
navigation context and navigate to the first page
rootFrame = new Frame();
rootFrame.NavigationFailed += OnNavigationFailed;
if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
{
//TODO: Load state from previously suspended
application
}
// Place the frame in the current
Window
Window.Current.Content = rootFrame;
}
if (e.PrelaunchActivated == false)
{
if (rootFrame.Content == null)
{
// When the navigation stack isn't
restored navigate to the first page,
// configuring the new page by
passing required information as a navigation
// parameter
rootFrame.Navigate(typeof(MainPage), e.Arguments);
}
// Ensure the current window is
active
Window.Current.Activate();
}
}
前面代码最重要的部分是当我们检查PreviousExecutionState
属性的值时,这是事件参数提供的属性之一。根据应用的先前状态,此属性可以采用不同的状态。通常,在启动事件中,您将能够捕捉到以下状态:
NotRunning
,表示是第一次启动应用。Terminated
,这意味着应用已经在内存中,但由于资源不足,它已经被操作系统终止。ClosedByUser
,这意味着应用已经在内存中,但是已经被用户终止。
作为默认行为,标准的App
类代码建议只管理Terminated
状态:应用已经被操作系统杀死,因此作为开发人员,我们有责任恢复我们之前保存的状态。我们将在本章的后面看到正确的方法。如您所见,另外两种状态(NotRunning
和ClosedByUser
)未被管理:应用未运行或已被用户明确关闭,因此从头开始是正确的,无需恢复任何先前的状态。
预启动是加速应用加载时间的一种方式。当预启动被激活时,Windows 10 可以检测出哪些是你最常用的应用,并预启动它们。在这个阶段(对用户来说是完全不可见的,除非他用任务管理器监控正在运行的进程),应用将能够执行一些操作来加速用户的真正启动,比如加载一些数据。例如,在预发布阶段,新闻应用可以从网络源下载最新的新闻,这样当用户显式打开它时,他就不必等待新闻被加载,但是它们已经在那里了。
11 月更新中增加了预发布,但在周年更新中处理方式发生了变化。在 build 10586 中,默认情况下每个应用都启用了预启动,如果您想退出,您需要在OnLaunched()
方法中检查方法参数的属性PrelaunchActivated
是否设置为true
:在这种情况下,您需要从方法返回,而不执行任何附加操作,如下例所示。
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
if (e.PrelaunchActivated)
{
return;
}
//standard initialization code
}
然而,由于并非所有的应用都能从这种方法中受益,Windows 团队决定在周年更新中默认禁用它:如果开发人员愿意,他们可以选择加入。
要选择加入,您需要调用CoreApplication
类的EnablePrelaunch()
方法(包含在Windows.ApplicationModel.Core
命名空间中),传递true
作为参数。以下是OnLaunched()
方法在基于 SDK 14393 的应用中的样子:
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
Frame rootFrame = Window.Current.Content as Frame;
// Do not repeat app initialization
when the Window already has content,
// just ensure that the window is
active
if (rootFrame == null)
{
// Create a Frame to act as the
navigation context and navigate to the first page
rootFrame = new Frame();
rootFrame.NavigationFailed += OnNavigationFailed;
if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
{
//TODO: Load state from previously suspended
application
}
// Place the frame in the current
Window
Window.Current.Content = rootFrame;
}
CoreApplication.EnablePrelaunch(true);
if (e.PrelaunchActivated == false)
{
if (rootFrame.Content == null)
{
// When the navigation stack isn't
restored navigate to the first page,
// configuring the new page by
passing required information as a navigation
// parameter
rootFrame.Navigate(typeof(MainPage), e.Arguments);
}
// Ensure the current window is
active
Window.Current.Activate();
}
else
{
//initialize
the data of the application
}
}
用黄色突出显示,您可以看到原始代码的变化:就在检查应用是否从预启动时启用之前,我们使用CoreApplication
类启用了该功能。如果PrelaunchActivated
属性为false
,则表示用户已经明确启动了 app,因此我们需要遵循常规流程(即激活当前窗口并触发导航至主页面)。否则,我们处于预启动状态,所以我们可以开始加载一些数据,这些数据在用户真正启动应用时可能会有用。
一旦预启动被终止,应用将被置于暂停状态:因此,在预启动期间,您不能执行长时间运行的操作,否则您的加载操作将在完成之前被取消。
通常,当当前应用不再处于前台时会触发暂停:在手机上,这意味着用户已经启动了另一个应用,或者他已经返回到“开始”屏幕;相反,在桌面上,这意味着用户已经最小化了任务栏中的应用。当这种情况发生时,操作系统会等待 10 秒钟,然后继续暂停应用:这样,如果用户改变主意,回到应用,它会立即恢复。
之后,应用被有效挂起:它将被存储在内存中(因此它将继续使用内存),但它将无法执行任何其他操作,也无法使用 CPU、网络、存储等资源。这样,用户打开的新应用将有机会利用设备的所有资源,这是一个重要的性能优势。
对于每个其他应用的生命周期事件,挂起的事件也在App
类中使用OnSuspending()
方法进行管理,默认情况下,该方法具有以下定义:
private void
OnSuspending(object sender, SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();
// TODO: Save application state and stop any background
activity
deferral.Complete();
}
在周年更新之前,这种方法的主要目的是允许开发人员保存应用的状态:由于我们事先不知道应用是否会被终止,所以我们需要在每次应用暂停时都这样做。从代码中可以看出,通用视窗平台应用的标准模板仍然利用这种方法:处理暂停的默认代码包含在这个事件中。
前面的代码使用了deferral
的概念,这个概念在 Universal Windows 平台中被广泛使用,需要它来管理异步操作。如果你回想一下上一本书中详细介绍的async
和await
模式的基本概念,当我们执行一个异步方法时,编译器设置一种书签,方法执行终止,这样主线程就可以自由地继续管理用户界面和其他资源。当我们处理挂起事件时,这种行为可能会引发一些问题:OnSuspending()
方法可能会在操作完成之前终止。deferral
对象解决了这个问题:在调用Complete()
方法之前,OnSuspending()
方法的执行不会终止。
当然,我们不能用这种变通方法来劫持 Windows 指南,让应用无限期地运行:我们需要几秒钟来保存应用的状态,否则应用将被强制挂起,无论保存操作是否完成。如您所见,时间框架相当短:如前所述,OnSuspending()
方法的目的是保存应用的状态,因此用户看不到标准暂停和终止之间的任何区别。相反,这不是保存应用数据的理想位置。为了实现保存应用状态的目标,您可以利用,例如,我们将在本书的下一章中讨论的设置 API。
然而,11 月的第一次更新和后来的周年更新引入了新的功能(如扩展执行、后台音频回放和单进程后台执行),当应用不在前台时,它不再被暂停。因此,当您的应用在后台运行时,它可能会被挂起,因此不会触发挂起事件。在其中一种情况下,如果您在应用中利用这些新功能之一,并继续在“暂停”事件中保存应用状态,您可能会面临丢失数据的风险。在本章的后面,我将重点介绍周年更新中引入的新功能与过去相比的差异。
恢复过程发生在应用从挂起状态恢复时,但它没有被操作系统终止。这个过程对开发人员来说是完全透明的:因为应用仍然在内存中,所以应用的状态被保留,我们不需要手动恢复它。
然而,App
类提供了一种拦截该事件的方法:由于应用因资源不足而终止,并且不是基于时间限制,如果系统有足够的内存来保持其活动,应用可以长时间保持挂起。因此,当应用恢复时,页面中显示的数据可能不再是最新的。
这就是 resumed 事件的目的:每次应用从挂起状态恢复到未终止状态时都会触发这个事件,我们可以使用它来刷新应用的数据(例如,通过向 web 服务执行新的请求来刷新主页面中显示的新闻列表)。
默认情况下,App
类不管理此事件,因此您需要在类构造函数中手动订阅它,如下例所示:
public sealed partial class App : Application
{
public App()
{
this.InitializeComponent();
this.Resuming += App_Resuming;
}
private void App_Resuming(object sender, object e)
{
//refresh the data
}
}
通用视窗平台提供了一个合同系统,开发者可以使用这个系统将他们的应用集成到操作系统中。因此,通用视窗平台应用可以通过不同的方式启动,而不仅仅是点击开始屏幕上的图标或小块:它可以由共享请求触发,或者因为用户已经通过 Cortana 使用语音命令激活了该应用。在所有这些场景中,应用不是通过启动事件打开的,而是通过特定的激活事件打开的,激活事件通常包含请求的相关信息,这些信息是识别上下文和以正确的方式操作所必需的。
App
类根据触发请求的事件提供了很多激活方法:比如OnFileActivated()
方法就是在我们支持的一个文件打开时触发的。在本系列的另一本书中,我们将详细地看到所有可用的合同和扩展以及相关的激活事件。
关闭事件在用户明确关闭应用时触发:在桌面上,通过点击屏幕右上角的 X 图标来执行此操作;在平板电脑上,通过将应用从屏幕顶部拖到底部;相反,在手机上,当用户从任务切换器关闭它时,它就会被触发,任务切换器是通过长按后退按钮来激活的。
如果你以前有过开发 Windows Phone 的经验,那么 Windows Phone 8.0 和 Windows 10 之间有一个重要的区别。在旧版本的 Windows Phone 中,当您按下应用主页中的后退按钮时,实际上是在终止它。相反,在 Windows 10 中,在每个平台上,按下主页上的“后退”按钮只会将您重定向到“开始”屏幕,但应用只会被暂停,而不会被终止。
了解和理解前面描述的生命周期非常重要,因为如果您的目标是 11 月更新 SDK 或之前的版本,您应该继续利用这种方法。此外,如果您的应用没有使用任何新的后台执行功能,您可以安全地继续采用前面描述的生命周期。
然而,11 月的第一次更新和后来的周年更新增加了一些新功能,稍微改变了应用生命周期的处理方式:扩展执行、后台音频回放和单进程后台执行。本章稍后将讨论扩展执行,而其他两个特性将在本系列的另一本书中详细介绍,届时我们将讨论多媒体应用和后台任务。在这些情况下,即使应用不在前台,它也可以继续运行。因此,暂停和恢复事件可能不再可靠:如果应用在后台运行时暂停或恢复,这两个事件不会被触发。因此,例如,如果您在应用挂起时包含保存应用状态的逻辑,并且 Windows 10 在后台运行时将其挂起,挂起事件将永远不会被触发,因此数据也不会被保存。
为了解决这个问题,周年更新引入了两个你可以处理的新事件:EnteringBackground
和LeavingBackground
。
以下是周年更新中的更新生命周期:
图 26:Windows 10 周年更新中通用 Windows 平台应用的应用生命周期
EnteredBackground
是应用从前台移动到后台时触发的新事件。从周年更新开始,这是一个更好利用的事件来保存您的应用的状态:事实上,无论应用是移动到后台继续运行还是暂停,在任何情况下都会触发此事件。
当应用从前台移动到后台时,两种场景的区别在于:
- 如果应用正在利用一个新的后台运行功能,窗口将只触发
EnteriedBackground
事件。 - 如果应用没有利用这些新功能之一,窗口将首先触发
EnteriedBackground
事件,然后是Suspending
事件。
在这种情况下,如果您在EnteriedBackground
事件中移动状态保存逻辑,您确保它将始终被触发。但是,要采用这种新方法,您需要手动订阅EnterieedBackground
事件,因为默认的 Visual Studio 模板只订阅Suspending
事件。以下是您更新后的App
类的外观:
sealed partial class App : Application
{
public App()
{
this.InitializeComponent();
this.Suspending += OnSuspending;
this.EnteredBackground += App_EnteredBackground;
}
private void App_EnteredBackground(object sender, EnteredBackgroundEventArgs e)
{
var deferral = e.GetDeferral();
//TODO: Save application state and stop any background
activity
deferral.Complete();
}
}
如您所见,就像我们看到的Suspending
事件一样,在这种情况下,由于事件处理程序的参数,我们可以通过调用GetDeferral()
方法来访问延迟,以防我们需要执行异步操作。
该事件与前一个事件相反,当应用从后台移动到前台时触发:在此阶段,用户界面尚不可见,之后应用将立即移动到Running
状态。因此,如果您需要在应用对用户可见之前执行任何操作来准备用户界面,最好利用LeavingBackgroundState
事件,而不是Resuming
或Activating
事件。同样在这种情况下,标准的App
类实现不处理此事件,因此您必须在类构造函数中手动订阅它,如下例所示。
sealed partial class App : Application
{
public App()
{
this.InitializeComponent();
this.Suspending += OnSuspending;
this.LeavingBackground += App_LeavingBackground;
}
private void App_LeavingBackground(object sender, LeavingBackgroundEventArgs e)
{
//prepare the UI
}
}
在本系列的另一本书中,您将了解到通用视窗平台提供了后台任务的概念,后台任务是解决方案的独立项目,由独立的进程执行。它们包含一组即使在应用暂停或根本不运行时也可以执行的操作。后台任务连接到触发器,这是导致任务执行的事件:用户收到推送通知,有来自套接字的传入连接,我们定义的时间间隔已经过去,等等。
周年更新引入了**单进程后台模型的概念:**我们可以利用相同的触发器,但是,它可以由应用本身在App
类中直接管理,而不是在我们应用的单独项目(后台任务)中处理后台代码。我们不会在本章中详细讨论这个主题,因为它与背景任务的概念有严格的联系,这将在本系列的另一本书中描述。
扩展执行是 Windows 10 的新功能之一,它改变了周年更新中应用生命周期的处理方式。事实上,在 Windows 10 之前,无论如何,当一个 Windows Store 应用不再处于前台时,它就会被挂起。在后台执行某些操作的唯一方法是利用后台任务。
相反,Windows 10 引入了扩展执行的概念:当应用被挂起时,我们可以要求操作系统保持它在后台运行。该功能通常在两种情况下有用:完成在前台启动的长时间运行的操作(如同步服务器上的一些数据)或继续跟踪用户的位置。基于这种方法,有两种不同的方法来实现这个特性,即使我们要利用相同的 API。
我们在本章前面已经提到,当应用挂起时,我们有几秒钟的时间来结束所有挂起的操作并保存应用的状态。然而,在某些情况下,这一时间是不够的,并可能导致数据丢失。例如,假设用户已经开始同步操作,并且在某个时候,他收到了一条 WhatsApp 消息,并决定立即回复。在这种情况下,执行同步的应用将在后台移动,如果它不能在 10 秒内完成操作,同步将被简单地中止。
扩展执行可用于请求更多时间,这是根据不同的条件(如可用内存或电池寿命)授予的。由于当用户移动到另一个任务时,我们要求更多的时间,我们需要在App
类的Suspending
事件中执行这个请求:当应用被挂起时,我们要求更多的时间。下面是一个示例代码:
private async void OnSuspending(object sender, SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();
using (var session = new ExtendedExecutionSession())
{
session.Reason =
ExtendedExecutionReason.SavingData;
session.Description = "Upload
Data";
session.Revoked
+= session_Revoked;
var result = await
session.RequestExtensionAsync();
if (result == ExtendedExecutionResult.Denied)
{
UploadBasicData();
}
// Upload Data
await UploadDataAsync(session);
}
deferral.Complete();
}
private void
session_Revoked(object sender, ExtendedExecutionRevokedEventArgs args)
{
//clean up the data
}
扩展执行由ExtendedExecutionSession
类管理,该类属于Windows.ApplicationModel.ExtendedExecution
命名空间
当我们创建一个新的ExtendedExecutionSession
对象时,我们必须处理一些属性:
Reason
属性,是ExtendedExecutionReason
枚举器的值之一。如果我们要求更多的时间来完成操作,那么我们使用SavingData
值。Description
属性,简单来说就是一个字符串,描述我们要做哪种操作。Revoked
,这是当会话被取消(例如,因为系统内存不足)时触发的事件,它给你几秒钟来清理你的数据,这样你就可以让应用保持一致的状态。
以正确的方式配置对象后,我们称之为RequestExtensionAsync()
方法。重要的是要强调,基于可用的资源,Windows 有机会拒绝我们的请求:因此,该方法将返回一个ExtendedExecutionResult
对象,我们可以使用它来了解会话是否被授权。
如果它被拒绝(ExtendedExecutionResult.Denied
),我们仍然必须遵守应用暂停前几秒钟的限制:因此,我们需要实现一个替代解决方案,其完成时间少于允许的时间。例如,在前面提到的同步场景中,我们可以在应用中标记一个尚未执行同步的标志(比如在存储中保存一个值),因此我们需要在应用重新启动时再次执行同步。相反,如果会话已被允许,我们可以继续并执行完整的操作。
在前面的示例中,如果会话被拒绝,我们调用UploadBasicData()
方法(只需要几秒钟就可以完成),否则我们调用完整的UploadData()
方法。
通过这种模式,应用实际上被挂起了:然而,它将无限期地保持这种状态,直到操作完成或扩展会话被撤销。
当我们想让应用保持运行时,另一个常见的场景是后台,即当我们想检测用户的位置时。一个常见的例子是跑步者专用的应用:用户应该能够启动应用,创建新的跑步,然后锁定手机,将其放入口袋并开始跑步。即使手机被锁定,我们也希望应用能够继续跟踪用户的位置,这样当他回家时,他就可以在手机上看到他在跑步过程中遵循的路线和一系列统计数据(如时间、平均速度等)。).
默认情况下,通用 Windows 平台应用不支持这种情况。事实上,锁定设备与在后台移动应用有着相同的后果:它是在暂停状态下移动的,因此,每个正在运行的操作(包括使用地理位置 API 跟踪用户位置的操作)都将被终止。
扩展执行 API 也可以用来处理这种情况。然而,在这种情况下,方法是不同的:实际上,应该在应用启动时请求执行,而不是在暂停时。原因是,在这个场景中,模型是不同的:应用不会无限期地保持暂停状态,但它会有效地继续保持运行状态,就像它仍然在前台一样。
下面的示例显示了这个场景的一个实现,通过在应用主页面的OnNavigatedTo()
方法中请求扩展执行会话:
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
}
protected override async void
OnNavigatedTo(NavigationEventArgs e)
{
using (var session = new ExtendedExecutionSession())
{
session.Reason = ExtendedExecutionReason.LocationTracking;
session.Description = "Turn
By Turn Navigation";
session.Revoked += session_Revoked;
var result = await session.RequestExtensionAsync();
if (result == ExtendedExecutionResult.Denied)
{
//show a warning to the user
}
Geolocator locator = new Geolocator();
locator.PositionChanged += Locator_PositionChanged;
}
}
private void Locator_PositionChanged(Geolocator sender, PositionChangedEventArgs args)
{
//store the new position in the
database
}
private void session_Revoked(object sender, ExtendedExecutionRevokedEventArgs args)
{
//clean up data
}
}
如您所见,从代码的角度来看,API 和要设置的属性是相同的。唯一不同的是,这一次,作为原因,我们使用了ExtendedExecutionReason
枚举器的LocationTracking
值。
如果会话没有被拒绝,我们就没事了:现在,当应用被放在后台时,它将继续正常运行,因此,Geolocator
类的PositionChanged
事件(每次用户从当前位置移动时都会被触发)将继续被触发。我们将在本系列书籍的另一部分看到更多关于Geolocator
类和地理本地化 API 的细节。
如果会话被拒绝,我们没有太多的选择:通常,我们只能向用户显示一个警告,该应用不允许在后台运行,因此任何后台位置跟踪功能都不起作用。
Windows 10 可以一次运行一个后台位置跟踪应用。
测试我们在本章中描述的所有场景可能是一个艰巨的挑战:应用不会按照精确的模式终止,而是由操作系统在资源不足时终止它们。此外,需要强调的是,为了方便调试体验,当调试器连接到正在运行的应用时,不会触发生命周期事件。例如,如果在调试器连接时将应用从前台移动到后台,即使超过了最长时间,挂起也不会发生。因此,Visual Studio 提供了一系列选项,开发人员可以使用这些选项来强制生命周期的各种状态:它们在下拉菜单中可用,该菜单包含在调试位置工具栏中,并且一旦您启动了通用 Windows 平台应用的调试会话,它就会被激活。
图 27:用于强制生命周期状态之一的下拉菜单
标准可用选项包括:
- **挂起:**应用被挂起并保存在内存中。
- **恢复:**应用从暂停状态恢复。
- **挂起关闭:**挂起应用,模拟操作系统终止。如果我们想测试我们是否正确管理了应用的状态,这是我们需要选择的选项。
然而,正如我们将在本系列的其他书中看到的,这个下拉菜单还可以显示其他选项,因为它也有助于测试后台任务。
另一个很难测试的场景是,当应用使用不同于标准启动事件的路径被激活时,比如通过 Cortana 的二级切片、通知或语音命令。为了帮助开发人员测试这些情况,Visual Studio 提供了一个选项,允许调试器启动,但不有效地启动应用。这样,无论使用哪种方式激活应用,调试器都将被连接并准备好捕捉任何错误或帮助我们调试特定问题。这个选项可以在项目的属性中启用(你可以通过在解决方案资源管理器中右键单击项目并选择属性来查看):你可以在调试部分找到它,它被称为不要启动,而是在它启动时调试我的代码。
图 28:启用调试应用而不启动它的选项