先上效果图:
图穷匕见: 该文字整理自我的博客文字《可能是软键盘状态与高度监听最佳方案》 与 《# 软键盘高度监测最佳实践》 源码参考 - skb-global
简单做一个补充,@GeekTR 同学提到了一个方式,就是利用WindowInsets API获取键盘高度。在经过简单实验以后,在Android 10及以上的系统版本中,确实是可以的,需要注意的是,键盘弹起过程中,会多次调用
onApplyWindowInsets
,这就必须通过“防抖”来解决,这就会遇到滞后性、误触性的问题;在低于Android 10的版本中,键盘弹起并不会触发onApplyWindowInsets
方法,并且没有WindowInsets.Type.ime()
这样专指键盘高度的insets,如果通过insets.systemWindowInsetBottom
又容易受到底部导航栏变化的影响。所以,不再做WindowInsets的方案了,有兴趣探索的,可以查看feat/insets分支下的代码。
背景
监听Android的软键盘状态与高度总是很麻烦,因为官方并没有给一个api来做这个事,我们就只能利用系统的其他机制来实现。
先说一下之前尝试的旧方案:
在布局中添加一个空白的FrameLayout,通过这个FrameLayout的onSizeChanged
或者addOnLayoutChangeListener
,来获取软键盘弹出前后的高度值差值来计算键盘的高度,由于尺寸变更的回调,可能在整个过程中,会多次触发,就需要使用一个延迟任务去测量布局的高度,即需要做防抖,如果在任务等待期间,又一次触发了尺寸变化的回调,说明布局还未稳定,则取消掉上一次测量任务,添加一个新的测量任务。 但是这种方案有如下问题:
- 侵入性,必须在布局中显式的放置测量布局;
- 滞后性,测量时机上延迟的,并不能在布局稳定的第一时间获取到键盘的高度;
- 误触性,由于获取键盘高度靠延迟任务,延迟的时间太长,则导致滞后性太严重,太短,则可能来不及取消上一个任务,测量高度就被错误触发;
- 不定性,由于设备可能会有横竖屏幕切换,因横竖屏切换导致的测量布局尺寸发生变化进而触发键盘高度事件,这个是不可接受的。
为了解决这些问题,我改成现在的监听方案 —— skb-global。
一、安装
该库托管于jitpack,所以在使用前,请先引入jitpack。
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
然后添加依赖。
dependencies {
implementation 'com.github.boybeak:skb-global:Tag'
}
最新版本为:
二、使用
有两种使用方式:全局和局部。
2.1 全局使用
在使用前,需要先在Application中初始化SoftKeyboardGlobal
。
class App : Application() {
override fun onCreate() {
super.onCreate()
SoftKeyboardGlobal.install(this, true)
}
}
其中,第二个参数传入
true
,可以在UI界面显示一个高度指示器。
然后,在你应用中的任意位置,可以监听键盘的状态与高度,如下方式:
SoftKeyboardGlobal.addSoftKeyboardCallback(object : SoftKeyboardGlobal.SoftKeyboardCallback {
override fun onOpen(height: Int) {
Log.d(TAG, "onOpen height=$height")
}
override fun onClose() {
Log.d(TAG, "onClose")
}
override fun onHeightChanged(height: Int) {
Log.d(TAG, "onHeightChanged height=$height")
}
})
2.2 局部使用
你可以在任意Activity
, Fragment
或者View
中使用这种方式,只要能获取到Activity
实例。以Activity
为例,如下方式:
class MainActivity : AppCompatActivity() {
private val observer by lazy { KeyboardObserver.create(this, true) }
private val switchBtn: SwitchCompat by lazy { findViewById(R.id.switchBtn) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
switchBtn.setOnCheckedChangeListener { buttonView, isChecked ->
if (isChecked) {
observer.watch()
} else {
observer.unwatch()
}
}
}
}
同样的,在创建KeyboardObserver
时,第二个参数传入true
可以显示一个键盘高度指示器,方便调试。
三、原理分析
该方案是在多年开发过程中,实践出来的最佳的方案。正如在摘要中说的,旧方案有四点问题,为了解决这些问题,我决定完全抛弃旧方案。
旧方案之所以出现这些问题,就是因为单一布局测量,1. 无法知道布局稳定的准确时机; 2. 键盘弹出时,弹出前的高度,可能因为横竖屏幕切换,导致变得不可靠。
进而我改成双布局测量,但是双布局都放置在原有布局中,同样避免不了因键盘弹出,同时遭到尺寸修改。
再进而,我改为不受键盘弹出影响的PopupWindow。用两个PopupWindow,一个用于测量当前屏幕的高度(实际上并不是屏幕高度,而是键盘底部的位置),称为Ruler——尺子,Ruler并不会随着键盘的弹出而改变尺寸;相反的,另外一个会跟随软键盘的弹出而改变尺寸,称为Cursor——游标。
两个PopupWindow的创建代码如下:
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 // if set to 0, getGlobalVisibleRect will not work
height = WindowManager.LayoutParams.MATCH_PARENT
elevation = 0F
isFocusable = false
isTouchable = false
isOutsideTouchable = false
}
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 // if set to 0, getGlobalVisibleRect will not work
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
}
二者的关键区别就在于makeCursorPopWin
时的这两行代码:
softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
inputMethodMode = PopupWindow.INPUT_METHOD_NEEDED
正是这两行代码,让Cursor会跟随键盘弹出而改变尺寸,尺寸改变后,再显示Ruler,必须是这样的时机,不然Ruler在某些厂商的系统中,也会跟随键盘尺寸发生变化,就失去了比较意义。等待Ruler显示后,会触发onLayoutChange
,且只触发这一次,则只需要在这一次回调中,去检测二者的高度差值,即为键盘高度值。
这样做,可以避免侵入。
- 低侵入性,只需要调用watch与unwatch即可;
- 无滞后性,由于不采用延迟任务的方式,所以没有滞后性;
- 无误触性,同样是因为没有采用延迟任务的方式,所以没有误触性;
- 稳定性,由于采用的是双布局的差值比较,所以不会因为横竖屏幕切换导致的触发键盘高度事件;
想要更多细节,请查看代码:skb-global。