Android 11 中的新功能之一是可以让应用在对于屏幕上的软键盘打开和关闭的过程创建无缝过渡的动画效果,这一功能源自 Android 11 中对 WindowInsets API 的大量改进。
在 Android 11 上有两个针对该功能的例子——这个功能已经被集成到 Google Search 应用和 Messages 应用中了:
两个 Android 11 中软键盘动画效果的示例: Google Search 应用 (左),Messages (右)
让我们来看看如何在您的应用中添加这种用户体验。总共分为三步:
上面的每一步都环环相扣,所以我们会在不同的文章中分别介绍。在这个系列的第一部中,我们会介绍如何实现边到边,以及 Android 11 中相关 API 的改动。
去年我们介绍了一个关于实现 "边到边" 的概念,这个方法可以让应用深度利用 Android 10 的手势导航: 开启全面屏体验 | 手势导航 (一)。
简单回顾一下,实现 "边到边" 会让您的应用渲染在系统状态栏的后面,如上图所示。
引用去年我自己的话:
实现从边到边的全面屏体验后,系统栏会覆盖在应用内容前方。应用也得以通过更大幅面的内容为用户带来更具有冲击力的体验。
其实,实现边到边不单单只是在状态栏和导航栏之后渲染。应用本身需要开始负责处理那些跟应用重叠的系统 UI 的部分。
正如我们前面提到的,两个最直观的例子是状态栏和导航栏。除此之外还有软键盘,有时候也叫 IME (输入法编辑器),这是另外一个我们需要了解的系统 UI 。
如果我们回想 去年的介绍,实现边到边可以分为三步:
我们会跳过第一步,因为从去年至今这个部分没有改动。教程中的第二步和第三步有一些针对 Android 11 的改动,让我们来看一下。
在以往的第二步中,应用需要使用 systemUiVisibility API 以及一些参数来设置全屏布局:
view.systemUiVisibility =
// 通知系统,视窗希望在极端的情况下该如何布局内容。查看文档来获取更具体的信息。
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
// 通知系统,视窗希望在导航栏被隐藏的情况下如何布局内容。
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
如果您的项目设置编译的目标 SDK 版本已经升级为 30 并且使用这个 API ,您会发现这些 API 都已经被标示为弃用了。
它们已经被 Window 的一个叫作 setDecorFitsSystemWindows()
的函数替代了:
// 通知视窗,我们(应用)会处理任何系统视窗(而不是 decor)
window.setDecorFitsSystemWindows(false)
// 或者您可以使用 AndroidX v1.5.0-alpha02 中的 WindowCompat
WindowCompat.setDecorFitsSystemWindows(window, false)
取代那些参数的是一个布尔值 false,它的意思是应用会处理任何系统窗口的适配 (换句话说就是全屏)。
在 WindowCompat 中,我们还有一个 Jetpack 版本的该函数,androidx.core 库的 v1.5.0-alpha02 版本里也包含了这个函数。
以上就是第二步的改动。
现在让我们来看一下第三步: 避免与系统 UI 产生重叠,也可以说是使用视窗边衬区来决定如何移动应用的内容来避免与系统 UI 的冲突。在 Android 系统中,边衬区可以通过 WindowInsets 类和 AndroidX 中的 WindowInsetsCompat 来访问。
如果我们查看 API 30 以前版本的 WindowInsets,最常用的边衬区类型是系统视窗边衬区。这些边衬区包括了状态栏、导航栏以及打开时的软键盘。
为了使用 WindowInsets,您通常需要在一个视图上添加 OnApplyWindowInsetsListener,并且在这个函数中处理传进来的边衬区:
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
v.updatePadding(bottom = insets.systemWindowInsets.bottom)
// 返回边衬区,这样它们才能够继续在视图树中继续传递下去
insets
}
在这个例子中,我们获取到 系统视窗边衬区,然后更新视图的内边距,这是一个常见的应用场景。
还有一些其他类型的边衬区,比如 Android 10 最近新增的手势边衬区:
ViewCompat.setOnApplyWindowInsetsListener(v) { view, windowInsets ->
val sysWindow = windowInsets.systemWindowInsets
val stable = windowInsets.stableInsets
val systemGestures = windowInsets.systemGestureInsets
val tappableElement = windowInsets.tappableElementInsets
}
和 systemUiVisibility API 类似,许多 WindowInsets API 已经被弃用了,取而代之的一些新函数来查询不同类型的边衬区:
我们刚刚多次提到 "类型",它们在 WindowInsets.Type 类中被定义为函数,每个函数都会返回一个整数标示。我们稍后还会展示如何使用 OR 位运算来查询结合到一起的类型。
所有这些 API 都已经被添加到 AndroidX Core 中的 WindowInsetsCompat,并且向前兼容到 API 14 (请查看 发行注记 来获取更多信息)。
再来看如果我们用新的 API 来更新之前的示例,它们就变成:
ViewCompat.setOnApplyWindowInsetsListener(...) { view, insets ->
- val sysWindow = insets.systemWindowInsets
+ val sysWindow = insets.getInsets(Type.systemBars() or Type.ime())
- val stable = insets.stableInsets
+ val stable = insets.getInsetsIgnoringVisibility(Type.systemBars())
- val systemGestures = insets.systemGestureInsets
+ val systemGestures = insets.getInsets(Type.systemGestures())
- val tappableElement = insets.tappableElementInsets
+ val tappableElement = insets.getInsets(Type.tappableElement())
}
这会儿那些敏锐的