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

视觉效果

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

源码分析

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

// 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
}
// 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多了两个属性

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

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

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

fun watch() {
    if (!cursorPopWin.isShowing) {
        cursorPopWin.showAtLocation(decorView, Gravity.BOTTOM or Gravity.END, 0, 0)
        cursorPopWin.contentView.addOnLayoutChangeListener(cursorLayoutChangeListener)
    }
}
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中。

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键导航条还是全面屏手势;再次,目前获取导航条高度,并没有一个完美的方案,有些系统下获取到的高度,跟实际高度是不相符的。