android 按键监听及键盘事件流(无法监听删除键)

android 按键监听及键盘事件流(无法监听删除键)

最近在做一个密码按键输入功能时需要对每次按键进行一些处理,于是使用了 OnKeyListener 接口监听,对于正常文本格式的输入按键事件都能监听到,但是一旦修改 EditText 的输入类型为 NumbberPassword(android:inputType="numberPassword") 则无法监听到键盘的删除按钮事件。

于是查阅资料:

一般使用 OnKeyListener 接口监听按键事件如下:

    editText.setOnKeyListener(new View.OnKeyListener() {
        @Override
        public boolean onKey(View v, int keyCode, KeyEvent event) {
            if(event.getAction() == KeyEvent.ACTION_DOWN) {
                switch (keyCode) {
                    case KeyEvent.KEYCODE_DEL:
                        // 处理相关退格键行为
                        return true;
                    ...
                }
            }
            return false;
        }
    });

上面这个这个方案在大多数情况下都没有问题,但是当使用 android:inputType="numberPassword" 时事件并未得到响应。于是翻看了关于 OnKeyListener 的注释:

    /**
     * Interface definition for a callback to be invoked when a hardware key event is
     * dispatched to this view. The callback will be invoked before the key event is
     * given to the view. This is only useful for hardware keyboards; a software input
     * method has no obligation to trigger this listener.
     */
    public interface OnKeyListener {
        /**
         * Called when a hardware key is dispatched to a view. This allows listeners to
         * get a chance to respond before the target view.
         * <p>Key presses in software keyboards will generally NOT trigger this method,
         * although some may elect to do so in some situations. Do not assume a
         * software input method has to be key-based; even if it is, it may use key presses
         * in a different way than you expect, so there is no way to reliably catch soft
         * input key presses.
         */
        boolean onKey(View v, int keyCode, KeyEvent event);
    }

类注释大概的意思是:硬件按键会一定会回调这个接口,这仅对硬件键盘有用。软件输入法没有义务触发此侦听器。
方法的注释大概意思是:硬件的key会派发到这里,但软件键盘中的按键通常不会触发此方法。尽管某些情况下可能会选择这样做,不要假设软件输入法必须基于密钥。即使是这样,它也可能以与您预期不同的方式使用按键,因此无法可靠地捕获软输入按键。(意思就是这个监听对软键盘来说并不可靠)。既然不可靠那么通过什么方式是谷歌推荐的呢,通过查阅资料得知。

InputConnection 接口

/**
 * The InputConnection interface is the communication channel from an
 * {@link InputMethod} back to the application that is receiving its
 * input. It is used to perform such things as reading text around the
 * cursor, committing text to the text box, and sending raw key events
 * to the application.
 ....
 */
public interface InputConnection {
    ...
}

从上面注释得知:InputConnection接口是从{@link InputMethod}返回到正在接收其输入的应用程序的通信通道。它用于执行诸如读取光标周围的文本,将文本提交到文本框以及将原始键事件发送到应用程序之类的事情。
事实上谷歌的键盘输入流式这样完成的:
avatar
InputConnection有几个关键方法,通过重写这几个方法,我们基本可以拦截软键盘的所有输入和点击事件:

//当输入法输入了字符,包括表情,字母、文字、数字和符号等内容,会回调该方法
public boolean commitText(CharSequence text, int newCursorPosition) 

//当有按键输入时,该方法会被回调。比如点击退格键时,搜狗输入法应该就是通过调用该方法,
//发送keyEvent的,但谷歌输入法却不会调用该方法,而是调用下面的deleteSurroundingText()方法。  
public boolean sendKeyEvent(KeyEvent event);   

//当有文本删除操作时(剪切,点击退格键),会触发该方法 
public boolean deleteSurroundingText(int beforeLength, int afterLength) 

//结束组合文本输入的时候,回调该方法
public boolean finishComposingText();

那么 InputConnection 如何与 EditText 建立关联的呢?

实际上在EditText和输入法建立连接的时候,EditText的onCreateInputConnection()方法会被触发:

    /**
     * Create a new InputConnection for an InputMethod to interact
     * with the view.  The default implementation returns null, since it doesn't
     * support input methods.  You can override this to implement such support.
     * This is only needed for views that take focus and text input.
     *
     * <p>When implementing this, you probably also want to implement
     * {@link #onCheckIsTextEditor()} to indicate you will return a
     * non-null InputConnection.</p>
     *
     * <p>Also, take good care to fill in the {@link android.view.inputmethod.EditorInfo}
     * object correctly and in its entirety, so that the connected IME can rely
     * on its values. For example, {@link android.view.inputmethod.EditorInfo#initialSelStart}
     * and  {@link android.view.inputmethod.EditorInfo#initialSelEnd} members
     * must be filled in with the correct cursor position for IMEs to work correctly
     * with your application.</p>
     */
    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
        return null;
    }

注释只贴了核心部分大概意思就是:为InputMethod创建一个新的InputConnection以便与视图进行交互。默认实现返回null,因为它不支持输入法。您可以覆盖它以实现这种支持。仅对于具有焦点和文本输入的视图才需要。
在实现此功能时,您可能还希望实现onCheckIsTextEditor()来指示您将返回非null的InputConnection。
另外,请务必正确且完整地填写EditorInfo对象,以使连接的IME可以依赖其值。例如,必须使用正确的光标位置填充initialSelStart和initialSelEnd成员,IME才能正确地与您的应用程序一起使用。

也就是说我们只需要实现这个方法并给一个实现接口的返回我们就可以接管键盘输入了。这个方法是 View 的方法,扩展下想象力,就是任何View都可以去响应按键的。那这里我们就可以直接使用了么?并不能因为接口并没有提供常规处理,如果完全自己实现,我们需要完成其他按键相关处理,工作量仍旧巨大。那么EditText具备这个功能那么应该也是有实现的吧,实时上是的。在 TextView 中就提供了 EditableInputConnection 类来处理输入,但是他是 hide 的无法被继承,可能出于安全角度考虑,所以就没有办法了么?其实google为我们提供了一个类 InputConnectionWrapper 一个默认代理类,完成了大部分常规的操作,我们可以继承这个类来针对自己想要的部分实现替换。

/**
 * <p>Wrapper class for proxying calls to another InputConnection.  Subclass and have fun!
 */
public class InputConnectionWrapper implements InputConnection {
    ...
}

注释解释:包装器类,用于代理对另一个InputConnection的调用。子类,玩得开心!(google工程师还是很幽默的)
到这里我们就可以通过实现这个类来完成键盘的拦截监听了。

/**
 * 始终从尾部输入的编辑文本控件
 */
public class TailInputEditText extends AppCompatEditText {

    public TailInputEditText(Context context) {
        this(context, null);
    }

    public TailInputEditText(Context context, AttributeSet attrs) {
        this(context, attrs, android.R.attr.editTextStyle);
    }

    public TailInputEditText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onSelectionChanged(int selStart, int selEnd) {
        super.onSelectionChanged(selStart, selEnd);
        if (selStart == selEnd){//防止不能多选
            if(getText() == null){//判空,防止出现空指针
                setSelection(0);
            }else {
                setSelection(getText().length()); // 保证光标始终在最后面
//                setSelection(0, getText().length());
            }
        }
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        // 其他按键事件响应
        return super.onKeyDown(keyCode, event);
    }

    @Override
    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
        // 返回自己的实现
        return new BackspaceInputConnection(super.onCreateInputConnection(outAttrs), true);
    }

    private class BackspaceInputConnection extends InputConnectionWrapper {

        public BackspaceInputConnection(InputConnection target, boolean mutable) {
            super(target, mutable);
        }

        /**
         * 当软键盘删除文本之前,会调用这个方法通知输入框,我们可以重写这个方法并判断是否要拦截这个删除事件。
         * 在谷歌输入法上,点击退格键的时候不会调用{@link #sendKeyEvent(KeyEvent event)},
         * 而是直接回调这个方法,所以也要在这个方法上做拦截;
         * */
        @Override
        public boolean deleteSurroundingText(int beforeLength, int afterLength) {
            // 做你想做的是拦截他
            return super.deleteSurroundingText(beforeLength, afterLength);
        }

    }

}

以上就是一个包含了拦截器并与控件关联的实现,当然你也可以不用内部类来完成,我只是简单的描述一下。

到这里键盘事件的拦截问题告一小段落,有什么其他的想法可以留言讨论。

posted @ 2021-01-07 10:59  summer_xx  阅读(85)  评论(0编辑  收藏