实现边到边的体验 | 让您的软键盘动起来 (一)

Android 11 中的新功能之一是可以让应用在对于屏幕上的软键盘打开和关闭的过程创建无缝过渡的动画效果,这一功能源自 Android 11 中对 WindowInsets API 的大量改进。

在 Android 11 上有两个针对该功能的例子——这个功能已经被集成到 Google Search 应用和 Messages 应用中了:

两个 Android 11 中软键盘动画效果的示例: Google Search 应用 (左),Messages (右)

让我们来看看如何在您的应用中添加这种用户体验。总共分为三步:

  • 首先,我们需要做到 "边到边" (edge-to-edge)
  • 第二步,应用需要针对边衬区动画做出反应;
  • 最后第三步就是应用在恰当的场景中控制并使用边衬区动画。

上面的每一步都环环相扣,所以我们会在不同的文章中分别介绍。在这个系列的第一部中,我们会介绍如何实现边到边,以及 Android 11 中相关 API 的改动。

实现边到边 (edge-to-edge)

去年我们介绍了一个关于实现 "边到边" 的概念,这个方法可以让应用深度利用 Android 10 的手势导航: 开启全面屏体验 | 手势导航 (一)

简单回顾一下,实现 "边到边" 会让您的应用渲染在系统状态栏的后面,如左上所示。

引用去年我自己的话:

实现从边到边的全面屏体验后,系统栏会覆盖在应用内容前方。应用也得以通过更大幅面的内容为用户带来更具有冲击力的体验。

实现边到边跟软键盘有什么关系?

其实,实现边到边不单单只是在状态栏和导航栏之后渲染。应用本身需要开始负责处理那些跟应用重叠的系统 UI 的部分。

正如我们前面提到的,两个最直观的例子是状态栏和导航栏。除此之外还有软键盘,有时候也叫 IME (输入法编辑器),这是另外一个我们需要了解的系统 UI 。

应用如何实现边到边?

如果我们回想 去年的介绍,实现边到边可以分为三步:

  • 改变系统栏的颜色
  • 设置全屏布局
  • 处理视觉冲突

我们会跳过第一步,因为从去年至今这个部分没有改动。教程中的第二步和第三步有一些针对 Android 11 的改动,让我们来看一下。

#2: 设置全屏布局

在以往的第二步中,应用需要使用 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 版本里也包含了这个函数。

以上就是第二步的改动。

#3: 处理视觉冲突

现在让我们来看一下第三步: 避免与系统 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())
}

软键盘类型 ⌨️

这会儿那些敏锐的 👀 可能已经开始盯着这个类型列表,尤其是其中的 软键盘类型

在姗姗来迟了十年后,我们终于可以回答这个关于如何查看软键盘可见性的 StackOverflow 问题。🎉

为了获取当前软键盘的可见性,我们可以取得根视窗的边衬区,然后执行 isVisible() 函数并传入 IME 类型。

同样地,如果我们想查出高度,我们也可以通过相同的方法实现:

val insets = ViewCompat.getRootWindowInsets(view)
val imeVisible = insets.isVisible(Type.ime())
val imeHeight = insets.getInsets(Type.ime()).bottom

如果我们需要监听软键盘的改变,我们可以照常使用 OnApplyWindowInsetsListener,并且使用同样的函数:

ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
    val imeVisible = insets.isVisible(Type.ime())
    val imeHeight = insets.getInsets(Type.ime()).bottom
}

隐藏或显示软键盘

既然我们正在回答 StackOverflow 上的问题,来看一下这个 11 年前关于如何关闭软键盘的问题。

这一次我们要介绍 Android 11 的一个新 API,它叫 WindowInsetsController

应用可以从任何视图获得一个控制器,然后我们就可以通过传入 IME 类型,并执行 show() 或者 hide() 函数来实现显示或隐藏软键盘:

val controller = view.windowInsetsController
// 显示软键盘( IME )
controller.show(Type.ime())
// 隐藏软键盘
controller.hide(Type.ime())

然而,这个控制器不单单能控制隐藏和显示软键盘...

WindowInsetsController

之前我们提到过,有一些 View.SYSTEM_UI_* 标志已经在 Android 11 中被弃用,并且被新的 API 代替。还有一些 View.SYSTEM_UI 标志本来是被用来改变系统 UI 的外观和可见性的,包括:

  • View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
  • View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
  • View.SYSTEM_UI_FLAG_LAYOUT_STABLE
  • View.SYSTEM_UI_FLAG_LOW_PROFILE
  • View.SYSTEM_UI_FLAG_FULLSCREEN
  • View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
  • View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
  • View.SYSTEM_UI_FLAG_IMMERSIVE
  • View.SYSTEM_UI_FLAG_VISIBLE
  • View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
  • View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR

和之前的标志类似,这些也都在 API 30 中被弃用,并被 WindowInsetsController 中的 API 代替。

接下来我们会通过几个常见的应用场景来介绍如何更新这些标志,而不是一一介绍所有这些标志的改变:

沉浸模式

如图所示,这个绘图应用隐藏了系统 UI 来让绘图区域最大化:

Markers 应用,展示隐藏系统 UI

为了实现这个效果,我们像以前一样使用 WindowInsetsController 来执行 hide()show() 函数,但是这一次我们要传入系统栏类型:

val controller = view.windowInsetsController

// 当我们想隐藏系统栏
controller.hide(Type.systemBars())

// 当我们想显示系统栏
controller.show(Type.systemBars())

应用使用 沉浸模式 来让用户在系统栏隐藏的时候可以通过滑动来召回系统栏。为了实现这个效果,我们使用 WindowInsetsController 并且改变 setSystemBarsBehavior()BEHAVIOR_SHOW_BARS_BY_SWIPE:

val controller = view.windowInsetsController

// 现在开始沉浸式..
controller.setSystemBarsBehavior(
    WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
)

// 当我们想要隐藏系统栏
controller.hide(Type.systemBars())

类似地,如果您之前使用吸附式的 沉浸模式,这个现在也可以用 BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE 来实现:

val controller = view.windowInsetsController

// 现在开始吸附式沉浸式体验 ...
controller.setSystemBarsBehavior(
    BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
)

// 当我们想要隐藏系统栏
controller.hide(Type.systemBars())

状态栏内容的颜色

接下来的这个应用场景是围绕着状态栏内容的颜色。您会看到如下两个应用:

两个应用,左边的使用的是深色状态栏背景,右边的使用的是浅色背景

左边的应用使用的是一个深色的状态栏背景,而它的内容用的是浅色,比如时间和图标。可如果我们想实现一个浅色的状态栏背景并且搭配深色的内容,像右边显示的一样,我们也可以使用 WindowInsetsController

要实现这个效果,我们可以使用 setSystemBarsAppearance() 函数,传入 APPEARANCE_LIGHT_STATUS_BARS 值:

val controller = view.windowInsetsController

// 启用浅色状态栏内容
controller.setSystemBarsAppearance(
    APPEARANCE_LIGHT_STATUS_BARS, // value
    APPEARANCE_LIGHT_STATUS_BARS // mask
)

但如果您想设置一个深色的状态栏,可以传入 0,而不是清除那个值。

注意: 您也可以在主题中通过设置 android:windowLightStatusBar 实现上述效果。在您知道这个值不会变动的情况下,这个方式可能更好。

APPEARANCE_LIGHT_NAVIGATION_BARS 标志可以给导航栏提供类似的功能。

AndroidX 中的 WindowInsetsController?

可惜的是这个 API 的 Jetpack 版本还没有上线,而我们正在加紧准备,敬请关注。

实现边到边: ✔️

我们的第一步完成了。在本系列下一篇文章中,我们会研究第二步: 应用对于边衬区的响应式动画。敬请关注。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354