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}返回到正在接收其輸入的應用程序的通信通道。它用於執行諸如讀取光標周圍的文本,將文本提交到文本框以及將原始鍵事件發送到應用程序之類的事情。
事實上谷歌的鍵盤輸入流式這樣完成的:
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);
}
}
}
以上就是一個包含了攔截器並與控件關聯的實現,當然你也可以不用內部類來完成,我只是簡單的描述一下。