最近终于总结出最佳的软键盘高度监测方案了,特此分享出来。
源码在此:KeyboardObserver.kt

视觉效果

当开启showDebug时候,可以看到这样的可视化的键盘高度监测。
showcase

源码分析

简单来说,这里说通过两个PopupWindow来实现的键盘高度测量。一个用于测量当前屏幕状态可绘制区域的最大高度,一个用于跟随键盘移动,进而通过高度差,算出键盘的高度。我们这里将前者称为RulerPopWin, 后者称为CursorPopWin。他们的代码分别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// RulerPopWin
private val rulerPopWin by lazy { makeRulerPopWin(activity) }
private fun makeRulerPopWin(activity: Activity) = PopupWindow(activity).apply {
contentView = if (showDebug) {
TextView(activity).apply {
background = GradientDrawable().apply {
this.setStroke(1.dp, Color.LTGRAY)
}
gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
setTextColor(Color.RED)
}
} else {
View(activity)
}
setBackgroundDrawable(null)
width = if (showDebug) 80.dp else 1
height = WindowManager.LayoutParams.MATCH_PARENT
elevation = 0F

isFocusable = false
isTouchable = false
isOutsideTouchable = false
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// CursorPopWin
private val cursorPopWin by lazy { makeCursorPopWin(activity) }
private fun makeCursorPopWin(activity: Activity) = PopupWindow(activity).apply {
contentView = if (showDebug) {
FrameLayout(activity).apply {
addView(
View(activity).apply {
background = ColorDrawable(Color.RED)
},
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
1.dp,
Gravity.BOTTOM
)
)
}
} else {
View(activity)
}
setBackgroundDrawable(null)

width = if (showDebug) 80.dp else 1
height = WindowManager.LayoutParams.MATCH_PARENT
elevation = 0F

softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
inputMethodMode = PopupWindow.INPUT_METHOD_NEEDED

isFocusable = false
isTouchable = false
isOutsideTouchable = false
}

这两个PopupWindow,除了contentView不同以外,还有两个属性不同,cursorPopWin多了两个属性

1
2
softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
inputMethodMode = PopupWindow.INPUT_METHOD_NEEDED

这两个属性,使得cursorPopWin高度会随着软键盘的弹出而变化。

当开启监听时,为cursorPopWin.contentView设置一个OnLayoutChangeListener,用于监听其布局变化。

1
2
3
4
5
6
fun watch() {
if (!cursorPopWin.isShowing) {
cursorPopWin.showAtLocation(decorView, Gravity.BOTTOM or Gravity.END, 0, 0)
cursorPopWin.contentView.addOnLayoutChangeListener(cursorLayoutChangeListener)
}
}
1
2
3
4
5
6
7
private val cursorLayoutChangeListener = OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
if (rulerPopWin.isShowing) {
rulerPopWin.dismiss()
}
rulerPopWin.showAtLocation(decorView, Gravity.BOTTOM or Gravity.END, 0, 0)
rulerPopWin.contentView.addOnLayoutChangeListener(rulerLayoutChangeListener)
}

在cursorLayoutChangeListener中,监听到cursotPopWin的变化后,再显示rulerPopWin。为什么要这么做呢?

因为在实践中,软键盘的变化,触发了onLayoutChange方法,如果是在这之前就把rulerPopWin显示出来,在某些机型或者系统版本中,也会出现rulerPopWin跟随键盘改变尺寸的情况。所以要将rulerPopWin显示在键盘弹出之后。

真实的键盘高度监听,实际上说在rulerLayoutChangeListener中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private val rulerLayoutChangeListener = object : OnLayoutChangeListener {
override fun onLayoutChange(
v: View?,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
rulerPopWin.contentView.removeOnLayoutChangeListener(this)
rulerPopWin.contentView.getGlobalVisibleRect(rulerRect)
cursorPopWin.contentView.getGlobalVisibleRect(cursorRect)

val keyboardHeight = rulerRect.bottom - cursorRect.bottom

if (callbacks.isNotEmpty()) {
val cbs = ArrayList<Callback>(callbacks)
cbs.forEach {
it.onKeyboardHeightChanged(keyboardHeight)
}
cbs.clear()
}

if (showDebug) {
(v as TextView).run {

text = "$keyboardHeight"
setPadding(0, 0, 0, max(keyboardHeight - this.lineHeight, 0))
}
}
}

}

OK,这就是全部关键逻辑了。

Q&A

  1. 为什么要通过rulerPopWin来获取高度,用其他获取屏幕高度的方法不好吗?

    不可以,这里rulerPopWin,实际上是测量当前状态下,键盘收起时的最底部。这个状态受到很多其他方面的影响,比如横竖屏切换、底部导航条的显示或者隐藏。如果通过直接获取屏幕高度的方式,并不能与状态严格对其。尤其说底部导航条的各种显示模式,导致键盘收起的0线也是变化的。

  2. 为什么不只用一个cursorPopWin的软键盘弹起前后进行差值计算高度呢?

    这样做在实践中也是不可行的。同样跟问题1中的场景类似,由于底部导航条各种显示模式的影响,导致软键盘0线是不确定的,而软键盘的弹起与收回,可能对应着不同的导航条显示模式,也就对应着不同的0线,这样计算出来的软键盘高度,很容易把导航条的高度也算进去。

  3. 由问题2想到,把导航条的高度减掉不就是键盘的高度了吗?

    这样做理论上可行,但是实践上问题会很多。首先,你要针对不同的导航条模式做不同策略;其次,不同系统的导航条的高度不同,包括说传统3键导航条还是全面屏手势;再次,目前获取导航条高度,并没有一个完美的方案,有些系统下获取到的高度,跟实际高度是不相符的。

本文采用CC-BY-SA-3.0协议,转载请注明出处
Author: boybeak