android——解决部分输入法监听回退键无效的问题

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



你可能感兴趣的:(android)