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