Windows 10 2015 年特别版

第 30 卷,第 11 期

UI 设计 - 适用于 Windows 10 的自适应应用

作者:Clint Rutkas | Windows 2015

通过使用 Windows 10 的通用 Windows 平台 (UWP),应用目前可在各种设备系列上运行,并在平台控件的支持下,在不同大小的屏幕和窗口中实现自动缩放。这些设备系列如何支持用户与您的应用程序之间的交互,您的应用程序如何响应并自适应于其运行所在的设备? 我们探讨这些问题以及 Microsoft 在该平台中提供的工具和资源,方便您不必再为了让应用程序能够运行在不同类型的设备上而编写和维护复杂代码。

让我们先来探讨可以用来优化不同设备系列的 UI 的响应技术。然后,再深入探讨如何使您的应用自适应特定的设备功能。

在深入探讨控件、API 和代码之前,我们先花点时间探讨一下目前谈到的设备系列。简而言之: 设备系列是指一组具有特定外形规格的设备,从 IoT 设备、智能手机、平板电脑和台式电脑到 Xbox 游戏控制台、大屏 Surface Hub 设备,甚至便携式设备。应用将在所有这些设备系列中运行,而在设计您的应用时,考虑将运行该应用的设备系列是一件很重要的事情。

虽然有许多设备系列,UWP 却设计为:让 85% 的 API 可以完全供任何应用访问,不受应用所运行位置的约束。此外,对于前 1,000 个应用,基础通用 Windows API 集中的 API 数量占所有已使用的 API 总数的 96.2%。提供大部分功能且这些功能可作为 UWP 的一部分,而每个设备上专用的 API 可用于进一步定制您的应用。

欢迎回来,Windows

“如何在 Windows 中使用应用”中的一个最大的变化是您已经很熟悉了的:在窗口中运行应用。Windows 8 和 Windows 8.1 允许应用全屏运行,或同时并排显示多达四个应用。相比而言,Windows 10 允许用户随意排列应用、重设应用大小和放置应用。Windows 10 中的新方法为用户提供了更加灵活的 UI,但可能需要您最终做一些工作来优化它。Windows 10 在 XAML 中所做的改进中引入了一些可以在您的应用中实现响应技术的方法,所以无论屏幕或窗口大小如何,看上去都很好。我们来探讨这三种方法。

VisualStateManager 在 Windows 10 中,VisualStateManager 类已扩展为两种机制,用于在基于 XAML 的应用中实现响应式设计。新 VisualState.StateTriggers 和 VisualState.Setters API 允许您定义符合一定条件的视觉状态。通过使用内置 AdaptiveTrigger 作为 VisualState 的 StateTrigger,并设置 MinWindowHeight 和 MinWindowWidth 属性,视觉状态就可以根据应用窗口的高度和宽度进行更改。您还可以对 Windows.UI.Xaml.StateTriggerBase 进行扩展以创建自己的触发器,例如触发设备系列或输入类型。请看一看图 1 中的代码。

图 1 创建自定义状态触发器

<Page>
  <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup>
        <VisualState>
          <VisualState.StateTriggers>
          <!-- VisualState to be triggered when window
            width is >=720 effective pixels. -->
            <AdaptiveTrigger MinWindowWidth="720" />
          </VisualState.StateTriggers>
          <VisualState.Setters>
            <Setter Target="myPanel.Orientation"
                    Value="Horizontal" />
          </VisualState.Setters>
        </VisualState>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    <StackPanel x:Name="myPanel" Orientation="Vertical">
      <TextBlock Text="This is a block of text. It is text block 1. "
                 Style="{ThemeResource BodyTextBlockStyle}"/>
      <TextBlock Text="This is a block of text. It is text block 2. "
                 Style="{ThemeResource BodyTextBlockStyle}"/>
      <TextBlock Text="This is a block of text. It is text block 3. "
                 Style="{ThemeResource BodyTextBlockStyle}"/>
    </StackPanel>
  </Grid>
</Page>

图 1 示例中,页面将显示三个在默认状态下相互间堆叠的 TextBlock 元素。VisualStateManager 具有在 MinWindowWidth 值为 720 时定义的 AdaptiveTrigger,当窗口宽度至少为 720 个有效像素时,它将使 StackPanel 的方向变为水平。这样的话,当用户在手机或平板电脑设备上调整窗口大小或从纵向模式变为横向模式时,就可以更好地利用额外的横向空间。请记住,如果您同时定义了宽度和高度两个属性,则您的触发器只有在同时满足两个条件的情况下才会被触发。您可以探索 GitHub (wndw.ms/XUneob) 上的状态触发器示例,查看使用触发器的更多方案,包括许多自定义触发器的示例。

相对面板图 1 示例中,StateTrigger 用于改变 StackPanel 的 Orientation 属性。XAML 中的许多容器元素与 StateTriggers 相结合,使您能够采用多种方法来控制您的 UI,但却没办法通过它们轻松创建一个元素布置位置彼此相关的复杂且反应灵敏的 UI。这也正是新的 RelativePanel 派上用场的原因。如图 2 所示,您可以通过表达元素间的空间关系,来使用 RelativePanel 布置您的元素。这意味着,您可以轻松使用 RelativePanel 和 AdaptiveTriggers 来创建响应式 UI,您可在其中根据可用的屏幕空间对元素进行移动。

图 2 使用 RelativePanel 表达空间关系

<RelativePanel BorderBrush="Gray" BorderThickness="10">
  <Rectangle x:Name="RedRect" Fill="Red" MinHeight="100" MinWidth="100"/>
  <Rectangle x:Name="BlueRect" Fill="Blue" MinHeight="100" MinWidth="100"
             RelativePanel.RightOf="RedRect" />
  <!-- Width is not set on the green and yellow rectangles.
       It's determined by the RelativePanel properties. -->
  <Rectangle x:Name="GreenRect" Fill="Green"
             MinHeight="100" Margin="0,5,0,0"
             RelativePanel.Below="RedRect"
             RelativePanel.AlignLeftWith="RedRect"
             RelativePanel.AlignRightWith="BlueRect"/>
  <Rectangle Fill="Yellow" MinHeight="100"
             RelativePanel.Below="GreenRect"
             RelativePanel.AlignLeftWith="BlueRect"
             RelativePanel.AlignRightWithPanel="True"/>
</RelativePanel>

需要提醒的是,您使用的带有附加属性的语法中包含了额外的括号,如图所示:

<VisualStateManager.VisualStateGroups>
  <VisualStateGroup>
    <VisualState>
      <VisualState.StateTriggers>
        <AdaptiveTrigger MinWindowWidth="720" />
      </VisualState.StateTriggers>
      <VisualState.Setters>
        <Setter Target="GreenRect.(RelativePanel.RightOf)"
                Value="BlueRect" />
      </VisualState.Setters>
    </VisualState>

您可以在 GitHub (wndw.ms/cbdL0q) 上的响应技术示例中查看其他一些使用 RelativePanel 的方案。

SplitView 应用窗口大小对应用页面的影响不仅仅是对其显示内容的影响;它可能需要导航元素响应窗口本身的大小变化。Windows 10 中引入的新 SplitView 控件通常用于创建顶级导航体验 - 可以根据应用的窗口大小相应地对行为方式进行调整。请记住,虽然这是 SplitView 的常见用例之一,但却不仅限于此用法。将 SplitView 分为两个区域:窗格和内容。

控件上的一些属性可以用来操控呈现。首先,DisplayMode 指定有关内容区域的窗格的呈现方式。有四种可用模式:Overlay、Inline、CompactOverlay 和 CompactInline。图 3 显示了在应用中呈现的 Inline、Overlay 和 CompactInline 模式的示例。

DisplayMode 导航元素
图 3 DisplayMode 导航元素

PanePlacement 属性将窗格显示在内容区域的左侧(默认)或右侧。OpenPaneLength 属性指定窗格完全展开时的宽度(默认为 320 个有效像素)。

请注意,SplitView 控件不包括用户用以触发面板状态的内置 UI 元素,如经常出现在移动应用中的“汉堡”菜单。如果您想展示这一行为,则必须在您的应用中定义此 UI 元素,并提供触发 SplitView 的 IsPaneOpen 属性的代码。

是否想探索 SplitView 提供的所有功能? 务必查看 GitHub (wndw.ms/qAUVr9) 上的 XAML 导航菜单示例。

引入后退按钮

如果您开发适用于 Windows Phone 早期版本的应用,您可能会习惯于在每台设备上设有一个硬件或软件后退按钮,让用户在您的应用中可以以后退的方式导航。然而,对于 Windows 8 和 8.1,您必须创建您自己的能够实现后退导航的 UI。如果针对的是 Windows 10 应用中的多个设备系列,为了实现起来更加容易,有一种方式可以确保为所有用户提供一致的后退导航机制。这有助于在您的应用运行过程中释放一些 UI 空间。

若要为您的应用启用系统后退按钮,可以使用 SystemNavigationManager 类中的 AppViewBackButtonVisibility 属性,即使是在没有硬件或软件后退按钮的设备系列上(如笔记本电脑和台式电脑)也是如此。只需将 SystemNavigationManager 用于当前视图,并将后退按钮设置为可见,如以下代码所示:

SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility =
  AppViewBackButtonVisibility.Visible;

SystemNavigationManager 类还展示了一个 BackRequested 事件,当用户调用系统提供的用于后退导航的按钮、手势或语音命令时,该事件就会被触发。这意味着,您可以处理这个单一事件,从而能够在所有设备系列上一致地在您的应用中执行后退导航。

Continuum 的优点

最后,但并非最不重要的是,我们要提到我们的一个个人收藏夹,供您参考: Windows 10 上的 Continuum。使用 Continuum,Windows 10 可以根据您想要执行的事项和执行的方式调整您的体验。例如,如果您的应用运行在一个二合一的 Windows 电脑上,则在您的应用中实施 Continuum 可以让用户通过触摸或使用鼠标和键盘来优化生产力。通过使用 UIViewSettings 类中的 UserInteractionMode 属性,您的应用仅需借助一行代码即可确定用户是通过触摸与视图实现交互还是通过使用鼠标和键盘实现:

UIViewSettings.GetForCurrentView().UserInteractionMode;
// Returns UserInteractionMode.Mouse or UserInteractionMode.Touch

在检测了交互模式之后,您可以优化应用的 UI,比如增加或减少边距、显示或隐藏复杂的功能等等。请查看 Lee McPherson 的 TechNet 文章“Windows 10 Apps: Leverage Continuum Feature to Change UI for Mouse/Keyboard Users Using Custom StateTrigger”(Windows 10 应用:利用 Continuum 功能为使用自定义 StateTrigger 的鼠标/键盘用户更改 UI)(wndw.ms/y3gB0J),此文章说明如何结合使用新的 StateTriggers 和 UserInteractionMode 构建您自己的自定义 Continuum StateTrigger。

自适应应用

能够响应屏幕尺寸和方向变化的应用是很有用的,但为了实现受人关注的跨平台功能,UWP 为开发人员提供了另外两种类型的自适应行为:

  • 通过检测可用的 API 和资源,使自适应应用的版本响应不同版本的 UWP。例如,在继续支持尚未升级的客户的同时,您可能想让您的应用使用一些较新的 API,而这些 API 仅存在于运行最新版本 UWP 的设备上。
  • 平台自适应应用可以对不同设备系列提供的独特功能做出响应。因此,可以构建出可在所有设备系列上运行的应用,但当它在智能手机之类的移动设备上运行时,您可能想要使用一些特定于移动的 API。

如前所述,在使用 Windows 10 的情况下,绝大多数 UWP API 都完全可供任何应用使用,无论这些应用在什么样的设备上运行。并且,与每个设备系列相关的特定 API 使开发人员可以进一步调整他们的应用。

自适应应用的基本思维模式是,您的应用会检查它所需要的功能(或特性),并且只在该功能可用的情况下才使用。在过去,应用会检查 OS 版本,然后再调用与该版本相关的 API。有了 Windows 10,您的应用就可以在运行时检查某个类、方法、属性、事件或 API 协定是否受当前操作系统的支持。如果支持,该应用将调用相应的 API。位于 Windows.Foundation.Metadata 命名空间中的 ApiInformation 类包含一些静态方法(如 IsApiContractPresent、IsEventPresent 和IsMethodPresent),它们可用于查询 API。例如:

using Windows.Foundation.Metadata;
if(ApiInformation.IsTypePresent("Windows.Media.Playlists.Playlist"))
{
  await myAwesomePlaylist.SaveAsAsync( ... );
}

此代码有两个作用。对是否存在 Playlist 类进行运行时检查,然后在该类的一个实例调用 SaveAsAsync 方法。还要注意的是,通过使用 IsTypePresent API,可以轻松检查当前操作系统上是否存在某个类型。在过去,这样的检查可能需要使用 LoadLibrary、GetProcAddress、QueryInterface、Reflection,或使用“动态”关键字等,具体取决于语言和框架。在进行方法调用时,还要注意强类型引用。当使用任何 Reflection 或“动态”时,您无法使用静态编译时诊断,该诊断可能会告知您是否拼错了方法名等。

使用 API 协定进行检测

API 协定本质上是一个 API 集。假设 API 协定可代表一个 API 集,其中包含两个类、五个接口、一个结构、两个枚举等。我们将逻辑相关类型编组到一个 API 协定中。在许多方面,一个 API 协定代表一个功能 - 一个相关的 API 集可以一起提供某些特殊功能。Windows 10 往前的每个 Windows 运行时 API 都是某个 API 协定的成员。msdn.com/dn706135 上的文档介绍各种可用的 API 协定。您会看到,它们大多表示一组功能相关的 API。

使用 API 协定还为像您一样的开发人员提供一些其他保障;最为重要的是,当平台可实现某个 API 协定中的任一 API 时,该平台一定可以实现该协定中的每个 API。换句话说,一个 API 协定就是一个原子单元,测试是否支持该 API 协定就相当于测试是否支持该集中的每一个 API。您的应用可以调用已检测的 API 协定中的任何 API,而不必逐个检查每个 API。

最大和最常用的 API 协定是 Windows.Foundation.UniversalApiContract。它包含了通用 Windows 平台中几乎所有的 API。如果您想查看当前 OS 是否支持 UniversalApiContract,您可以编写以下代码:

if (ApiInformation.IsApiContractPresent(
  "Windows.Foundation.UniversalApiContract"), 1, 0)
{
  // All APIs in the UniversalApiContract version 1.0 are available for use
}

目前 UniversalApiContract 唯一现存的版本是 1.0 版,因此该检查略显多此一举。但 Windows 10 的未来更新版本中可能会引入其他 API,可能出现包括新的通用 API的 UniversalApiContract 2.0 版本。在将来,如果某个应用希望能在所有设备上运行,并且还希望使用新的 2.0 版本的 API,可以使用以下代码:

if (ApiInformation.IsApiContractPresent(
  "Windows.Foundation.UniversalApiContract"), 2, 0)
{
  // This device supports all APIs in UniversalApiContract version 2.0
}

如果您的应用只需要从 2.0 版本调用单个方法,可以直接使用 IsMethodPresent 来检查此方法。在这种情况下,您可以使用您认为是最简单的任何方法。

除了 UniversalApiContract 之外,还有其他的 API 协定。大多数表示一个功能或一个 API 集:不是普遍存在于所有 Windows 10 平台上,而是存在于一个或多个特定设备系列上。正如前面所提到的,您不再需要检查设备的特定类型,然后推断是否支持 API。只需检查您的应用要使用的 API 集。

现在我可以重写我原来的示例,用以检查是否存在 Windows.Media.Playlists.PlaylistsContract,而不是仅仅检查是否存在 Playlist 类:

if(ApiInformation.IsApiContractPresent(
  "Windows.Media.Playlists.PlaylistsContract"), 1, 0)
{
  // Now I can use all Playlist APIs
}

无论您的应用何时需要调用所有设备系列中并不存在的 API,您都必须添加一个对定义该 API 的相应扩展 SDK 的引用。在 Visual Studio 2015 中,前往“添加引用”对话框,然后打开“扩展”选项卡。在那里,您可以找到三个最重要的扩展: 移动扩展、桌面扩展以及 IoT 扩展。

但是,您的应用需要做的只是,检查所需 API 协定是否存在,并有条件地调用相应的 API。没有必要担心设备的类型。现在的问题是: 我需要调用 Playlist API,但它不是一个普遍可用的 API。文档 (bit.ly/1QkYqky) 会告诉我此类在哪个 API 协定中。但哪个扩展 SDK 会定义它呢?

事实证明,Playlist 类(目前)仅在桌面设备上可用,而不适用于移动设备、Xbox 和其他设备系列。所以,您必须在任何原有代码进行编译之前将引用添加到桌面扩展 SDK 中。

Lucian Wischik 是 Visual Studio 团队的成员和 MSDN 杂志的偶有撰稿人,他创建了一个可提供帮助的工具。该工具会在调用特定于平台的 API 时分析您的应用代码,验证是否完成了与此相关的自适应检查。如果未完成任何检查,分析器就会发出警告,并提供便捷的“快速修复”项以将正确的检查插入到代码中,只需按下“Ctrl+Dot”或单击灯泡图标即可进行。(请参阅 bit.ly/1JdXTeV,以了解详细信息。) 分析器也可以通过 NuGet (bit.ly/1KU9ozj) 进行安装。

我们来通过查看有关 Windows 10 自适应编码的更完整示例进行总结。首先,以下是一些未正确自适应的代码:

// This code will crash if called from IoT or Mobile
async private Task CreatePlaylist()
{
  StorageFolder storageFolder = KnownFolders.MusicLibrary;
  StorageFile pureRockFile = await storageFolder.CreateFileAsync("myJam.mp3");
  Windows.Media.Playlists.Playlist myAwesomePlaylist =
    new Windows.Media.Playlists.Playlist();
  myAwesomePlaylist.Files.Add(pureRockFile);
  // Code will crash here as this is a Desktop-only call
  await myAwesomePlaylist.SaveAsAsync(KnownFolders.MusicLibrary,
    "My Awesome Playlist", NameCollisionOption.ReplaceExisting);
}

现在,我们来看同样的代码,我们添加一行用来验证目标设备上是否支持可选 API 的代码行,然后再调用方法。这样做可以防止发生运行时故障。请注意,您可能想进一步利用这个示例,在您的应用检测到设备上不支持播放列表功能时,不显示调用 CreatePlaylist 方法的 UI:

async private Task CreatePlaylist()
{
  StorageFolder storageFolder = KnownFolders.MusicLibrary;
  StorageFile pureRockFile = await storageFolder.CreateFileAsync("myJam.mp3");
  Windows.Media.Playlists.Playlist myAwesomePlaylist =
    new Windows.Media.Playlists.Playlist();
  myAwesomePlaylist.Files.Add(pureRockFile);
  // Now I'm a safe call! Cache this value if this will be queried a lot
  if (Windows.Foundation.Metadata.ApiInformation.IsTypePresent(
    "Windows.Media.Playlists.Playlist"))
  {
      await myAwesomePlaylist.SaveAsAsync(
        KnownFolders.MusicLibrary, "My Awesome Playlist",
        NameCollisionOption.ReplaceExisting);
  }
}

最后,以下是一个代码示例,看上去可以访问许多移动设备上显示的专用照相机按钮:

// Note: Cache the value instead of querying it more than once
bool isHardwareButtonsAPIPresent =
  Windows.Foundation.Metadata.ApiInformation.IsTypePresent(
  "Windows.Phone.UI.Input.HardwareButtons");
if (isHardwareButtonsAPIPresent)
{
  Windows.Phone.UI.Input.HardwareButtons.CameraPressed +=
    HardwareButtons_CameraPressed;
}

请注意检测步骤。如果我在使用台式电脑时,对 CameraPressed 事件直接引用了 HardwareButtons 对象,而没有检查 HardwareButtons 是否存在,这可能会导致我的应用发生故障。

还有许多关于 Windows 10 中响应式 UI 和自适应应用的内容。您是否想了解更多? 请查看在 Build 2015 会议 (wndw.ms/IgNy0I) 上 Brent Rector 做出的有关 API 协定的精彩演讲,并且务必要观看内容详尽的有关自适应代码的 Microsoft Virtual Academy 视频 (bit.ly/1OhZWGs),视频中涵盖了本主题的更多详细信息。


Clint Rutkas是一位专注于开发人员平台的 Windows 资深产品经理。他在 Microsoft 的“Halo at 343 Industries”和第 9 频道从事工作,并构建出一些使用 Windows 技术的疯狂项目,如计算机控制的迪斯科舞池、一个自定义的 Ford Mustang、T 恤射击机器人等。

Rajen Kishna目前在华盛顿雷德蒙德担任 Microsoft 的 Windows 平台开发人员市场营销团队的资深产品营销经理。此前,他在荷兰担任 Microsoft 的顾问和技术推广员。

衷心感谢以下 Microsoft 技术专家对本文的审阅: Sam Jarawan、Harini Kannan 和 Brent Rector