前些天测试部的妹子测出来一个奇葩的问题,使用android原生的输入法和华为的Swype输入法时,监听软键盘的回退键(删除键)竟然无效!搜狗输入法和百度输入法是正常的。先看一下原代码的写法:
editText.setOnKeyListener(new OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DEL
&& event.getAction() == KeyEvent.ACTION_DOWN) {
//TODO:
return true;
}
return false;
}
}
通过给EditText添加OnKeyListener监听回退键的事件,这是一个大家熟知的标准的写法。而如今在某些输入法上却失效了,此时我的内心是崩溃的。。。
android源码中,这个方法说明如下:
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.
简单翻译一下:注册一个回调,在view按下物理按键时触发。输入法中的键按下时通常不会触发该回调。
说的很明白了,输入法回调该方法是情分,不回调是本分。。那就只能看看EditText、TextVeiw、View甚至Activity中有没有什么方法可以解决了。
探索一:
首先想到的是TextView.setOnEditorActionListener这个方法,但是注释说这个方法是用来监听enter键的,果断放弃。
探索二:
翻一下TextView,发现有个setKeyListener方法,瞬间又燃起来希望。KeyListener的说明是这样的:
*Key presses on soft input methods are not required to trigger the methods
* in this listener, and are in fact discouraged to do so. The default
* android keyboard will not trigger these for any key to any application
* targetting Jelly Bean or later, and will only deliver it for some
* key presses to applications targetting Ice Cream Sandwich or earlier
软键盘的按键事件不提倡调用这个方法。好吧,又被浇了一头冷水。。。
探索三:
继续探索TextView,还发现一系列的继承方法:onKeyDown、onKeyUp、onKeyPreIme...好吧,这么多揉到一块说,只能说明它们都不是主角。
探索四:
覆写Activity|View的dispatchKeyEvent方法,然而依然没有作用。。
探索到这里,说明一点:从KeyEvent上去找线索已经走错路了,那两个输入法根本没按套路出牌!
好吧,不绕圈子了,直接进入主题。
百度一下“自定义接收软键盘输入的View”发现,要接受输入法的输入事件,view有一个方法是关键:
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
return null;
}
该方法返回一个InputConnection,这是输入法与view交互的纽带,那可不可以通过它监听到回退键事件呢?TextView中是这样覆写该方法的:
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
if (onCheckIsTextEditor() && isEnabled()) {
……此处省略……
if (mText instanceof Editable) {
InputConnection ic = new EditableInputConnection(this);
outAttrs.initialSelStart = getSelectionStart();
outAttrs.initialSelEnd = getSelectionEnd();
outAttrs.initialCapsMode = ic.getCursorCapsMode(getInputType());
return ic;
}
}
return null;
}
最终该方法返回一个EditableInputConnection实例。该类继承自BaseInputConnection。而与回退键相关的方法,只有一个deleteSurroundingText方法,让我们怀着忐忑的心情整理代码实验一下吧。
首先需要改动一下EditableInputConnection这个类。由于是internal包的类,在android sdk中找不到,我们需要从网上下载一个,然后使用反射替换掉隐藏方法的调用,最后为deleteSurroundingText方法设置一个监听器,最终的代码是这个样子的:
EditableInputConnection.java
public class EditableInputConnection extends BaseInputConnection {
private static final boolean DEBUG = false;
private static final String TAG = "EditableInputConnection";
private final TextView mTextView;
// Keeps track of nested begin/end batch edit to ensure this connection always has a
// balanced impact on its associated TextView.
// A negative value means that this connection has been finished by the InputMethodManager.
private int mBatchEditNesting;
private final InputMethodManager mIMM;
private OnDelEventListener delEventListener;
public EditableInputConnection(TextView textview) {
super(textview, true);
mTextView = textview;
mIMM = (InputMethodManager) textview.getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
}
@Override
public Editable getEditable() {
TextView tv = mTextView;
if (tv != null) {
return tv.getEditableText();
}
return null;
}
@Override
public boolean beginBatchEdit() {
synchronized (this) {
if (mBatchEditNesting >= 0) {
mTextView.beginBatchEdit();
mBatchEditNesting++;
return true;
}
}
return false;
}
@Override
public boolean endBatchEdit() {
synchronized (this) {
if (mBatchEditNesting > 0) {
// When the connection is reset by the InputMethodManager and reportFinish
// is called, some endBatchEdit calls may still be asynchronously received from the
// IME. Do not take these into account, thus ensuring that this IC's final
// contribution to mTextView's nested batch edit count is zero.
mTextView.endBatchEdit();
mBatchEditNesting--;
return true;
}
}
return false;
}
protected void reportFinish() {
synchronized (this) {
while (mBatchEditNesting > 0) {
endBatchEdit();
}
// Will prevent any further calls to begin or endBatchEdit
mBatchEditNesting = -1;
}
}
@Override
public boolean clearMetaKeyStates(int states) {
Editable content = getEditable();
if (content == null) {
return false;
}
KeyListener kl = mTextView.getKeyListener();
if (kl != null) {
try {
kl.clearMetaKeyState(mTextView, content, states);
} catch (AbstractMethodError e) {
// This is an old listener that doesn't implement the
// new method.
}
}
return true;
}
@Override
public boolean commitCompletion(CompletionInfo text) {
if (DEBUG) {
Log.v(TAG, "commitCompletion " + text);
}
mTextView.beginBatchEdit();
mTextView.onCommitCompletion(text);
mTextView.endBatchEdit();
return true;
}
/**
* Calls the {@link TextView#onCommitCorrection} method of the associated TextView.
*/
@Override
public boolean commitCorrection(CorrectionInfo correctionInfo) {
if (DEBUG) {
Log.v(TAG, "commitCorrection" + correctionInfo);
}
mTextView.beginBatchEdit();
mTextView.onCommitCorrection(correctionInfo);
mTextView.endBatchEdit();
return true;
}
@Override
public boolean performEditorAction(int actionCode) {
if (DEBUG) {
Log.v(TAG, "performEditorAction " + actionCode);
}
mTextView.onEditorAction(actionCode);
return true;
}
@Override
public boolean performContextMenuAction(int id) {
if (DEBUG) {
Log.v(TAG, "performContextMenuAction " + id);
}
mTextView.beginBatchEdit();
mTextView.onTextContextMenuItem(id);
mTextView.endBatchEdit();
return true;
}
@Override
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
if (mTextView != null) {
ExtractedText et = new ExtractedText();
if (mTextView.extractText(request, et)) {
if ((flags & GET_EXTRACTED_TEXT_MONITOR) != 0) {
Reflector.invokeMethodExceptionSafe(mTextView, "setExtracting",
new Reflector.TypedObject(request, ExtractedTextRequest.class));
}
return et;
}
}
return null;
}
@Override
public boolean performPrivateCommand(String action, Bundle data) {
mTextView.onPrivateIMECommand(action, data);
return true;
}
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
if (mTextView == null) {
return super.commitText(text, newCursorPosition);
}
if (text instanceof Spanned) {
Spanned spanned = (Spanned) text;
SuggestionSpan[] spans = spanned.getSpans(0, text.length(), SuggestionSpan.class);
Reflector.invokeMethodExceptionSafe(mIMM, "registerSuggestionSpansForNotification",
new Reflector.TypedObject(spans, SuggestionSpan[].class));
}
Reflector.invokeMethodExceptionSafe(mTextView, "resetErrorChangedFlag");
boolean success = super.commitText(text, newCursorPosition);
Reflector.invokeMethodExceptionSafe(mTextView, "hideErrorIfUnchanged");
return success;
}
@Override
public boolean requestCursorUpdates(int cursorUpdateMode) {
if (DEBUG) {
Log.v(TAG, "requestUpdateCursorAnchorInfo " + cursorUpdateMode);
}
// It is possible that any other bit is used as a valid flag in a future release.
// We should reject the entire request in such a case.
int KNOWN_FLAGS_MASK = InputConnection.CURSOR_UPDATE_IMMEDIATE |
InputConnection.CURSOR_UPDATE_MONITOR;
int unknownFlags = cursorUpdateMode & ~KNOWN_FLAGS_MASK;
if (unknownFlags != 0) {
if (DEBUG) {
Log.d(TAG, "Rejecting requestUpdateCursorAnchorInfo due to unknown flags." +
" cursorUpdateMode=" + cursorUpdateMode +
" unknownFlags=" + unknownFlags);
}
return false;
}
if (mIMM == null) {
// In this case, TYPE_CURSOR_ANCHOR_INFO is not handled.
// TODO: Return some notification code rather than false to indicate method that
// CursorAnchorInfo is temporarily unavailable.
return false;
}
Reflector.invokeMethodExceptionSafe(mIMM, "setUpdateCursorAnchorInfoMode",
new Reflector.TypedObject(cursorUpdateMode, int.class));
if ((cursorUpdateMode & InputConnection.CURSOR_UPDATE_IMMEDIATE) != 0) {
if (mTextView == null) {
// In this case, FLAG_CURSOR_ANCHOR_INFO_IMMEDIATE is silently ignored.
// TODO: Return some notification code for the input method that indicates
// FLAG_CURSOR_ANCHOR_INFO_IMMEDIATE is ignored.
} else {
// This will schedule a layout pass of the view tree, and the layout event
// eventually triggers IMM#updateCursorAnchorInfo.
mTextView.requestLayout();
}
}
return true;
}
@Override
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
return delEventListener != null && delEventListener.onDelEvent() || super
.deleteSurroundingText(beforeLength, afterLength);
}
public void setDelEventListener(
OnDelEventListener delEventListener) {
this.delEventListener = delEventListener;
}
public interface OnDelEventListener {
boolean onDelEvent();
}
}
注:Reflector是自定义实现的反射调用的类。
然后重写一下EditText:
DetectDelEventEditText.java
public class DetectDelEventEditText extends EditText implements View.OnKeyListener,
EditableInputConnection.OnDelEventListener {
private DelEventListener delEventListener;
/**
* 防止delEvent触发两次。
* 0:未初始化;1:使用onKey方法触发;2:使用onDelEvdent方法触发
*/
private int flag;
public DetectDelEventEditText(Context context) {
super(context);
init();
}
public DetectDelEventEditText(Context context,
@Nullable
AttributeSet attrs) {
super(context, attrs);
init();
}
public DetectDelEventEditText(Context context,
@Nullable
AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setOnKeyListener(this);
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
super.onCreateInputConnection(outAttrs);
EditableInputConnection editableInputConnection = new EditableInputConnection(this);
outAttrs.initialSelStart = getSelectionStart();
outAttrs.initialSelEnd = getSelectionEnd();
outAttrs.initialCapsMode = editableInputConnection.getCursorCapsMode(getInputType());
editableInputConnection.setDelEventListener(this);
flag = 0;
return editableInputConnection;
}
public void setDelListener(DelEventListener l) {
delEventListener = l;
}
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (flag == 2) {
return false;
}
flag = 1;
return delEventListener != null && keyCode == KeyEvent.KEYCODE_DEL && event
.getAction() == KeyEvent.ACTION_DOWN && delEventListener.delEvent();
}
@Override
public boolean onDelEvent() {
if (flag == 1) {
return false;
}
flag = 2;
return delEventListener != null && delEventListener.delEvent();
}
public interface DelEventListener {
boolean delEvent();
}
}
实验一下,bingo!终于监听到了!
需要注意的是,这里我们使用了双重保险,既监听了deleteSurroundingText方法,又设置了keyListener。因为搜狗输入法和百度输入法不响应deleteSurroundingText。就是每个输入法只选择一条路走。
更深层次暂时不去探索了。有哪位大神有更简单的方法,欢迎讨论学习,望不吝赐教!
好多朋友问Reflector这个类,自己实现了一下,供参考:
public class Reflector {
public static class TypedObject {
private Object obj;
private Class> clazz;
public TypedObject(Object obj, Class> clazz) {
this.obj = obj;
this.clazz = clazz;
}
}
public static Object invokeMethodExceptionSafe(Object target, String methodName,
TypedObject... typedObjects) {
Object[] params = null;
Class>[] paramClazzes = null;
if (typedObjects != null && typedObjects.length > 0) {
params = new Object[typedObjects.length];
paramClazzes = new Class>[typedObjects.length];
for (int i = 0; i < typedObjects.length; i++) {
params[i] = typedObjects[i].obj;
paramClazzes[i] = typedObjects[i].clazz;
}
}
Method method;
Class> targetClass = target.getClass();
do {
method = getMethod(targetClass, methodName, paramClazzes);
if (method != null) {
break;
}
targetClass = targetClass.getSuperclass();
} while (targetClass != Object.class);
if (method != null) {
if(!method.isAccessible()) {
method.setAccessible(true);
}
try {
return method.invoke(target, params);
} catch (Exception e) {
}
}
return null;
}
private static Method getMethod(Class> target, String methodName, Class>... types) {
try {
return target.getDeclaredMethod(methodName, types);
} catch (NoSuchMethodException e) {
}
return null;
}
}