2018 年 4 月

第 33 卷,第 4 期

Fluent Design - 使用 Fluent:轻扫手势动作和连接动画

作者 Lucas Haines | 2018 年 4 月

应用开发人员要处理的设备类型越来越多,从普遍使用的智能电话、传统台式机和笔记本电脑,到大幅面触摸屏,甚至再到 HoloLens 等新兴增强现实虚拟平台。Fluent Design System 专为此趋势而设计,并且在不断发展中,以便于开发人员能够更轻松地为混合设备生态系统集成新式功能。Windows 10 Fall Creators Update 平台中新增的 Fluent 功能就是一个很典型的例子。这些功能可确保应用外观非常自然,不管是在什么设备上运行。

结合使用 Fluent UI 功能(如亮度、模糊和深度),可以打造引人入胜的直观用户体验,从而提高用户参与度。不过,不久前,我们仍面临着挑战,无法满足用户充分利用设备的需求。随着 Windows 10 Fall Creators Update 推出,Fluent 现新增以下两项功能,以解决对应两方面的问题:轻扫手势动作和连接动画。

轻扫手势动作

配备有触摸屏的电脑销售量还会继续节节攀升,这一点不足为奇。对于开发人员来说,问题在于如何通过启用对触摸屏电脑以及经典键盘和鼠标都适用的交互模型来提高用户参与度。

答案就是对对象执行命令,这种方法已有数年历史,使用方法为右键单击或点击并按住。虽然这些屡试不爽的方法很好理解,但只需多做一些工作,就可以生成利用设备生态系统各项功能的加速器,让这一切更上一层楼。

输入手势动作就是对触控用户和键盘用户都适用的典型加速器。手势动作是用于发出上下文命令的便捷加速器,通常隐藏在右键单击菜单后面或单独的命令栏上。其他手势动作示例包括悬停按钮、轻扫和键盘加速器。

轻扫发出命令并不是一个新概念。Google Material Design 和 Apple iOS 中都有这种交互模式。它适用于快速会审列表项(例如,滚动删除照片或电子邮件),也适用于显示方案中的最常用命令。使用 XAML 轻扫控件,可以将此强大功能轻松添加到通用 Windows 平台 (UWP) 应用中。

轻扫控件包含显示模式和执行模式这两种行为:

显示模式可以显示一个或多个与选定项关联的命令,以便用户能够从中选择。例如,若有邮件列表,并且要快速启用转发、创建跟进提醒或安排会议,就可以使用显示模式实现这一切,而无需迫使用户在屏幕之间导航。对于忙个不停的用户来说,此模式非常强大。

执行模式可通过简单手势完成命令。最常见且最简单的例子就是删除。如果我不再需要此邮件,并要将它彻底删除,只需一个简单手势(如向左轻扫),即可将此邮件移到回收站中。执行模式并不一定都具有破坏性。另一个典型示例是,轻扫保存。假设在一列中有一个客户列表,选择其中一个客户,就会在右侧看到详细信息窗格。对表单或客户记录进行更改后,用户只需轻扫项,即可保存更改。真是又简单又快速。

不过,需要先为应用设置 SymbolIconSource,然后才能对列表创建轻扫行为。这样一来,就可以非常轻松地访问应用图标。将符号添加到 page.resources 中,这样稍后需要时就可以在代码中轻松添加图标。不管是否实现轻扫,这对于提高代码可读性和在 Visual Studio 中充分利用 IntelliSense 来说都非常重要。代码如下:

<Page.Resources>
  <SymbolIconSource x:Key="ReplyIcon" Symbol="MailReplyAll" />
  <SymbolIconSource x:Key="PinIcon" Symbol="Pin" />
  <SymbolIconSource x:Key="DeleteIcon" Symbol="Delete" />
</Page.Resources>

在资源加载后,我就可以开始对元素实现轻扫手势 API 了。列表视图是用于实现轻扫手势的极常见元素,所以也是我要处理的元素,如图 1 中的代码所示。

图 1:对 ListView 实现轻扫

<ListView x:Name="MainList" Width="400" Height="500">
  <ListView.ItemTemplate>
    <DataTemplate x:DataType="x:String">
      <SwipeControl x:Name="LVSwipeContainer"
                    LeftItems="{StaticResource RevealOptions}"
                    RightItems="{StaticResource ExecuteDelete}">
        <StackPanel Orientation="Vertical" Margin="5">
          <TextBlock Text="{x:Bind}" FontSize="18" />
          <StackPanel Orientation="Horizontal">
            <TextBlock Text="Data Template Font" FontSize="12" />
          </StackPanel>
        </StackPanel>
      </SwipeControl>
    </DataTemplate>
  </ListView.ItemTemplate>
</ListView>

请注意,我在图 1**** 中的 SwipeControl API 内使用了 LeftItems 和 RightItems 属性。LeftItems 决定了在用户向左轻扫时发生什么(在此示例中为 RevealOptions),并在右侧显示命令按钮。

方向项引用的是,在页面资源(包含我的图标源)中的 SwipeItems 内定义的静态资源。我就是在其中将行为模式设置为显示模式或执行模式,如下所示:

<Page.Resources>
  <SwipeItems x:Key="RevealOptions" Mode="Reveal">
    <SwipeItem Text="Reply" IconSource="{StaticResource ReplyIcon}"
      Foreground="White"/>
    <SwipeItem Text="Pin" IconSource="{StaticResource PinIcon}"
      Foreground="White" />
  </SwipeItems>
  <SwipeItems x:Key="ExecuteOptions" Mode="Execute">
    <SwipeItem Text="Delete" IconSource="{StaticResource DeleteIcon}"
      Invoked="SwipeItem_Invoked"
      Background="Red" Foreground="White" />
  </SwipeItems>
</Page.Resources>

至此,我已创建 UI 并确保交互可正常运行。现在,我可以使用 Invoke 属性来设置事件处理程序并处理操作。若要在用户轻扫后处理命令,可通过设置 Invoked 属性,在 page.resources 的 SwipeItems 声明中完成。相关代码如下:

<SwipeItem Text="Delete" IconSource="{StaticResource DeleteIcon}"  
  Invoked="DeleteItem_Invoked"
  Background="Red" Foreground="White" />

在代码隐藏文件中,就可以添加命令处理代码,如下所示:

private void DeleteItem_Invoked(SwipeItem sender, 
  SwipeItemInvokedEventArgs args)
{
  int index = myListView.Items.IndexOf(args.SwipeControl.DataContext);
  myListView.Items.RemoveAt(index);
}

尽管轻扫原点植根于触控设备,但轻扫也非常适用于轨迹板和触笔。我常常发现,使用未启用触控的设备时,需要使用轨迹板来检查轻扫手势。发现可以这样做时,我总是很高兴。默认情况下,Fluent 轻扫手势动作适用于轨迹板。这样一来,用户就可以通过更多方式快速完成任务并继续操作。这就是实现一种输入方式且让所有用户都受益的典型示例。

若要向应用添加轻扫手势,请考虑它是否与应用中的其他行为冲突。是否将 FlipView、Hub 或 Pivot 用作应用的主要内容区域?如果是,这可能不是实现轻扫的最佳位置,因为交互模式非常相似,这就导致对用户实现适当平衡可能会很困难。

如果需要多次重复执行相同动作,轻扫手势就非常实用,但请务必确保动作结果一致。我想不到还有什么比以下情况更糟糕的:用户知道向左轻扫可以收藏项,但在下一个屏幕上却发现相同手势用于删除项。最后提示一下,根据我的经验,当要轻扫的项至少有 300 像素宽时,最适合为触控用户或鼠标用户实现轻扫。

连接动画

目前,许多应用和网站都存在“不流畅”或不合逻辑的切换效果。执行需要导航页面的操作后,用户只会看到新页面。几乎没有任何视觉提示提供上下文或指导,告知用户要做什么、转到何处或有哪些最近更新。借助连接动画,可以为用户暂留上下文并提高参与度,如通过动画效果从点击的列表项转到详细信息页面中的目标位置。也可以在用户导航应用时一直显示用户头像。所有这些都是为了暂留上下文和维持关注度。

添加连接动画是一个渐进的过程。若要为所有页面切换效果实现连接动画,请不要因我而停止。不过,我们都有积压工作 (backlog) 要处理,所以最好是有选择性地分批处理这些工作。若要实现连接动画,必须执行以下两步:

  1. 在源页面上准备动画对象。
  2. 在目标页面上开始播放动画。

在源页面上准备动画,可以设置要在各页面上有动画效果的源对象。应在目标页面上设置相同的图像源,尽管可以对目标页面使用分辨率更低的图像。这样可以减少应用占用的内存。然后,连接动画就可以在这两个图像之间交叉淡入淡出。

连接动画的一般经验法则是,动画应在这两步间隔大约 250 毫秒后开始播放,否则源元素可能会在视图中挂起,最后看起来会很奇怪。借助 Windows 10 随附的隐式动画引擎,如果准备的动画未在三秒内开始播放,系统就会自行处理掉它。

对于此方案,我将使用 ListView,其中包含一些文本和图像,具体代码如图 2 所示。ListView 和 GridView 都专门为连接动画添加了以下两个方法:PrepareConnectedAnimations 和 TryStartConnectedAnimationAsync。

图 2:将 ListView 集合设置为有动画效果

<ListView x:Name="Collection"
                  ItemClick="Collection_ItemClick"
                  ItemsSource="{Binding Source={StaticResource ItemsViewSource}}"
                  IsItemClickEnabled="True"
                  SelectionMode="None"
                  Loaded="Collection_Loaded"
                  Grid.Row="1">
  <ListView.ItemTemplate>
    <DataTemplate x:DataType="local:CustomDataObject">
      <Grid Margin="0,12,0,12">
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="Auto" MinWidth="150" />
          <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <!-- image to be animated-->
        <Image x:Name="ConnectedElement" Source="{x:Bind ImageLocation}"
          MaxHeight="100"
          Stretch="Fill" />
        <StackPanel Margin="12,0,0,0" Grid.Column="1" >
          <TextBlock Text="{x:Bind Title}" HorizontalAlignment="Left"
            Margin="0,0,0,6" />
          <StackPanel Orientation="Horizontal" >
            <TextBlock Text="{x:Bind Popularfor}" />
          </StackPanel>
        </StackPanel>
      </Grid>
    </DataTemplate>
  </ListView.ItemTemplate>
</ListView>

此列表中的关键元素是 DataTemplate 中图像的 x:Name。这是我在创建 ConnectedAnimation 并为目标页面准备它时使用的名称。如果用户单击集合中的项,就会转到新页面。我将准备单击方法运行期间的连接动画。对列表 Collection_ItemClicked 设置的事件处理程序就是我准备导航前 ConnectedAnimation 的位置。PrepareConnectedAnimation 方法需要使用唯一键、项和要添加动画效果的元素的名称。在此方法中,我将动画命名为“ca1”,动画开始播放后就会引用目标页面,如图 3**** 所示。

图 3:PrepareConnectedAnimation 方法

private void Collection_ItemClick(object sender, 
  ItemClickEventArgs e)
{
  var container = 
    collection.ContainerFromItem(e.ClickedItem) as ListViewItem;
  if (container != null)
  {
    _storeditem = container.Content as CustomDataObject;
  var animation = collection.PrepareConnectedAnimation("ca1", _
    storeditem, "ConnectedElement");
  }
  Frame.Navigate(typeof(DestinationPage), _storeditem);
}

SuppressNavigationTransitionInfo 阻止播放默认页面切换动画,以防它干扰连接动画。通过对目标页面使用 OnNavigatedTo 方法,我创建了 ConnectedAnimation,并传入了我在源页面上创建的唯一键(“ca1”)。然后,我调用 TryStart,并传入要添加动画效果的 XAML 图像元素的名称,具体是使用下面的 XAML 代码:

<Image x:Name="detailedIamge" MaxHeight="400" 
  Source="{x:Bind ImageLocation}" />

以及下面的 C# 代码:

ConnectedAnimation imageAnimation =
  ConnectedAnimationService.GetForCurrentView().GetAnimation("ca1");
if (imageAnimation != null)
{
  imageAnimation.TryStart(detailedIamge);
}

这就创建了从 ListView 到目标页面的单向连接动画。我仍需要创建另一个连接动画,以处理后退导航方案。我在 OnNavigateFrom 重写中准备了连接动画,并向它传递唯一键“ca2”,如下面的代码所示:

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
  base.OnNavigatedFrom(e);
  ConnectedAnimationService.GetForCurrentView().PrepareToAnimate("ca2", detailedIamge);
}

ca2 动画是使用 ListView 模板中声明的集合加载方法开始播放,如下所示:

private async void Collection_Loaded(object sender, RoutedEventArgs e)
{
  if (_storeditem != null)
  {
    ConnectedAnimation backAnimation =
      ConnectedAnimationService.GetForCurrentView().GetAnimation("ca2");
    if (backAnimation != null)
    {
      await collection.TryStartConnectedAnimationAsync(backAnimation, _
        storeditem, "ConnectedElement");
    }
  }
}

我要对 ListView 调用异步 TryStart 方法,以确保 ListView 内容在动画开始播放前解除冻结。

对于数据列表或数据密集型视图,通常都会有许多与项关联的次要数据(例如,邮件主题、发件人、日期/时间等)。可以动画呈现这些元素,从而进一步帮助用户。这需要使用 CoordinatedAnimation。为此,我可以对 TryStart 使用两参数重载。

首先,我需要在目标页面上创建显示相应内容的元素。我使用的是内含文本块的堆栈面板,我已将堆栈面板命名为 CoordinatedPanel,如下所示:

<StackPanel x:Name="CoordinatedPanel" Grid.Column="1"
  VerticalAlignment="Top" Margin="20,0">
<TextBlock Text="{x:Bind HeaderText}" Style="{ThemeResource SubheaderTextBlockStyle}"
  Margin="0,0,0,10" />
</StackPanel>

然后,我对 TryStart 使用重载,以引用连接动画和要与之协调的 UI 元素,如下所示:

ConnectedAnimation imageAnimation =
  ConnectedAnimationService.GetForCurrentView().GetAnimation("ca1");
if (imageAnimation != null)
{
  imageAnimation.TryStart(detailedIamge, new UIElement[] { CoordinatedPanel });
}

这样一来,我创建的连接动画和 UI 上的其他任何动画就可以同时运行,不仅有助于用户更快理解上下文,还会让用户体验更有身临其境之感。

简要说明:如果 UI 依赖网络请求,或在准备和开始播放动画之间有任何长时间运行的异步操作,我就会避免使用连接动画。这些情况会导致应用出现明显问题,或造成冲淡动画影响力的延迟。为了弥补这些情况造成的影响,建议提前将资产和图像加载到应用中。

总结

轻扫手势动作和连接动画是实用资源,有助于简化交互、添加视觉对象上下文,并为最终用户打造引人入胜的直观体验。对于轻扫手势动作,交互模式实现起来很容易,并将效率级别提升到一个新高度,将适用范围扩展覆盖到轨迹板和触笔用户。随着用户一次又一次重复相同动作,这些小交互的影响就会逐渐累积起来。

连接动画在用户导航页面时提供视觉对象上下文,同时打造有吸引力的用户体验。从开发人员角度来看,可以在应用中的关键时间点增量应用连接动画。其结果是:最终用户喜爱更统一且更引人入胜的体验,这样可以提高他们的应用使用频率。


Lucas Haines 与 Microsoft XAML 控件团队合作,共同重点研究 Fluent Design System 的设计和 UI 解决方案。他还在 Central Windows Design 工作室工作了三年,在此期间他帮助打造了 Fluent 平台。**

衷心感谢以下 Microsoft 技术专家对本文的审阅:Steven Moyes、Kiki Saintonge
Kiki Saintonge 出生于缅因州的埃尔斯沃思,热衷于视频游戏,尤其是工具和 UI 技术。这样的兴趣爱好引领她来到华盛顿州学习计算机科学。她一直对 Windows 保持着孩童般的好奇心,现致力于为 Microsoft XAML 团队开发最佳控件和用户体验。

Steven Moyes 是 Microsoft 项目经理,工作地点是华盛顿州雷德蒙德,致力于简化和增强 UWP XAML 动画。空闲时,他经常玩视频游戏、阅读或钻研个人编码项目。


在 MSDN 杂志论坛讨论这篇文章