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



更多相关文章

  1. Android基础教程之-------Android中两种设置全屏的方法!!!
  2. Android:view常用属性和操作方法
  3. 在android studio中用SQLiteOpenHelper()方法建立数据库
  4. Android中显示Dialog的方法
  5. android 点击EditTextView不弹出输入法
  6. Android 关于嵌套listView时onItemClick事件不响应的解决办法
  7. Android webkit keyevent 事件传递过程

随机推荐

  1. android textview空格占位符以及一些其他
  2. Android编程学习笔记 之 ListActivity源
  3. Android之SQLite数据库篇
  4. 无法将视图添加到相对布局
  5. 20169221 2016-2017-2《移动平台与androi
  6. 键盘打开时,带有片段的活动不会调整大小
  7. Android studio,第一个生成,调用成功的jni
  8. aidl 在android tv中的应用
  9. js 区分浏览器来源是PC端还是移动端
  10. Android中WebView实现Javascript调用Java