美文网首页
android 键盘输入流程

android 键盘输入流程

作者: yQ_01 | 来源:发表于2018-03-11 22:10 被阅读0次

    最近项目需要,需要做一个输入验证码的组件,就如同下图展示的样子。

    image.png
    每个验证码数字是个独立的view,不能通过一个editText来完成,基于这种实现就需要获取到每次键盘点击事件的内容,还有监听删除事件,这样才能实现每次输入完成自动将焦点放到后一个view。
    https://github.com/yan010/VerificationCodeInputView
    这里是一个我写的验证码开源库,大家可以直接使用,下面开始给搭建讲解具体的键盘输入法输入流程。
    看到这样的ui跟要求,大家的第一反应可能就是监听view的setOnKeyListener,但是经过实验发现在原生键盘上不能监听到删除事件,这样就会导致删除后不能控制当前的焦点。
    这时需要找到一种方法通用的方法来获取键盘的点击事件。在寻找这种方法之前需要完整的来看一下整个键盘唤起输入到关闭的整个流程。
    唤起键盘通过InputMethodManager类的showSoftInput方法,那么就从这方法往下看,
    public boolean showSoftInput(View view, int flags, ResultReceiver resultReceiver) {
            checkFocus();
            synchronized (mH) {
                if (mServedView != view && (mServedView == null
                        || !mServedView.checkInputConnectionProxy(view))) {
                    return false;
                }
    
                try {
                    return mService.showSoftInput(mClient, flags, resultReceiver);
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
        }
    
    mService = IInputMethodManager.Stub.asInterface(ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE));    
    

    上面代码mService是InputMethodManagerService的代理类,实现了binder机制,所以mService是binder的客户端类,执行showSoftInput,这时就要进入InputMethodManagerService类来看在服务端是怎么执行的

    InputMethodManagerService.class
        @Override
        public boolean showSoftInput(IInputMethodClient client, int flags,
                ResultReceiver resultReceiver) {
            if (!calledFromValidUser()) {
                return false;
            }
            int uid = Binder.getCallingUid();
            long ident = Binder.clearCallingIdentity();
            try {
                synchronized (mMethodMap) {
                    if (mCurClient == null || client == null
                            || mCurClient.client.asBinder() != client.asBinder()) {
                        try {
                            // We need to check if this is the current client with
                            // focus in the window manager, to allow this call to
                            // be made before input is started in it.
                            if (!mIWindowManager.inputMethodClientHasFocus(client)) {
                                Slog.w(TAG, "Ignoring showSoftInput of uid " + uid + ": " + client);
                                return false;
                            }
                        } catch (RemoteException e) {
                            return false;
                        }
                    }
    
                    if (DEBUG) Slog.v(TAG, "Client requesting input be shown");
                    return showCurrentInputLocked(flags, resultReceiver);
                }
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }
    
    boolean showCurrentInputLocked(int flags, ResultReceiver resultReceiver) {
            ...
                executeOrSendMessage(mCurMethod, mCaller.obtainMessageIOO(
                        MSG_SHOW_SOFT_INPUT, getImeShowFlags(), mCurMethod,
                        resultReceiver));
               ...
        }
    

    看到到这里会给handler发一条消息通知展示键盘。

    case MSG_SHOW_SOFT_INPUT:
                    args = (SomeArgs)msg.obj;
                    try {
                        if (DEBUG) Slog.v(TAG, "Calling " + args.arg1 + ".showSoftInput("
                                + msg.arg1 + ", " + args.arg2 + ")");
                        ((IInputMethod)args.arg1).showSoftInput(msg.arg1, (ResultReceiver)args.arg2);
                    } catch (RemoteException e) {
                    }
                    args.recycle();
                    return true;
    

    可以看到在handler中会执行一个IInputMethod的showSoftInput方法,IInputMethod是个接口他的实例类是前边的mCurMethod,

    mCurMethod = IInputMethod.Stub.asInterface(service);
    

    mCurMethod是IInputMethod的代理类,这时我们就要看InputMethodService类中的showSoftInput实现了。

    /**
             * Handle a request by the system to show the soft input area.
             */
            public void showSoftInput(int flags, ResultReceiver resultReceiver) {
                if (DEBUG) Log.v(TAG, "showSoftInput()");
                boolean wasVis = isInputViewShown();
                if (dispatchOnShowInputRequested(flags, false)) {
                    try {
                        showWindow(true);
                    } catch (BadTokenException e) {
                    }
                }
                clearInsetOfPreviousIme();
                // If user uses hard keyboard, IME button should always be shown.
                boolean showing = isInputViewShown();
                mImm.setImeWindowStatus(mToken, mStartInputToken,
                        IME_ACTIVE | (showing ? IME_VISIBLE : 0), mBackDisposition);
                if (resultReceiver != null) {
                    resultReceiver.send(wasVis != isInputViewShown()
                            ? InputMethodManager.RESULT_SHOWN
                            : (wasVis ? InputMethodManager.RESULT_UNCHANGED_SHOWN
                                    : InputMethodManager.RESULT_UNCHANGED_HIDDEN), null);
                }
            }
    
    public void showWindow(boolean showInput) {
            ...
                showWindowInner(showInput);
            ...
        }
    
    void showWindowInner(boolean showInput) {
           ...
            initialize();
            updateFullscreenMode();
            updateInputViewShown();
           ...
        }
    
    public void updateInputViewShown() {
            boolean isShown = mShowInputRequested && onEvaluateInputViewShown();
            if (mIsInputViewShown != isShown && mWindowVisible) {
                mIsInputViewShown = isShown;
                mInputFrame.setVisibility(isShown ? View.VISIBLE : View.GONE);
                if (mInputView == null) {
                    initialize();
                    View v = onCreateInputView();
                    if (v != null) {
                        setInputView(v);
                    }
                }
            }
        }
    
    public View onCreateInputView() {
            return null;
        }
    

    最终通过onCreateInputView方法返回一个view类,这个类在当前类中是个空实现,需要子类继承并重写这个方法,这样就展示出了一个键盘。
    到这里键盘的创建过程已经完成。
    这时如果有点击键盘的操作时,事件是怎么完成的呢?
    先看InputMethodService中获取到键盘点击事件的方法sendKeyChar

     public void sendKeyChar(char charCode) {
            switch (charCode) {
                case '\n': // Apps may be listening to an enter key to perform an action
                    if (!sendDefaultEditorAction(true)) {
                        sendDownUpKeyEvents(KeyEvent.KEYCODE_ENTER);
                    }
                    break;
                default:
                    // Make sure that digits go through any text watcher on the client side.
                    if (charCode >= '0' && charCode <= '9') {
                        sendDownUpKeyEvents(charCode - '0' + KeyEvent.KEYCODE_0);
                    } else {
                        InputConnection ic = getCurrentInputConnection();
                        if (ic != null) {
                            ic.commitText(String.valueOf(charCode), 1);
                        }
                    }
                    break;
            }
        }
    public void sendDownUpKeyEvents(int keyEventCode) {
            InputConnection ic = getCurrentInputConnection();
            if (ic == null) return;
            long eventTime = SystemClock.uptimeMillis();
            ic.sendKeyEvent(new KeyEvent(eventTime, eventTime,
                    KeyEvent.ACTION_DOWN, keyEventCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
                    KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE));
            ic.sendKeyEvent(new KeyEvent(eventTime, SystemClock.uptimeMillis(),
                    KeyEvent.ACTION_UP, keyEventCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
                    KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE));
        }
    

    方法中会根据传过来的字符区分类型进行不同处理,可以看到回退的处理会被单独处理,其他字符如果是数字会在进行一步处理,通过sendKeyEvent方法传递给InputConnection连接对象,其他的则执行commitText。那么这时就需要关注这个InputConnection对象的内部实现,因为这是键盘返回参数的第一优先级处理的对象,查看TextView会发现内部会有一个onCreateInputConnection方法,会创建系统写好的EditableInputConnection对象,onCreateInputConnection这个方法是个可被重写的方法这也是完成上边所说需求的关键方法,我们自己创建一个InputConnection子类,重写其中方法就可以拦截键盘事件。
    回到输入流程我们查看EditableInputConnection的内部实现sendKeyEvent的过程

     public boolean sendKeyEvent(KeyEvent event) {
            mIMM.dispatchKeyEventFromInputMethod(mTargetView, event);
            return false;
        }
    

    方法中会执行InputMethodManager的dispatchKeyEventFromInputMethod方法

    public void dispatchKeyEventFromInputMethod(@Nullable View targetView,
                @NonNull KeyEvent event) {
            synchronized (mH) {
                ViewRootImpl viewRootImpl = targetView != null ? targetView.getViewRootImpl() : null;
                if (viewRootImpl == null) {
                    if (mServedView != null) {
                        viewRootImpl = mServedView.getViewRootImpl();
                    }
                }
                if (viewRootImpl != null) {
                    viewRootImpl.dispatchKeyFromIme(event);
                }
            }
        }
    

    看到上边代码会获取到目标view的ViewRootImpl对象,执行ViewRootImpl的dispatchKeyFromIme方法

    public void dispatchKeyFromIme(KeyEvent event) {
            Message msg = mHandler.obtainMessage(MSG_DISPATCH_KEY_FROM_IME, event);
            msg.setAsynchronous(true);
            mHandler.sendMessage(msg);
        }
    
    case MSG_DISPATCH_KEY_FROM_IME: {
                    if (LOCAL_LOGV) Log.v(
                        TAG, "Dispatching key "
                        + msg.obj + " from IME to " + mView);
                    KeyEvent event = (KeyEvent)msg.obj;
                    if ((event.getFlags()&KeyEvent.FLAG_FROM_SYSTEM) != 0) {
                        // The IME is trying to say this event is from the
                        // system!  Bad bad bad!
                        //noinspection UnusedAssignment
                        event = KeyEvent.changeFlags(event, event.getFlags() &
                                ~KeyEvent.FLAG_FROM_SYSTEM);
                    }
                    enqueueInputEvent(event, null, QueuedInputEvent.FLAG_DELIVER_POST_IME, true);
                } break;
    
    void enqueueInputEvent(InputEvent event,
                InputEventReceiver receiver, int flags, boolean processImmediately) {
            ...
            if (processImmediately) {
                doProcessInputEvents();
            } else {
                scheduleProcessInputEvents();
            }
        }
    
    void doProcessInputEvents() {
           ...
                deliverInputEvent(q);
           ...
        }
    private void deliverInputEvent(QueuedInputEvent q) {
            ...
            InputStage stage;
            if (q.shouldSendToSynthesizer()) {
                stage = mSyntheticInputStage;
            } else {
                stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
            }
    
            if (stage != null) {
                stage.deliver(q);
            } else {
                finishInputEvent(q);
            }
        }
    

    经过一系列的传递最终stage对象进行分发,stage对象有很多子类,这里的stage的最终实现类是ViewPostImeInputStage

     public final void deliver(QueuedInputEvent q) {
                if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) {
                    forward(q);
                } else if (shouldDropInputEvent(q)) {
                    finish(q, false);
                } else {
                    apply(q, onProcess(q));
                }
            }
    //重写了onProcess方法
      @Override
            protected int onProcess(QueuedInputEvent q) {
                if (q.mEvent instanceof KeyEvent) {
                    return processKeyEvent(q);
                } else {
                    final int source = q.mEvent.getSource();
                    if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
                        return processPointerEvent(q);
                    } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
                        return processTrackballEvent(q);
                    } else {
                        return processGenericMotionEvent(q);
                    }
                }
            }
    
           private int processKeyEvent(QueuedInputEvent q) {
                final KeyEvent event = (KeyEvent)q.mEvent;
    
                // Deliver the key to the view hierarchy.
                if (mView.dispatchKeyEvent(event)) {
                    return FINISH_HANDLED;
                }
                ....
          }
    

    重写了onProcess方法并执行了mView.dispatchKeyEvent(event),mView对象可以知道是DecorView对象,这时事件也就传递到了Decorview中

     @Override
        public boolean dispatchKeyEvent(KeyEvent event) {
            final int keyCode = event.getKeyCode();
            final int action = event.getAction();
            final boolean isDown = action == KeyEvent.ACTION_DOWN;
    
            if (isDown && (event.getRepeatCount() == 0)) {
                // First handle chording of panel key: if a panel key is held
                // but not released, try to execute a shortcut in it.
                if ((mWindow.mPanelChordingKey > 0) && (mWindow.mPanelChordingKey != keyCode)) {
                    boolean handled = dispatchKeyShortcutEvent(event);
                    if (handled) {
                        return true;
                    }
                }
    
                // If a panel is open, perform a shortcut on it without the
                // chorded panel key
                if ((mWindow.mPreparedPanel != null) && mWindow.mPreparedPanel.isOpen) {
                    if (mWindow.performPanelShortcut(mWindow.mPreparedPanel, keyCode, event, 0)) {
                        return true;
                    }
                }
            }
    
            if (!mWindow.isDestroyed()) {
                final Window.Callback cb = mWindow.getCallback();
                final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event)
                        : super.dispatchKeyEvent(event);
                if (handled) {
                    return true;
                }
            }
    
            return isDown ? mWindow.onKeyDown(mFeatureId, event.getKeyCode(), event)
                    : mWindow.onKeyUp(mFeatureId, event.getKeyCode(), event);
        }
    

    在Decorview中获取当前window的callback,这个callback会回调到activity中,当不存在回调时则会执行父类的dispatchKeyEvent方法,如果window已经被销毁的情况,则执行phonewindow的onKeyDown和onKeyUp,下边分别分析下边三种情况。
    第一种回调activity的dispatchKeyEvent方法。

        public boolean dispatchKeyEvent(KeyEvent event) {
            onUserInteraction();
    
            // Let action bars open menus in response to the menu key prioritized over
            // the window handling it
            final int keyCode = event.getKeyCode();
            if (keyCode == KeyEvent.KEYCODE_MENU &&
                    mActionBar != null && mActionBar.onMenuKeyEvent(event)) {
                return true;
            }
    
            Window win = getWindow();
            if (win.superDispatchKeyEvent(event)) {
                return true;
            }
            View decor = mDecor;
            if (decor == null) decor = win.getDecorView();
            return event.dispatch(this, decor != null
                    ? decor.getKeyDispatcherState() : null, this);
        }
    

    方法中会优先执行window的superDispatchKeyEvent方法看是否处理掉了这个事件

    PhoneWindow.java
     @Override
        public boolean superDispatchKeyEvent(KeyEvent event) {
            return mDecor.superDispatchKeyEvent(event);
        }
    DecorView.java
        public boolean superDispatchKeyEvent(KeyEvent event) {
            // Give priority to closing action modes if applicable.
            if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
                final int action = event.getAction();
                // Back cancels action modes first.
                if (mPrimaryActionMode != null) {
                    if (action == KeyEvent.ACTION_UP) {
                        mPrimaryActionMode.finish();
                    }
                    return true;
                }
            }
    
            return super.dispatchKeyEvent(event);
        }
    
    ViewGroup.java
        @Override
        public boolean dispatchKeyEvent(KeyEvent event) {
            if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onKeyEvent(event, 1);
            }
    
            if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
                    == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
                if (super.dispatchKeyEvent(event)) {
                    return true;
                }
            } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
                    == PFLAG_HAS_BOUNDS) {
                if (mFocused.dispatchKeyEvent(event)) {
                    return true;
                }
            }
    
            if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
            }
            return false;
        }
    

    这时其实就跟第二种情况super.dispatchKeyEvent(event)一样了,第二种情况则是从这里开始。执行到ViewGroup中,如果此ViewGroup是focused或者具体的大小被设置了(有边界),则执行父类view的dispatchKeyEvent,如果子类中有设置了foucus的对象,则执行foucus的dispatchKeyEvent,如果都没有则返回false。

        public boolean dispatchKeyEvent(KeyEvent event) {
            if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onKeyEvent(event, 0);
            }
    
            // Give any attached key listener a first crack at the event.
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
                return true;
            }
    
            if (event.dispatch(this, mAttachInfo != null
                    ? mAttachInfo.mKeyDispatchState : null, this)) {
                return true;
            }
    
            if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
            }
            return false;
        }
    

    方法中会优先调用onKeyListener,如果它非空且view是ENABLED状态,监听器优先触发,没有设置监听则调用KeyEvent.dispatch方法,并将view对象本身作为参数传递进去,view的各种callback方法在这里被触发。这里我们进入event.dispatch方法来看下实现

        public final boolean dispatch(Callback receiver, DispatcherState state,
                Object target) {
            switch (mAction) {
                case ACTION_DOWN: {
                    mFlags &= ~FLAG_START_TRACKING;
                    if (DEBUG) Log.v(TAG, "Key down to " + target + " in " + state
                            + ": " + this);
                    boolean res = receiver.onKeyDown(mKeyCode, this);
                    if (state != null) {
                        if (res && mRepeatCount == 0 && (mFlags&FLAG_START_TRACKING) != 0) {
                            if (DEBUG) Log.v(TAG, "  Start tracking!");
                            state.startTracking(this, target);
                        } else if (isLongPress() && state.isTracking(this)) {
                            try {
                                if (receiver.onKeyLongPress(mKeyCode, this)) {
                                    if (DEBUG) Log.v(TAG, "  Clear from long press!");
                                    state.performedLongPress(this);
                                    res = true;
                                }
                            } catch (AbstractMethodError e) {
                            }
                        }
                    }
                    return res;
                }
                case ACTION_UP:
                    if (DEBUG) Log.v(TAG, "Key up to " + target + " in " + state
                            + ": " + this);
                    if (state != null) {
                        state.handleUpEvent(this);
                    }
                    return receiver.onKeyUp(mKeyCode, this);
                case ACTION_MULTIPLE:
                    final int count = mRepeatCount;
                    final int code = mKeyCode;
                    if (receiver.onKeyMultiple(code, count, this)) {
                        return true;
                    }
                    if (code != KeyEvent.KEYCODE_UNKNOWN) {
                        mAction = ACTION_DOWN;
                        mRepeatCount = 0;
                        boolean handled = receiver.onKeyDown(code, this);
                        if (handled) {
                            mAction = ACTION_UP;
                            receiver.onKeyUp(code, this);
                        }
                        mAction = ACTION_MULTIPLE;
                        mRepeatCount = count;
                        return handled;
                    }
                    return false;
            }
            return false;
        }
    

    会执行相应view的callback的onKeyDown,onKeyUp,onKeyMultiple方法,同时处理内部的状态信息。这时会执行activity和View的onKeyDown方法。

     public boolean onKeyDown(int keyCode, KeyEvent event)  {
            if (keyCode == KeyEvent.KEYCODE_BACK) {
                if (getApplicationInfo().targetSdkVersion
                        >= Build.VERSION_CODES.ECLAIR) {
                    event.startTracking();
                } else {
                    onBackPressed();
                }
                return true;
            }
    
            if (mDefaultKeyMode == DEFAULT_KEYS_DISABLE) {
                return false;
            } else if (mDefaultKeyMode == DEFAULT_KEYS_SHORTCUT) {
                Window w = getWindow();
                if (w.hasFeature(Window.FEATURE_OPTIONS_PANEL) &&
                        w.performPanelShortcut(Window.FEATURE_OPTIONS_PANEL, keyCode, event,
                                Menu.FLAG_ALWAYS_PERFORM_CLOSE)) {
                    return true;
                }
                return false;
            } else if (keyCode == KeyEvent.KEYCODE_TAB) {
                // Don't consume TAB here since it's used for navigation. Arrow keys
                // aren't considered "typing keys" so they already won't get consumed.
                return false;
            } else {
                // Common code for DEFAULT_KEYS_DIALER & DEFAULT_KEYS_SEARCH_*
                boolean clearSpannable = false;
                boolean handled;
                if ((event.getRepeatCount() != 0) || event.isSystem()) {
                    clearSpannable = true;
                    handled = false;
                } else {
                    handled = TextKeyListener.getInstance().onKeyDown(
                            null, mDefaultKeySsb, keyCode, event);
                    if (handled && mDefaultKeySsb.length() > 0) {
                        // something useable has been typed - dispatch it now.
    
                        final String str = mDefaultKeySsb.toString();
                        clearSpannable = true;
    
                        switch (mDefaultKeyMode) {
                        case DEFAULT_KEYS_DIALER:
                            Intent intent = new Intent(Intent.ACTION_DIAL,  Uri.parse("tel:" + str));
                            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                            startActivity(intent);
                            break;
                        case DEFAULT_KEYS_SEARCH_LOCAL:
                            startSearch(str, false, null, false);
                            break;
                        case DEFAULT_KEYS_SEARCH_GLOBAL:
                            startSearch(str, false, null, true);
                            break;
                        }
                    }
                }
                if (clearSpannable) {
                    mDefaultKeySsb.clear();
                    mDefaultKeySsb.clearSpans();
                    Selection.setSelection(mDefaultKeySsb,0);
                }
                return handled;
            }
        }
    

    经过这些处理如果还是没有完成,则开始第三种情况mWindow.onKeyDown(mFeatureId, event.getKeyCode(), event)执行window的onKeyDowm。
    到这里也就完整的完成了整个输入流程的讲解。其中有很大篇幅讲解的是android系统中key事件的传递流程,这个可以跟输入法流程分开来看。

    1. View的各种KeyEvent.Callback接口早于Activity的对应接口被调用;

    2. 整个处理环节中只要有一处表明处理掉了,则处理结束,不在往下传递;

    3. 各种Callback接口的处理优先级低于监听器,也就是说各种onXXXListener的方法优先被调用。

    image.png

    相关文章

      网友评论

          本文标题:android 键盘输入流程

          本文链接:https://www.haomeiwen.com/subject/ajyxxftx.html