上一篇文章探究了用户点击输入框,体系主动拉起IME软键盘的整个作业流程。在这一篇文章中,将从另外一个角度出发,看看在软键盘按下字母时,IME是怎么将它转化为中文字符,当选择某个中文字符时,体系又是怎么将字符填充到输入框中的。

Q1:软键盘是怎么将拼音转化成汉字的?

当用户在界面上敲下hong这个拼音时,输入法是怎么将它转化成可选的中文汉字的?其根本在于中文字典的保护和字典索引逻辑的规划。虽然各个输入法的细节逻辑上都有所不同,但核心的规划思维都是类似的。

开源软件RIME为例,它的中文字典如下所示:

Android 11 InputMethod作业原理(二)

能够看到,字典中一般由三部分内容组成,分别是 文字——编码——词频,除了文字是必选项,其它的都归于可选的内容。比如在其它的某些输入法的英文字典中,单纯以以下形式体现也是完全OK的:

animator
apple

能够看到,由于英文的特别性,它或许并不需要编码;词频也是能够省略的,就单纯依照从上到下的次序作为词频。

字典索引逻辑也是八仙过海各显神通,实际上每种言语编好字典今后,问题就转化成了 怎么从字典中找出给定编码的所有文字,并按定义的频次由高到低排列。这个编码问题就不难了吧,90%以上的工程师都能完成这个功用。

但实际上这只是完成了一个最简略的输入,现代输入法为了更好地进步输入功率,在能有的基础上加了很多花活,来让它变得好用,举例来说:

在输入法上键入天天两个字后,它会主动引荐补全高兴两个字。其实这仍是基于现有字典的运用,IME在字典的基础上开发了一个主动补全器,从字典里索引能和天天两个字调配起来的词组并作为引荐内容显现。

更进一步的操作,还有依据用户习惯动态调整词频,甚至运用言语模型优化输出结果等做法。因而,IME的门槛并不在于其自身的输入技能,而在于怎么让它变得更智能上。

Q2:软键盘的文字是怎么传输到输入框中的?

输入法与EditText之间能够交流,它们之间必定存在一个跨进程桥梁,这个桥梁便是IInputContext。为了了解这个进程,咱们先得回忆一下上篇文章调起输入法进程的内容。

在调起输入法的进程中,会调用到InputMethodManagerstartInputInner办法,其中有如下片段:

InputMethodManager.java
 boolean startInputInner(@StartInputReason int startInputReason,
            @Nullable IBinder windowGainingFocus, @StartInputFlags int startInputFlags,
            @SoftInputModeFlags int softInputMode, int windowFlags) {
        ...
		//Attention !!!
        InputConnection ic = view.onCreateInputConnection(tba);
        if (DEBUG) Log.v(TAG, "Starting input: tba=" + tba + " ic=" + ic);
        synchronized (mH) {
            ...
        if (ic != null) {
                mCursorSelStart = tba.initialSelStart;
                mCursorSelEnd = tba.initialSelEnd;
                mCursorCandStart = -1;
                mCursorCandEnd = -1;
                mCursorRect.setEmpty();
                mCursorAnchorInfo = null;
                final Handler icHandler;
                missingMethodFlags = InputConnectionInspector.getMissingMethodFlags(ic);
                if ((missingMethodFlags & InputConnectionInspector.MissingMethodFlags.GET_HANDLER)
                        != 0) {
                    // InputConnection#getHandler() is not implemented.
                    icHandler = null;
                } else {
                    icHandler = ic.getHandler();
                }
				//Attention!!!
                servedContext = new ControlledInputConnectionWrapper(
                        icHandler != null ? icHandler.getLooper() : vh.getLooper(), ic, this, view);
            } else {
                servedContext = null;
                missingMethodFlags = 0;
            }
            mServedInputConnectionWrapper = servedContext;
         ...
        return true;
    }

请注意这句话, InputConnection ic = view.onCreateInputConnection(tba);,它创立了一个InputConnection目标,用于将IInputContext接收到的事情转发到相应的EditText上。

默许情况下,View的onCreateInputConnection办法是一个空实现,即:

View.java
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
        return null;
}

TextView这个办法对它进行了重载,变成了:

TextView.java
 @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());
                outAttrs.setInitialSurroundingText(mText);
                return ic;
            }
        }
        return null;
    }

当满意条件mText instanceof Editable时,即控件为EditText时,才会创立EditableInputConnection目标。

再回到InputMethodManager中,持续往下看,能够发现现已满意ic != null的条件了,紧接着创立了ControlledInputConnectionWrapper目标,并把EditText供给的EditableInputConnection封装了进去。

在这儿创立的ControlledInputConnectionWrapper目标但是大有考究,来扒一扒它的承继联系,能够看到以下的链路——ControlledInputConnectionWrapper->IInputConnectionWrapper->IInputContext.Stub,终究它承继的是IInputContext.Stub。因而能够大胆地估测,ControlledInputConnectionWrapper终究会经过Binder传递到IMS中,作为IMMSIMS通讯的桥梁。

事实确实如此,详细的追寻细节这儿不在阐述了,简略地写一下调用链吧: InputMethodManagerService#startInputOrWindowGainedFocus->InputMethodManagerService#startInputOrWindowGainedFocusInternalLocked->InputMethodManagerService#startInputUncheckedLocked->InputMethodManagerService#executeOrSendMessage->IInputMethodWrapper#startInput->>IInputMethodWrapper#executeMessage

现在来看,软键盘输入一段文字后,发生了什么事情。软键盘向EditText提交文字的代码如下:

getCurrentInputConnection().commitText(String.valueOf(charCode), 1);

持续盯梢后如下:

InputConnectionWrapper.java
public boolean commitText(CharSequence text, int newCursorPosition) {
        try {
            mIInputContext.commitText(text, newCursorPosition);
            notifyUserActionIfNecessary();
            return true;
        } catch (RemoteException e) {
            return false;
        }
    }

能够很清楚地看到,这儿发生了IPC通讯,依据之前的剖析,能够直接去IPC的客户端ControlledInputConnectionWrapper中追寻后续的内容:

IInputConnectionWrapper.java
public void commitText(CharSequence text, int newCursorPosition) {
        dispatchMessage(obtainMessageIO(DO_COMMIT_TEXT, newCursorPosition, text));
 }
...
  case DO_COMMIT_TEXT: {
                InputConnection ic = getInputConnection();
                if (ic == null || !isActive()) {
                    Log.w(TAG, "commitText on inactive InputConnection");
                    return;
                }
                ic.commitText((CharSequence)msg.obj, msg.arg1);
                return;
            }

这儿调用到了InputConnectioncommitText办法里了。还记得前文提到的InputConnection效果吗,它负责将IInputContext接收到的事情转发到相应的EditText上。而在之前的剖析中能够发现,获取到焦点的EditText每次都会创立一个新的EditableInputConnection目标。

EditableInputConnection.java
  @Override
    public boolean commitText(CharSequence text, int newCursorPosition) {
        if (mTextView == null) {
            return super.commitText(text, newCursorPosition);
        }
        mTextView.resetErrorChangedFlag();
        boolean success = super.commitText(text, newCursorPosition);
        mTextView.hideErrorIfUnchanged();
        return success;
    }

持续往下看:

BaseInputConnection.java
  public boolean commitText(CharSequence text, int newCursorPosition) {
        if (DEBUG) Log.v(TAG, "commitText " + text);
        replaceText(text, newCursorPosition, false);
        sendCurrentText();
        return true;
    }
 private void sendCurrentText() {
        	...
            // Otherwise, revert to the special key event containing
            // the actual characters.
            KeyEvent event = new KeyEvent(SystemClock.uptimeMillis(),
                    content.toString(), KeyCharacterMap.VIRTUAL_KEYBOARD, 0);
            sendKeyEvent(event);
            content.clear();
        }
    }

sendCurrentText里有一个非常要害的操作,便是把输入法输入的文字封装成了KeyEvent,然后调用sendKeyEvent敞开了事情分发流程。

android事情分发机制这儿便不再阐述了,相信咱们都比较熟悉了。咱们只需要关注TextView对这个特别的KeyEvent处理方式即可。

来看一下TextViewonKeyDown办法,代码如下:

TextView.java
@Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        final int which = doKeyDown(keyCode, event, null);
        if (which == KEY_EVENT_NOT_HANDLED) {
            return super.onKeyDown(keyCode, event);
        }
        return true;
    }

持续看doKeyDown办法:

TextView.java
private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) {
       ...
        if (mEditor != null && mEditor.mKeyListener != null) {
            boolean doDown = true;
            if (otherEvent != null) {
                try {
                    beginBatchEdit();
					//Attention here;
                    final boolean handled = mEditor.mKeyListener.onKeyOther(this, (Editable) mText,
                            otherEvent);
                    hideErrorIfUnchanged();
                    doDown = false;
                    if (handled) {
                        return KEY_EVENT_HANDLED;
                    }
                } catch (AbstractMethodError e) {
                    // onKeyOther was added after 1.0, so if it isn't
                    // implemented we need to try to dispatch as a regular down.
                } finally {
                    endBatchEdit();
                }
            }
            if (doDown) {
                beginBatchEdit();
                final boolean handled = mEditor.mKeyListener.onKeyDown(this, (Editable) mText,
                        keyCode, event);
                endBatchEdit();
                hideErrorIfUnchanged();
                if (handled) return KEY_DOWN_HANDLED_BY_KEY_LISTENER;
            }
        }
       ...
        return mPreventDefaultMovement && !KeyEvent.isModifierKey(keyCode)
                ? KEY_EVENT_HANDLED : KEY_EVENT_NOT_HANDLED;
    }

要害在onKeyOther这个监听,依据INPUT_TYPE类型的差别,keyListener也各不相同,或许存在的Listener有TextKeyListenerDialerKeyListenerDigitsKeyListener等,但它们都存在一个一起的父类,即BaseKeyListener。为了方便接下来的剖析,本文就以BaseKeyListeneronKeyOther办法来持续追寻。

BaseKeyListener.java
public boolean onKeyOther(View view, Editable content, KeyEvent event) {
       ...
        CharSequence text = event.getCharacters();
        if (text == null) {
            return false;
        }
		//Key Sentence
        content.replace(selectionStart, selectionEnd, text);
        return true;
    }
SpannableStringBuilder.java
 public SpannableStringBuilder replace(final int start, final int end,
            CharSequence tb, int tbstart, int tbend) {
       ...
        sendTextChanged(textWatchers, start, origLen, newLen);
        sendAfterTextChanged(textWatchers);
        // Span watchers need to be called after text watchers, which may update the layout
        sendToSpanWatchers(start, end, newLen - origLen);
        return this;
    }

这儿修改了TextView相关的SpannableStringBuilder的文字内容,并经过sendTextChanged通知TextView控件文本内容发生了改变。

TextView.java
  public void onTextChanged(CharSequence buffer, int start, int before, int after) {
            TextView.this.handleTextChanged(buffer, start, before, after);
            if (AccessibilityManager.getInstance(mContext).isEnabled()
                    && (isFocused() || isSelected() && isShown())) {
                sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after);
                mBeforeText = null;
            }
 }

要害句子handleTextChanged

TextView.java
 void handleTextChanged(CharSequence buffer, int start, int before, int after) {
    	...
        if (ims == null || ims.mBatchEditNesting == 0) {
            updateAfterEdit();
        }
        ...
    }

要害句子updateAfterEdit,在这个办法里调用了invalidate办法,开端刷新TextView控件,将修改后的文本显现到界面上。 至此,将软键盘输入的文字传输到文本框控件的整个流程已履行完毕。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。