android

您所在的位置:网站首页 监听返回按钮无效 android

android

2023-11-01 21:54| 来源: 网络整理| 查看: 265

前些天测试部的妹子测出来一个奇葩的问题,使用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; } }



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3