借助适用于 Android 的 Material 运动效果构建精美的过渡

Material Design 是一个用于构建醒目、美观的数字产品的系统。产品团队可以根据一套统一的原则和组件将样式、品牌、交互性和运动效果结合起来,从而发挥最大的设计潜力。

logo_components_color_2x_web_96dp.png

Material 组件 (MDC) 可帮助开发者实现 Material Design。MDC 由 Google 的工程师和用户体验设计师团队打造,提供了数十种精美实用的界面组件,并且支持 Android、iOS、网站和 Flutter。

material.io/develop

什么是适用于 Android 的 Material 运动效果系统?

Material Design 准则中所述,适用于 Android 的 Material 运动效果系统是 MDC-Android 库中的一组过渡模式,它们可以帮助用户了解应用并在应用中导航。

四种主要的 Material 过渡模式如下:

  • 容器转换:用于包含容器的界面元素之间的过渡;通过将一个元素无缝转换为另一个元素,在两个不同的界面元素之间创造可视化的连接。
  • 共享轴:用于具有空间或导航关系的界面元素之间的过渡;让元素在转换时共用 x 轴、y 轴或 z 轴,用以强调元素间的关系。
  • 淡出后淡入:用于彼此之间没有密切关系的界面元素之间的过渡;使用依序淡出和淡入的效果,并会对转入的元素进行缩放。
  • 淡出:用于进入或退出屏幕画面范围的界面元素。

MDC-Android 库为基于 AndroidX Transition 库 (androidx.transition) 和 Android Transition Framework (android.transition) 构建的这些模式提供了过渡类。

AndroidX

  • com.google.android.material.transition 软件包中提供
  • 支持 API 级别 14 及更高级别
  • 支持 fragment 和视图,但不支持 activity 或窗口
  • 包含向后移植的 bug 修复,并在各 API 级别中提供一致的行为

框架

  • com.google.android.material.transition.platform 软件包中提供
  • 支持 API 级别 21 及更高级别
  • 支持 fragment、视图、activity 和窗口
  • bug 修复未向后移植,并且在不同 API 级别中可能有不同的行为

在此 Codelab 中,您将使用基于 AndroidX 库构建的 Material 过渡,这意味着您将主要专注于学习 fragment 和视图。

构建内容

此 Codelab 将指导您使用 Kotlin 在一款名为 Reply 的示例 Android 电子邮件应用中构建一些过渡,以便演示如何使用 MDC-Android 库中的过渡来自定义应用的外观和风格。

我们将提供 Reply 应用的起始代码,您要将以下 Material 过渡整合到该应用中,如下面已完成 Codelab 的 GIF 所示:

  • 容器转换,用于从电子邮件列表向电子邮件详情页面过渡
  • 容器转换,用于从 FAB 向电子邮件撰写页面过渡
  • 共享 Z 轴,用于从搜索图标向搜索视图页面过渡
  • 淡出后淡入,用于在邮箱页面之间过渡
  • 容器转换,用于从电子邮件地址条状标签向卡片视图过渡

所请求的 iframe 的网域 (youtu.be) 尚未列入白名单。

所需条件

  • Android 开发和 Kotlin 方面的基础知识
  • Android Studio(如果尚未安装,请在此处下载)
  • Android 模拟器或设备(可通过 Android Studio 获取)
  • 示例代码(参见下一步)

您如何评价自己在构建 Android 应用方面的经验水平?

新手水平 中等水平 熟练水平

启动 Android Studio

打开 Android Studio 后,您应该会看到一个标题为“Welcome to Android Studio”的窗口。不过,如果这是您第一次启动 Android Studio,请使用默认值完成 Android Studio 设置向导中的步骤。该步骤可能需要几分钟时间才能下载并安装必要的文件,因此您可以让其在后台运行,同时继续执行下一部分的操作。

选项 1:从 GitHub 克隆起始 Codelab 应用

如需从 GitHub 克隆此 Codelab,请运行以下命令:

git clone https://github.com/material-components/material-components-android-motion-codelab.git
cd material-components-android-motion-codelab

选项 2:下载起始 Codelab 应用 ZIP 文件

下载起始应用

起始应用位于 material-components-android-motion-codelab-develop 目录中。

在 Android Studio 中加载起始代码

  1. 在设置向导完成且系统显示 Welcome to Android Studio 窗口后,点击 Open an existing Android Studio project

  1. 转到您安装示例代码的目录,然后选择示例目录以打开项目。
  2. 等待 Android Studio 构建和同步项目,如 Android Studio 窗口底部的 activity 指示器所示。
  1. 此时,由于您缺少 Android SDK 或构建工具,因此 Android Studio 可能会显示一些构建错误(如下所示)。按照 Android Studio 中的说明进行安装/更新,并同步您的项目。如果问题仍未解决,请按照指南使用 SDK 管理器更新工具。

验证项目依赖项

项目需要一个 MDC-Android 库的依赖项。您下载的示例代码应该已经列出了这个依赖项,但我们还是要检查一下相应配置,以确保万无一失。

转到 app 模块的 build.gradle 文件,并确保 dependencies 代码块包含 MDC-Android 的依赖项:

implementation 'com.google.android.material:material:1.2.0'

运行起始应用

  1. 确保设备选择左侧的 build 配置为 app
  2. 按绿色的“Run/Play”按钮以构建并运行该应用。

  1. Select Deployment Target 窗口中,如果您的 Android 设备已列在可用设备列表中,请跳至第 8 步。否则,请点击 Create New Virtual Device
  2. Select Hardware 屏幕中,选择一部手机设备(如 Pixel 3),然后点击 Next
  3. System Image 屏幕中,选择最新的 Android 版本,最好选择最高的 API 级别。如果您尚未安装,请点击随即显示的 Download 链接,然后完成下载。
  4. 点击 Next
  5. Android Virtual Device (AVD) 屏幕上,让各项设置保持不变,然后点击 Finish
  6. 从部署目标对话框中选择一个 Android 设备
  7. 点击 Ok
  8. Android Studio 会构建并部署该应用,然后自动在目标设备上将其打开。

大功告成!Reply 首页的起始代码应该已经开始在您的模拟器中运行了。您应该会看到包含电子邮件列表的收件箱。

可选:将设备动画播放速度放慢

由于此 Codelab 涉及到快速但精细的过渡,因此放慢设备的动画播放速度有助于观察您所实现的过渡的一些更精细的细节。若要实现这一点,您可以使用 adb shell 命令或“快速设置”图块。请注意,这些用于放慢设备动画播放速度的方法也会对设备上除 Reply 应用以外的动画产生影响。

方法 1:ADB shell 命令

若要将设备的动画播放速度放慢至原来的十分之一,您可以从命令行运行以下命令:

adb shell settings put global window_animation_scale 10
adb shell settings put global transition_animation_scale 10
adb shell settings put global animator_duration_scale 10

若要将设备的动画播放速度重新重置为正常速度,请运行以下命令:

adb shell settings put global window_animation_scale 1
adb shell settings put global transition_animation_scale 1
adb shell settings put global animator_duration_scale 1

方法 2:“快捷设置”图块

或者,若要设置“快捷设置”图块,请首先在设备上启用“开发者设置”(如果您之前未启用的话):

  1. 打开设备的“设置”应用
  2. 向下滚动到底部,然后点击“关于模拟设备”
  3. 向下滚动到底部,然后快速点击“build 号”,直到“开发者设置”启用为止

接下来,依然在设备的“设置”应用中,执行以下操作以启用“快捷设置”图块:

  1. 点击屏幕顶部的搜索图标或搜索栏
  2. 在搜索字段中输入“图块”
  3. 点击“快捷设置开发者图块”行
  4. 点击“窗口动画缩放”开关

最后,在整个 Codelab 中,从屏幕顶部下拉系统通知栏并使用 图标,即可在慢速和正常速度播放的动画之间切换。

我们来看一看代码。我们提供了一款应用,该应用使用 Jetpack Navigation 组件库在几个不同的 fragment 之间导航,所有这些 fragment 都在同一个 activity 中,即 MainActivity

  • HomeFragment:显示电子邮件列表
  • EmailFragment:显示单个完整的电子邮件
  • ComposeFragment:让用户能够撰写新电子邮件
  • SearchFragment:显示搜索视图

首先,如需了解如何设置应用的导航图,请在 app -> src -> main -> res -> navigation 目录中打开 navigation_graph.xml

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   android:id="@+id/navigation_graph"
   app:startDestination="@id/homeFragment">

   <fragment
       android:id="@+id/homeFragment"
       android:name="com.materialstudies.reply.ui.home.HomeFragment"
       android:label="HomeFragment">
       <argument...>
       <action
           android:id="@+id/action_homeFragment_to_emailFragment"
           app:destination="@id/emailFragment" />
   </fragment>
   <fragment
       android:id="@+id/emailFragment"
       android:name="com.materialstudies.reply.ui.email.EmailFragment"
       android:label="EmailFragment">
       <argument...>
   </fragment>
   <fragment
       android:id="@+id/composeFragment"
       android:name="com.materialstudies.reply.ui.compose.ComposeFragment"
       android:label="ComposeFragment">
       <argument...>
   </fragment>
   <fragment
       android:id="@+id/searchFragment"
       android:name="com.materialstudies.reply.ui.search.SearchFragment"
       android:label="SearchFragment" />
   <action
       android:id="@+id/action_global_homeFragment"
       app:destination="@+id/homeFragment"
       app:launchSingleTop="true"
       app:popUpTo="@+id/navigation_graph"
       app:popUpToInclusive="true"/>
   <action
       android:id="@+id/action_global_composeFragment"
       app:destination="@+id/composeFragment" />
   <action
       android:id="@+id/action_global_searchFragment"
       app:destination="@+id/searchFragment" />
</navigation>

请注意上述所有 fragment 是如何存在的,其中默认启动 fragment 已通过 app:startDestination="@id/homeFragment" 设为 HomeFragment。fragment 目的地图的这一 XML 定义及相关操作会为生成的 Kotlin 导航代码(您在附加过渡时遇到)提供指示。

activity_main.xml

接下来,我们要看一看 app -> src -> main -> res -> layout 目录中的 activity_main.xml 布局。您将看到使用上面的导航图配置的 NavHostFragment

<fragment
   android:id="@+id/nav_host_fragment"
   android:name="androidx.navigation.fragment.NavHostFragment"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   app:defaultNavHost="true"
   app:navGraph="@navigation/navigation_graph"/>

NavHostFragment 会填充屏幕,并处理应用中的所有全屏 fragment 导航方式变化。BottomAppBar 及其锚定的 FloatingActionButton(同样位于 activity_main.xml 中)会在 NavHostFragment 显示的当前 fragment 上层布局,因此,所提供的示例代码会根据 fragment 定义来显示或隐藏这些元素。

此外,activity_main.xml 中的 BottomNavDrawerFragment 是一个底部抽屉式导航栏,其中包含用于在不同的电子邮箱之间导航的菜单,系统会通过 BottomAppBar Reply 徽标按钮有条件地显示该导航栏。

MainActivity.kt

最后,如需查看所使用的导航操作的示例,请在 app -> src -> main -> java -> com.materialstudies.reply.ui 目录中打开 MainActivity.kt。找到 navigateToSearch() 函数,它应如下所示:

private fun navigateToSearch() {
   val directions = SearchFragmentDirections.actionGlobalSearchFragment()
   findNavController(R.id.nav_host_fragment).navigate(directions)
}

该示例展示了如何在未添加任何自定义过渡的情况下,导航到搜索视图页面。在此 Codelab 中,您将深入了解 Reply 的 MainActivity 和 4 个主要 fragment,以设置能够与整个应用中的各种导航操作协同工作的 Material 过渡。

现在,您已经熟悉了起始代码,接下来,我们要实现第一个过渡。

首先,您将添加在点击电子邮件时显示的过渡。对于这种导航方式变化,容器转换模式非常适用,因为该模式是专为包含容器的界面元素之间的过渡而设计的。这个模式可在两个界面元素之间创建可视化连接。

在添加任何代码之前,请先尝试运行 Reply 应用,然后点击一个电子邮件。此时,该应用应该会进行简单的跳接,也就是说,屏幕会在没有任何过渡的情况下被替换:

首先,在 email_item_layout.xml 中的 MaterialCardView 上添加一个 transitionName 属性,如以下代码段所示:

email_item_layout.xml

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

过渡名称会接受一个带参数的字符串资源。您需要使用每个电子邮件的 ID,以确保 EmailFragment 中的每个 transitionName 都是唯一的。

现在,您已经设置了电子邮件列表项的过渡名称,接下来,我们要在电子邮件详情布局中执行同样的操作。在 fragment_email.xml 中,将 MaterialCardViewtransitionName 设为以下字符串资源:

fragment_email.xml

android:transitionName="@string/email_card_detail_transition_name"

HomeFragment.kt 中,将 onEmailClicked 中的代码替换为以下代码段,以便创建从起始视图(电子邮件列表项)到结束视图(电子邮件详情屏幕)的映射:

HomeFragment.kt

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)

现在,基础已经打好,接下来,您就可以创建容器转换了。在 EmailFragment onCreate 方法中,添加以下代码段,将 sharedElementEnterTransition 设为 MaterialContainerTransform 的新实例(导入 com.google.android.material.transition 版本,而不是 com.google.android.material.transition.platform 版本):

EmailFragment.kt

sharedElementEnterTransition = MaterialContainerTransform().apply {
   drawingViewId = R.id.nav_host_fragment
   duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
   scrimColor = Color.TRANSPARENT
   setAllContainerColors(requireContext().themeColor(R.attr.colorSurface))
}

现在尝试再次运行该应用。

效果开始变好了!当您点击电子邮件列表中的电子邮件时,容器转换应将列表项展开为全屏的详情页面。不过请注意,按返回按钮并不会让展开的电子邮件重新收起到列表中。此外,电子邮件列表会在过渡开始时立即消失,并显示灰色的窗口背景。因此,我们的任务还没有完成。

若要修复“返回”过渡,请将以下两行代码添加到 HomeFragment.kt 中的 onViewCreated 方法:

HomeFragment.kt

postponeEnterTransition()
view.doOnPreDraw { startPostponedEnterTransition() }

尝试再次运行该应用。如果您打开一个电子邮件,然后按返回按钮,打开的电子邮件就会重新收起到列表中。很好!接下来,让我们继续改进动画。

电子邮件列表消失的问题的原因在于,当使用 Navigation 组件转到新的 fragment 时,系统会立即移除当前 fragment,并将其替换为新的传入 fragment。为了让电子邮件列表即使在被替换后仍保持可见状态,您可以向 HomeFragment 添加“退出”过渡。

将以下代码段添加到 HomeFragment onEmailClicked 方法,让电子邮件列表在退出时逐渐缩小,并在重新进入时恢复原状:

HomeFragment.kt

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()
}

接下来,为了确保将 MaterialElevationScale 过渡应用到整个主屏幕(而不是层次结构中的各个视图),请将 fragment_home.xml 中的 RecyclerView 标记为过渡组。

fragment_home.xml

android:transitionGroup="true"

在此阶段,您应该已经实现了一个完全可以正常运行的容器转换。如果您点按某个电子邮件,相应列表项就会展开为详情屏幕,同时,电子邮件列表也会消失。如果您按返回按钮,电子邮件详情屏幕就会重新收起为列表项,同时在电子邮件列表中放大。

接下来,我们要继续设置容器转换,并添加从悬浮操作按钮到 ComposeFragment 的过渡,从而将 FAB 展开为要由用户撰写的新电子邮件。首先,再次运行该应用,然后点击 FAB,以确保启动电子邮件撰写屏幕时没有任何过渡。

虽然我们使用相同的过渡类,但配置此实例的方式会有所不同,这是因为 FAB 位于 MainActivity 中,而 ComposeFragment 位于 MainActivity 导航宿主容器中。

ComposeFragment.kt 中,将以下代码段添加到 onViewCreated 方法,并确保导入 Slideandroidx.transition 版本。

ComposeFragment.kt

enterTransition = MaterialContainerTransform().apply {
   startView = requireActivity().findViewById(R.id.fab)
   endView = emailCardView
   duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
   scrimColor = Color.TRANSPARENT
   containerColor = requireContext().themeColor(R.attr.colorSurface)
   startContainerColor = requireContext().themeColor(R.attr.colorSecondary)
   endContainerColor = requireContext().themeColor(R.attr.colorSurface)
}
returnTransition = Slide().apply {
   duration = resources.getInteger(R.integer.reply_motion_duration_medium).toLong()
   addTarget(R.id.email_card_view)
}

除了用于配置我们先前的容器转换的参数之外,我们要在此处手动设置 startViewendView。必要时,您可以通过手动指定的方式告知 Android 过渡系统应该转换哪些视图,而无需使用 transitionName 属性。

现在,再次运行该应用。您应该会看到 FAB 转换为撰写屏幕(请参阅这一步骤末尾的 GIF)。

与上一步类似,您需要向 HomeFragment 添加过渡,以防止其在被移除并替换为 ComposeFragment 后消失。

在调用 NavController navigate 前,请将以下代码段复制到 MainActivity 中的 navigateToCompose 方法。

MainActivity.kt

currentNavigationFragment?.apply {
   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()
   }
}

现在,这一步骤大功告成了!您应该实现了从 FAB 到撰写屏幕的过渡,如下所示:

在这一步,我们将添加从搜索图标到全屏搜索视图的过渡。由于这项导航方式变化不涉及任何永久性容器,因此,我们可以使用共享 Z 轴过渡来加强两个屏幕之间的空间关系,并指明要在应用的层次结构中上移一级。

在添加其他代码之前,请先尝试运行该应用,然后点按屏幕右下角的搜索图标。这样操作后,系统应该会显示搜索视图页面,但该过程中没有任何过渡。

首先,在 MainActivity 中找到 navigateToSearch 方法,并将以下代码段添加到 NavController navigate 方法调用前方,以设置当前 fragment 的退出和重新进入 MaterialSharedAxis Z 轴过渡。

MainActivity.kt

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

接下来,将以下代码段添加到 SearchFragment 中的 onCreate 方法,以配置其进入和返回 MaterialSharedAxis 过渡。

SearchFragment.kt

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

最后,为了确保将 MaterialSharedAxis 过渡应用到整个搜索屏幕(而不是层次结构中的各个视图),请将 fragment_search.xml 中的 LinearLayout 标记为过渡组。

fragment_search.xml

android:transitionGroup="true"

大功告成!现在,请尝试再次运行该应用,然后点按搜索图标。首页屏幕和搜索视图屏幕应同时沿 Z 轴执行深度淡出/淡入和缩放,从而在这两个屏幕之间形成无缝的过渡效果。

在这一步,我们将在不同邮箱之间添加过渡。由于我们并不想突出空间或层级关系,因此,我们将使用淡出后淡入效果在电子邮件列表之间进行简单的“转换”。

在添加其他代码之前,请先尝试运行该应用,点按底部应用栏中的 Reply 徽标并切换邮箱。电子邮件列表应该会发生变化,但该过程中没有任何过渡。

首先,在 MainActivity 中找到 navigateToHome 方法,并将以下代码段添加到 NavController navigate 方法调用前方,以便设置当前 fragment 的退出 MaterialFadeThrough 过渡。

MainActivity.kt

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

接下来,打开 HomeFragment。在 onCreate 中,将 fragment 的 enterTransition 设为 MaterialFadeThrough 的新实例。

HomeFragment.kt

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

再次运行该应用。当您打开底部抽屉式导航栏并更改邮箱时,当前电子邮件列表应该会淡出并缩小,而新的列表应该会淡入并放大。很好!

在这一步,您将添加一个过渡,以便将条状标签转换为弹出式卡片。此时要使用容器转换,这有助于告知用户,在弹出式窗口中执行的操作将会影响产生相应弹出式窗口的条状标签。

在添加任何代码之前,请先运行 Reply 应用,点击某个电子邮件,点击“reply”FAB,然后尝试点击收件人的联系人条状标签。该条状标签应该会立即消失,并且视图中应该会弹出一个包含相应联系人的电子邮件地址的卡片,但该过程中没有任何过渡。

您将在 ComposeFragment 中完成这一步。ComposeFragment 布局中已添加了多个收件人条状标签(默认可见)和一个收件人卡片(默认不可见)。收件人条状标签和这个卡片是两个视图,而您将在这两个视图之间创建一个容器转换。

首先,打开 ComposeFragment 并找到 expandChip 方法。点击提供的 chip 时,系统会调用该方法。在转换 recipientCardViewchip 可见性的代码行上方添加以下代码段,这会触发通过 beginDelayedTransition 注册的容器转换。

ComposeFragment.kt

val transform = MaterialContainerTransform().apply {
   startView = chip
   endView = binding.recipientCardView
   scrimColor = Color.TRANSPARENT
   endElevation = requireContext().resources.getDimension(
       R.dimen.email_recipient_card_popup_elevation_compat
   )
   addTarget(binding.recipientCardView)
}

TransitionManager.beginDelayedTransition(binding.composeConstraintLayout, transform)

如果您现在运行该应用,条状标签应该会转换为收件人电子邮件地址的卡片。接下来,我们要配置返回过渡,以将卡片重新收起到条状标签中。

ComposeFragmentcollapseChip 方法中,添加以下代码段,以将卡片重新收起到条状标签中。

ComposeFragment.kt

val transform = MaterialContainerTransform().apply {
   startView = binding.recipientCardView
   endView = chip
   scrimColor = Color.TRANSPARENT
   startElevation = requireContext().resources.getDimension(
       R.dimen.email_recipient_card_popup_elevation_compat
   )
   addTarget(chip)
}

TransitionManager.beginDelayedTransition(binding.composeConstraintLayout, transform)

再次运行该应用。如果点击条状标签,该条状标签应该会展开为卡片;如果点击卡片,该卡片应该会重新收起到条状标签中。很好!

借助不到 100 行 Kotlin 代码和一些基本的 XML 标记,MDC-Android 库就可以帮助您在一款符合 Material Design 准则的现有应用中创建出精美的过渡效果,同时让这款应用在所有 Android 设备上保持一致的外观和行为。

后续步骤

如需详细了解 Material 运动效果系统,请参阅相关规范和完整的开发者文档,并尝试向您的应用添加一些 Material 过渡!

感谢您试用 Material 运动效果。希望您喜欢此 Codelab!

我能够以合理的时间和精力完成此 Codelab

非常赞同 赞同 一般 不赞同 非常不赞同

我希望日后继续使用 Material 运动效果系统

非常赞同 赞同 一般 不赞同 非常不赞同