使用 Material Design 组件实现 Material 动效

近期发布的 Material 动效系统MDC-Android 库 (v 1.2.0) 的一部分,它将常用的过渡效果归纳为一组简单的模式,提供更流畅更加容易理解的用户体验。Material 动效目前包括四种过渡效果:

我们已经在 Android 平台AndroidX 过渡系统 实现了以上过渡效果,以便在 Activity、Fragment 和 View 之间切换时轻松使用。

本文会介绍上面每种模式,并解释如何将这些模式应用到您的应用中。我将会通过在示例应用 Reply (一个简单易用的邮件客户端) 中实现对应的效果来说明每个步骤。Reply 应用的三个操作流程会使用到这些过渡动效: 打开邮件打开搜索页面切换信箱

如果您不满足于上手介绍,更希望深入源码,请参阅 Material 动效 Codelab,按步骤上手实践这项技术,Codelab 也提供了在 Android 上使用这些过渡效果的其他信息。

容器转换: 打开邮件

容器转换是过渡的主角,容器转换用在将一个元素转换为另一个元素。什么意思呢?例如示例的一个列表展开成为了详情页、FAB 变形为工具栏,或 chip 扩展为了浮动的 卡片。在每个场景中都有一个组件变换为另一个组件,并以动画方式切换 “内部” 内容,同时维护一个共享的 “外部” 容器。使用容器变换,实现视图间的动画切换,可帮助增强它们之间的联系,并维持一个用户的 导航上下文

在 Reply 示例中,我们在展示邮件列表的 Fragment (HomeFragment) 和邮件详情 Fragment (EmailFragment) 间添加了容器转换。如果您熟悉 Android 共享元素过渡,它与容器转换的设置非常相似。

首先,确定两个共享元素的视图,并为每一个视图添加 过渡名称。第一个是单个邮件列表项的卡片,我们将使用 数据绑定,来确保每一个列表项都有唯一的过渡名称。

android:transitionName="@{@string/email_card_transition_name(email.id)}"

第二个是 EmailFragment 内部的全屏卡片组件,这个组件可以设置一个静态的过渡名称,因为在视图层级中只有这一个视图。注意,两个共享元素不需要使用相同的过渡名称。

这两个视图会被我们的容器转换使用。工作原理是: 它们都会被放在一个 drawable 内部,此 drawable 的边界会被裁剪到 “容器” 中,而 “容器” 会将自己的形状通过动画从一个列表项转换为详情页。在过渡过程中,通过传入页面在传出屏幕上淡入,容器的内容 (列表项和详情页) 发生了交换。

现在我们已经标记了共享元素的视图,接下来就可以创建目的地 Fragment 的 sharedElementEnterTransition,并将其设置给一个 MaterialContainerTransform 的实例。默认情况下,从详情页面返回时,这个 sharedElementEnterTransition 会自动反转并播放。

sharedElementEnterTransition = MaterialContainerTransform().apply {
   //  drawingViewId 是视图的 id,在其上方,容器变换将在 z 轴空间进行
   drawingViewId = R.id.nav_host_fragment
   duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
   // 由于我们也想将列表页面通过动画转换出视图,所以将 scrimColor 设置为透明
   scrimColor = Color.TRANSPARENT
   setAllContainerColors(requireContext().themeColor(R.attr.colorSurface))
}

有关 MaterialContainerTransform 参数的详细信息,请参阅 动效文档

当一封邮件被点击时,我们所有需要做的就是为 Fragment 事务提供开始视图和结束视图过渡名称之间的映射。有了这些信息,邮箱详情 Fragment 共享元素过渡就可以使用我们提供的 MaterialContinaerTransform 找到并在两个视图之间进行动画切换。

override fun onEmailClicked(cardView: View, email: Email) {
     exitTransition = MaterialElevationScale(false).apply {
         duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
     }
     reenterTransition = MaterialElevationScale(true).apply {
           duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
     }
     val emailCardDetailTransitionName = getString(R.string.email_card_detail_transition_name)
     val extras = FragmentNavigatorExtras(cardView to emailCardDetailTransitionName)
     val directions = HomeFragmentDirections.actionHomeFragmentToEmailFragment(email.id)
     findNavController().navigate(directions, extras)
 }

在上面的代码片段中,我们也为传出页邮件 列表 Fragment 设置了 exitreenter 的过渡效果。Material 组件提供了两个过渡辅助: HoldMaterialElevationScale,以平滑地为将要被替换的 Fragment 设置动画。除了褪色 (Fade),MaterialElevationScale 还会在邮件列表页退出时,对其进行缩放,并在重新进入邮件列表时缩放回来。Hold 仅仅是简单地保留邮件列表。如果没有设置退出时的过渡,我们的邮件列表会被立刻删除并从视图中消失。

如果我们在这个时候运行代码,从详情页导航返回到邮件列表页,则返回过渡不会执行。这是因为当过渡开始时,邮件列表的适配器还未被填充,过渡系统找不到与过渡名称对应的两个视图。幸运的是,有两个简单方法可供我们使用: postponeEnterTransition 和 startPostponedEnterTransition。这两个方法允许我们延迟过渡,直到我们知道我们的共享元素已经被布局,并且可以被过渡系统发现。在 Reply 应用中,我们可以使用以下代码延迟过渡,直到我们确定 RecyclerView 适配器已被填充,列表项已和过渡名称绑定:

postponeEnterTransition()
view.doOnPreDraw { startPostponedEnterTransition() }

在您自己的应用中,您可能需要尝试这两种方法,以根据您填充 UI 的方式和时间,来找到合适的时间开始延迟过渡。如果您发现您的返回动画没有执行,可能是在共享元素就绪之前开始了过渡。

接下来进入我们的搜索页面。

共享轴: 打开搜索页面

共享轴模式用于有空间和导航关系的 UI 元素之间的过渡。在 Reply 应用中,打开搜索页面会将用户带到邮件列表顶部的新页面。为了介绍这个三维模型,我们可以在邮件列表 (HomeFragment) 和搜索页面 (SearchFragment) 之间使用共享 z 轴过渡。

共享轴过渡会在操作两个目标的同时创建最终的、编排过的过渡效果。这意味着 “成对” 的过渡会一起运行去创建连续的定向的动画。对 Fragment 来说,这成对的过渡包括:

  • FragmentA 的 exitTransition 和 FragmentB 的 enterTransition

  • FragmentA 的 reenterTransition 和 FragmentB 的 returnTransition

MaterialSharedAxis 是实现了共享轴模式的类,它接收 forward 属性来控制方向性的概念。在每一个过渡配对中,forward 必须被设置为相同的值,以便正确地协调这对动画。

如需了解更多关于共享轴方向性的详细信息,请查阅 动效文档

在 Reply 应用中,这是我们为当前的 Fragment (HomeFragment) 建立退出和重入过渡的方法。

currentNavigationFragment?.apply {
   exitTransition = MaterialSharedAxis(
      MaterialSharedAxis.Z, 
      /* forward= */ true
   ).apply {
      duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
   }
   reenterTransition = MaterialSharedAxis(
      MaterialSharedAxis.Z, 
      /* forward= */ false
   ).apply {
       duration =resources.getInteger(R.integer.reply_motion_duration_large).toLong()
   }
}

在我们目的 fragment (SearchFragment) 中,我们建立进入和返回的过渡。

enterTransition = MaterialSharedAxis(
   MaterialSharedAxis.Z, 
   /* forward= */ true
).apply {
   duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
}
returnTransition = MaterialSharedAxis(
   MaterialSharedAxis.Z, 
   /* forward= */ false
).apply {
   duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
}

注意: 当前 Fragment 的退出过渡和搜索 Fragment 的进入过渡使用相同的 forward 值 - true,当前 Fragment 的重入过渡和搜索 Fragment 的返回过渡也是如此。

接下来,默认情况下,过渡会在场景根层次结构内的所有子视图上运行,这意味着一个共享轴过渡会应用于邮件列表上的每一封邮件以及搜索页面的每一个子视图。如果您想要 “传播” 或者 “错开” 动画,这是一个非常好的功能,但是由于我们需要对每个 Fragment 的根作为整体进行动画处理,我们需要在 邮件列表的 RecyclerView 和我们的 搜索页面的根 view group 设置 android:transitionGroup=“true”

这样,我们就在进出搜索页面时有了一个漂亮的共享 z 轴过渡!共享轴是一个非常灵活的过渡,可以应用于许多不同的场景,从页面过渡到智能回复选择,再到进入或者垂直的步骤流程。您已经配置好了设置,还可以尝试使用 MaterialSharedAxis 的 axis 参数来了解其他轴动画是什么样子。

淡入淡出: 切换邮箱

我们要介绍的最后一个模式是淡入淡出模式。淡入淡出可用于在没有强关系的 UI 元素间过渡。当在两个信箱之间过渡时,我们不希望用户认为他们已经发送的邮件和他们的收件箱在导航上相关。由于每个信箱是一个顶级的目的地,淡入淡出是一个合适的选择。在 Reply 应用中,我们将用不同的电子邮件列表 (带有新参数的 HomeFragment) 替换电子邮件列表 (HomeFragment)。

由于 MaterialFadeThrough 没有方向性,所以设置起来更加简单。我们只需要为传出 Fragment 设置一个退出过渡,为传入 Fragment 设置一个进入过渡。

currentNavigationFragment?.apply {
   exitTransition = MaterialFadeThrough().apply {
       duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
   }
}
enterTransition = MaterialFadeThrough().apply {
   duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
}}

邮件列表的 RecyclerView 上设置 android:transitionGroup=“true” 的需求同样适用于这里,但是我们已经在共享轴配置的步骤中解决了这个问题。

以上就是淡入淡出过渡!您可以在自己项目有趣的地方来使用淡入淡出模式,比如: 底部导航栏的切换、列表项的交换,或替换一个工具栏菜单。

一往无前!

本文简要介绍了 Android 的 Material 动效系统。通过使用该系统所提供的模式,您可以在自定义动效时,做很多事情,使动效成为品牌体验的一部分。本文我们看到了 Fragment 的过渡,但动效系统也可用于 Activity 甚至 View 间的过渡。查看完整的 动效规范 文档,获得更多启发,以便思考哪些地方可提高您应用的核心体验,或在一些小的地方增加额外的乐趣。

继续学习,请查看以下其他资源:

  • Material 动效开发文档: 您可以在 Material Android 动效文档找到许多关于在 Activity 和 View 之间进行动画的自定义选项和建议。
  • Material 动效 Codelab: 一个完整的分步的开发者教程,内容涉及如何在 Reply 应用中添加 Material 动效。
  • Android Google 云盘: 您可以在 Android Google 云盘应用中看到正在运行的动效系统。点击文件夹、打开搜索、在底部导航间切换,这些都用到了 MDC-Android 的过渡效果。