[译] Android 11 中的窗口衬件(inset)和键盘动画教程

在本教程中,你将了解 Android 11 中的窗口衬件和键盘动画,以及如何将这些功能添加到你的 Android 应用中。

作者:Carlos Mota;日期:2020-12-02

在 Android 11 之前,键盘和 Android 系统似乎走向了相反的方向。你无法通过查询 API 来了解键盘是否被打开,也无法知道它的大小。当键盘弹出时,屏幕和视图会自动重新排列,而且没有平滑的过渡。

开发者曾经用复杂的逻辑手动处理这一切,既费时又费力。为了克服这些问题,Android 11 引入了专注于窗口衬件和键盘动画的新功能。

在本教程中,你将学习:

  • 关于窗口衬件和键盘;
  • Android 11 中的新功能,以及旧版 API 中可用于处理键盘的内容;
  • 键盘动画
  • 在 Android 11 中与键盘交互。

起步

……(此处译者省略一部分原文中有关 Android 工程结构的介绍)

了解窗口衬件

在 Android 系统上,窗口视图可以分为两类:你的应用程序的部分和 Android 操作系统的部分。窗口衬件是屏幕中与系统 UI 交互的各个部分,比如状态栏、导航栏以及用于新的系统手势的横向导航部分。

为了避免在已经被系统定义为导航区的区域上定义用户操作,结果使其无法使用,你可以使用一组衬件来定位这些区域,并根据你的应用程序的规格来控制它们。下面是一些可用的衬件:

  • 系统窗口衬件
  • 可触摸元素衬件
  • 手势衬件
  • 稳定性衬件

在本教程中,你将使用系统窗口衬件。要了解更多关于窗口衬件的信息,请查看《Android 手势导航教程》。

注意:Android 在 API 20 中引入了 WindowInsets。如果你的目标是低版本,你应该使用 AndroidX 的 WindowInsetsCompat 类,它是你在整个教程中要使用的类。

接下来,你将熟悉键盘。

了解键盘

到目前为止,还没有一个直接的 API 可以用来访问键盘和检索有关其状态的信息。收集这些信息是一项复杂的计算和推理工作,如果不是猜测的话。

不得不手动实现一切要获得键盘当前状态信息的日子已经一去不复返了。随着 Android 11 的推出,开发者有了一套新的功能,可以对键盘的出现或消失进行动画处理,以及用户如何与之交互。

更妙的是,Android 系统将这些功能中的大部分移植回之前的 Android 版本。读完本教程后,你可以回到你的项目中,删除数百行的键盘处理相关代码。

旧版的 API 上有什么可用

以下功能在 AndroidX 的 appcompat 库所支持的所有 Android 版本上都可用。

启动键盘

你可以使用两个不同的 API 来启动键盘。

  • requestFocus:通过在 EditText 或其他让用户输入文本的组件上调用此方法来启动键盘;
  • windowInsetsController:通过这个 API,你可以手动强制键盘显示:

    或者如果你想关闭它,就用相应的隐藏方法:

    在这两个调用中,你对于想要访问的 WindowInsetsCompat —— 在这儿是键盘(或叫 IME)—— 使用相应的 WindowInsetsController,从而可以打开或关闭之。

注意:你完全可以使用这些 API 取代 InputMethodManager API 来控制你的键盘。

检查键盘是否可见

要查看键盘是否打开,你可以调用:

WindowInsetsCompat.Type.ime() 定义了要访问的衬件的类型。

获取键盘高度

你猜中了!你还可以依靠 WindowInsetsCompat 来获得键盘高度。只需调用:

这个底部值与键盘的可见性直接相关。如果键盘是可见的,它返回其高度。如果不是,这个值就是 0。

现在你已经回顾了旧的 API 能做什么,现在是时候看看 Android 11 中的新内容了。

Android 11 的新内容

除了新方法,Android 11 还引入了一组功能,着重于键盘动画以及视图如何与之互动。

在深入研究之前,……,可以看一下打开键盘(跟视图的变化)是多么的不同步:

https://koenig-media.raywenderlich.com/uploads/2020/11/brain_dump_android10_open_keyboard.gif

为键盘动画准备应用

要使键盘或 IME 和周围的 UI 产生动画效果,需要将应用程序设置为全屏,因为 IME 是系统 UI 的一部分。在 Android 11 之前,可以通过以下方式来实现:

但这个 API 现在已经废弃了。这是一件好事,因为要找到正确的标志组合总是很困难。取而代之的是,Android 通过 WindowCompat 向以前的 Android 版本后向移植了一个方法以实现同样的行为。

到 MainActivity.kt 中,在调用 super.onCreate(savedInstanceState) 之前,添加:

Android Studio 会提示需要两个导入。因此,导入 androidx.core.view.WindowCompatcom.raywenderlich.android.braindump.isAtLeastAndroid11

在这里,第二个参数,!isAtLeastAndroid11(),定义了应用程序是否会处理系统窗口。如果设备运行 Android 11 或更新版本,这个值将是 false,这样应用程序就可以定义键盘动画。在较低的版本中,由于这些功能不可用,该值将为 true,所以系统会控制它们。

为了理解这些差异,请在一个版本低于 Android 11 的设备上编译运行。

看上去一切都很好。如果你把它运行在 Android 11 的设备上呢?

可以看到,因为你的应用要求占据整个屏幕,所以 UI 重叠到了系统栏上。而且,它说的它会照顾到系统窗口也并未兑现。

所以,是时候了 :]

处理系统窗口

setDecorFitsSystemWindows 仅设置于 Android 版本 11 或更高,所以,打开 RWCompat11.kt 来更新 setUiWindowInsets

当提示需要导入时,加入以下内容:

setUiWindowInsets 业已声明,需要添加内容。

以下为此逻辑的分步拆解:

  1. 声明两个全局量,因为后面要在一个处理键盘动画的方法中使用它们;
  2. 要保证与之前 Android 版本的兼容性,因此优先考虑 appcompat API;
  3. 首次运行时两量皆空,需要赋值。由于输入栏应该在导航 UI 之上,而工具栏应该在状态栏之下,所以要检查这两个衬件的边距,并相应更新其容器的边距;
  4. 根据前面定义的值,收到的容器与活动的根视图相对应。你用它们来更新视图的底部和顶部边缘。有了这个,就不会有组件被叠加了。

注意:你需要在 setOnApplyWindowInsetsListener 中计算 postToppostBottom。否则当你查询 systemBars 的内嵌时,你可能会收到 0 作为顶部和底部的边距。不能保证视图在这个监听器之外就能准备好。

现在,你重新安排了用户界面,使其在屏幕限制范围内,点击编译并运行该应用程序。

一切完美 – 干得漂亮! :]

注意:想了解更多关于窗口嵌入和手势导航的信息?请看这个 Android 的手势导航教程

现在,UI 跟窗口合拍了,是时候对键盘进行动画处理了。

让键盘动画起来

这就要使用 WindowInsetsAnimationCallback 了。

这个 API 仅在 Android 11 及以上可用。到 RWCompat11.kt 中更新 animateKeyboardDisplay 如下:

以下是逻辑拆解:

  1. setWindowInsetsAnimationCallback 仅在 Android R 上可用。尽管 RWCompat11.kt 只包含目标为此 API 级别的代码,但加上注解以知会其他程序员需要检查一下设备对此调用是否支持是个好习惯;
  2. 这儿可以使用两种模式:DISPATCH_MODE_CONTINUE_ON_SUBTREE 和 DISPATCH_MODE_STOP。在此场景中使用后者是因为动画发生于父一级,而且也不需要将此事件传递到视图层次中的其他层级。
  3. 在此情况下,使用 onProgress 以更新 UI。另一些方法可以在其他情况下使用,稍后会有更多的介绍。
  4. 每有一次 WindowInsetsCompat 变化,onProgress 都会被调用而你需要更新视图的边距。这保证了 UI 与动画无缝更新。要重新计算边距,就需要获取 systemBars 的底边距并把它加到 IME 现在的大小上,否则你的 UI 就会处于系统导航栏或者键盘的下面。如果用户打开键盘,除非动画结束,否则这个结果会持续累加;如果用户关闭键盘,这个结果会递减直到最终结果为 systemBars 的底边。
  5. 使用这些新的值来更新边距,从而 UI 将与键盘动画同步。
  6. 定义完回调后,重要的是还要设置它,否则什么也不会发生。

定义好所有这些后,试一下这些新的动画。编译并运行该应用程序,看看键盘的打开和关闭有多顺畅。

WindowInsets 动画的生命周期

setWindowInsetsAnimationCallback 中其他可用的方法有:

  • onPrepare:让你在动画发生前记录任意特定的配置。例如,你可以记录一个视图的初始坐标。
  • onStart:和上面的方法类似,你可以用它来保存任何以后要改变的值。你也可以在动画开始时用它来触发与此事件相关的任何其他行为。
  • onProgress:这个事件在键盘显示或从屏幕上消失时会被多次触发。每次 WindowInsetsCompat 改变时它都会被调用。
  • onEnd:这个事件在动画结束后触发。你可以用它来清理任何分配的资源或使任何 UI 视图反映这个新状态。

与键盘互动

在本节中,你将实现一个新的功能。当用户在 RecyclerView 中向上滚动时,你将以同样的速度推动键盘,直到它完全打开。如果它是可见的,在列表中向下滑动将导致相反的行为,键盘将关闭。

有几种方法可以实现这一点。你可以使用任何支持滚动的组件,如 ScrollViewNestedScrollView。在这个案例中将使用 RecyclerView

为了在用户滚动列表时打开或关闭键盘,你需要选择一个支持这种行为的组件:

  • 滚动方向:这取决于用户的滚动方向是上还是下。在这儿,要相应地打开或者关闭。
  • 过度滚动:列表可能不会移动,因为用户已经滚动到了它的极限,而用户仍在用手指在屏幕上滑动,期望看到相应的动作。因此,你要选择的组件需要支持这种行为。
  • 检测移动的开始和停止时间:也许你想检测键盘动画何时开始和结束。假如用户滚动了一小下,而你又想完全打开或关闭键盘,那么了解运动何时结束很重要。

为了实现这一点,你要使用 LinearLayoutManager,它支持上述所有的功能。转到 RWCompat11.kt,将 createLinearLayoutManager 更新为:

在这个方法中,有几个域已经被声明了,

  • scrolledY:保存用户拖动的距离,单位为像素。
  • scrollToOpenKeyboard:如果用户向上滚动以打开键盘,则为真;如果向下滚动以关闭键盘,则为假。
  • visible:用户开始动作时键盘的初始状态。

现在,你正要返回一个 LinearLayoutManager 的实例。而且,为了进行所需的计算,要重写两个不同的方法:

  • onScrollStateChanged:通知用户何时开始滚动列表,何时结束。
  • scrollVerticallyById: 在用户滚动的时候触发。这可以让你将键盘动画与用户滚动的列表同步起来。

观察滚动状态

现在覆盖 onScrollStateChanged。在 LinearLayoutManager 声明中加入这个方法:

如果 Android studio 提示需要导入,请导入 android.widget.AbsListView,暂时忽略那些缺失的方法。

下面是这段代码干的事:

  1. 当用户最初触摸到列表时,他们会触发值为 SCROLL_STATE_TOUCH_SCROLLonScrollStateChanged
  2. 为了了解键盘是要关闭还是打开,你需要保存其初始状态。如果已经是打开的,那就是要关闭键盘。反之,如果是关闭的,那就是要打开。
  3. 如果它已经可见,你需要用 IME 的当前底部位置初始化 scrolledY,否则 UI 的一部分将被覆盖。这个值在后面会很重要,因为它也会影响 scrollToOpenKeyboard 的值,此值定义了键盘的最终动作是打开还是关闭。
  4. 在这个方法中,你定义了在动画中使用的动画控制器的监听器。
  5. 在用户完成动作后,清理任何使用过的资源并完成动画。
  6. 如果键盘不是完全可见的(因为用户滚动了屏幕的一小部分),这个调用负责将这个动作完成。

在这个代码块中,有一个对你还没添加的方法的调用:createWindowInsetsAnimation。在同一个类中,在 createLinearLayoutManager 的声明后添加:

注意要使用的导入语句:
import android.os.CancellationSignal
import android.view.animation.LinearInterpolator

这将为你要执行动画的衬件添加一个控制器。这个方法接收以下参数:

  • types:你的应用程序想要控制的衬件的类型。在这个例子中,因为是键盘,所以你要把它设置为 IME
  • durationMillis:动画的持续时间。在用户拖动列表时,任意一个动作都会使键盘产生动画。你可以将其设置为 -1 来禁用动画。
  • interpolator:用于动画的插值器。在本例中,你将使用 LinearInterpolator
  • cancellationSignal:用于取消动画并返回到之前的状态。在当前情况下,选择的行为是完成动画,所以你不会用到这个。
  • listener:动画控制器的监听器,当窗口准备好做动画或操作取消或完成时被调用。

处理动画

现在你已经定义了 controlWindowInsetsAnimation,你需要声明这个方法中使用的 animationControlListener。在 RWCompat11 类的顶部、setUiWindowInsets 之前,添加:

当键盘准备好做动画时,你用你设置的将键盘拉上屏幕或从屏幕上弹走的 animationController 调用 onReady。在这里,animationController 被更新为要用于 LinearLayoutManager 的方法中的新引用。如果这个动作被取消或完成,它就会清理所有的资源,因为它们不再需要了。

在进行到最后一个方法之前,在 animationControlListener 的前面声明 animationController 域:

最后,回到 createLinearLayoutManager。你已经声明了 onScrollStateChanged,在这里键盘动画被设置并完成。现在是创建动画本身的时候了。

覆盖 scrollVerticallyBy

当提示导入时,导入 androidx.recyclerview.widget.RecyclerView.*android.graphics.Insets

在上面的代码中,

  1. 由于用户可以向上或向下滚动列表,所以你不能依赖键盘的初始可见状态。为了确定是应该打开还是关闭键盘,scrollToOpenKeyboard 根据 scrolledYdy 计算用户的滑动方向。如果最后一次滚动是向上滚动到列表的开头,那么键盘将显示,否则将隐藏。
  2. dy 包含来自 scrollVerticallyBy 事件的距离。要知道被滚动的总距离,你必须把它加到在 LinearLayoutManager 中设置的变量 scrolledY 上。
  3. 如果 scrolledY 是负值,这个值将被设置为0,因为不可能将键盘移动到一个负值。
  4. 最后,setInsetsAndAlpha 定义了需要在 IME 窗口上发生的移动。在这种情况下,你只需要定义底部的值,所以其他的值都被设置为 01f 对应的是为 alpha 属性设置的值,它被设置为最大值以避免有任何透明度。0f 是动画的进度。

现在你已经定义了一切,是时候编译和运行该应用程序了。

挺漂亮的,不是吗? :]

去往哪里?

祝贺你!你学会了如何在启动键盘时创建一个无缝动画。

你可以点击教程顶部或底部的下载材料按钮,下载完成的项目文件。

安卓 10 和 11 有很多可以用来增强你的应用的新功能,如果你想要获得一个有趣的挑战,可以尝试一下《泡泡》教程。考虑学习 ARCore 与 Kotlin 中的增强现实应用。你的应用程序要处理文件吗?不要忘了让它为范围存储做好准备。

如果你有任何问题或意见,请加入下面的讨论。

译自:Window Insets and Keyboard Animations Tutorial for Android 11 | raywenderlich.com

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注