Android监听软键盘删除键按键事件的可靠方案

三味码屋 2022年08月17日 2,778次浏览

场景描述

EditText中输入了一些内容后,要删除部分内容有多种方式,例如:通过EditText#setText方法设置新的内容,或者通过Editable#delete删除内容,又或者通过点击系统软键盘删除键删除内容。那么,当删除输入内容时,我们如何判断是不是通过点击系统软键盘删除键来删除的呢?

一般方法

一般通过android.view.View#setOnKeyListener来实现键盘按键监听,如下:

editText.setOnKeyListener { _, keyCode, event ->
    if (event.action == KeyEvent.ACTION_DOWN) {
        if (keyCode == KeyEvent.KEYCODE_DEL) {
            // 按下了删除键
        }
    }
    return@setOnKeyListener false
}

缺陷

此方法虽然可行,但是存在缺陷,因为按系统软键盘的删除键时,并不一定每次都会触发删除事件的回调。可以看下android.view.View#setOnKeyListener的代码:

    /**
     * Register a callback to be invoked when a hardware key is pressed in this view.
     * Key presses in software input methods will generally not trigger the methods of
     * this listener.
     * @param l the key listener to attach to this view
     */
    public void setOnKeyListener(OnKeyListener l) {
        getListenerInfo().mOnKeyListener = l;
    }

代码注释的大意是:注册在此View中按下 硬件键 时要调用的回调。 软件输入法中的按键一般不会触发该监听器的方法。
也就是说,android.view.View#setOnKeyListener是用于监听硬件键的各种事件的,按软键盘中的键不一定会触发事件回调。

解决办法

既然问题来了,我们要如何解决呢?
我们可以通过自定义EditText并重写onCreateInputConnection方法来解决,如下:

/**
 * 自定义[EditText],用于监听系统软键盘按键事件,保证通过[EditText.setOnKeyListener]注册的回调接口实例能够正常接收
 * 到软键盘按键事件
 */
@SuppressLint("AppCompatCustomView")
class MyEditText @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : EditText(context, attrs) {

    override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection {
        return MyInputConnection(super.onCreateInputConnection(outAttrs), true)
    }

    /**
     * 自定义[InputConnectionWrapper]
     * 因为默认情况下,通过[EditText.setOnKeyListener]注册监听并不能保证每次按软键盘中的键时都会触发事件回调
     * 通过重写[deleteSurroundingText]捕获删除键事件进行分发,以触发 android.view.View.OnKeyListener.onKey 回调
     */
    private class MyInputConnection(target: InputConnection?, mutable: Boolean) :
        InputConnectionWrapper(target, mutable) {

        /**
         * 删除当前光标位置之前的文本的 beforeLength 个字符,删除当前光标位置之后的文本的 afterLength 个字符,不包括所选内容。
         */
        override fun deleteSurroundingTextInCodePoints(
            beforeLength: Int,
            afterLength: Int
        ): Boolean {
            return checkSurroundingText(beforeLength, afterLength)
                ?: super.deleteSurroundingTextInCodePoints(beforeLength, afterLength)
        }

        /**
         * 删除当前光标位置之前的文本的 beforeLength 个字符,删除当前光标位置之后的文本的 afterLength 个字符,不包括所选内容。
         */
        override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
            return checkSurroundingText(beforeLength, afterLength)
                ?: super.deleteSurroundingText(beforeLength, afterLength)
        }

        private fun checkSurroundingText(beforeLength: Int, afterLength: Int): Boolean? {
            if (beforeLength == 1 && afterLength == 0) {
                // 捕获到删除键按压事件,通过 sendKeyEvent 分发事件,以触发 android.view.View.OnKeyListener.onKey 回调
                return sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
                        && sendKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL))
            }
            return null
        }
    }
}

过程分析

  1. 自定义EditText并重写android.widget.TextView#onCreateInputConnection方法,创建InputConnection对象;
  2. 在自定义InputConnection中重写android.view.inputmethod.InputConnectionWrapper#deleteSurroundingText方法和android.view.inputmethod.InputConnectionWrapper#deleteSurroundingTextInCodePoints方法,用来捕获删除键按压事件,之所以要同时重写这两个方法,是为了尽可能兼容更多机型,因为部分机型,特别是鸿蒙系统的华为机型,只能触发其中某一个方法;
  3. 捕获到删除键按压事件后,再通过 sendKeyEvent 分发事件,从而触发 android.view.View.OnKeyListener.onKey 回调。