上一篇文章探究了用户点击输入框,体系主动拉起IME软键盘的整个作业流程。在这一篇文章中,将从另外一个角度出发,看看在软键盘按下字母时,IME是怎么将它转化为中文字符,当选择某个中文字符时,体系又是怎么将字符填充到输入框中的。
Q1:软键盘是怎么将拼音转化成汉字的?
当用户在界面上敲下hong这个拼音时,输入法是怎么将它转化成可选的中文汉字的?其根本在于中文字典的保护和字典索引逻辑的规划。虽然各个输入法的细节逻辑上都有所不同,但核心的规划思维都是类似的。
以开源软件RIME为例,它的中文字典如下所示:
能够看到,字典中一般由三部分内容组成,分别是 文字——编码——词频,除了文字是必选项,其它的都归于可选的内容。比如在其它的某些输入法的英文字典中,单纯以以下形式体现也是完全OK的:
animator
apple
能够看到,由于英文的特别性,它或许并不需要编码;词频也是能够省略的,就单纯依照从上到下的次序作为词频。
字典索引逻辑也是八仙过海各显神通,实际上每种言语编好字典今后,问题就转化成了 怎么从字典中找出给定编码的所有文字,并按定义的频次由高到低排列。这个编码问题就不难了吧,90%以上的工程师都能完成这个功用。
但实际上这只是完成了一个最简略的输入,现代输入法为了更好地进步输入功率,在能有的基础上加了很多花活,来让它变得好用,举例来说:
在输入法上键入天天两个字后,它会主动引荐补全高兴两个字。其实这仍是基于现有字典的运用,IME在字典的基础上开发了一个主动补全器,从字典里索引能和天天两个字调配起来的词组并作为引荐内容显现。
更进一步的操作,还有依据用户习惯动态调整词频,甚至运用言语模型优化输出结果等做法。因而,IME的门槛并不在于其自身的输入技能,而在于怎么让它变得更智能上。
Q2:软键盘的文字是怎么传输到输入框中的?
输入法与EditText之间能够交流,它们之间必定存在一个跨进程桥梁,这个桥梁便是IInputContext。为了了解这个进程,咱们先得回忆一下上篇文章调起输入法进程的内容。
在调起输入法的进程中,会调用到InputMethodManager的startInputInner办法,其中有如下片段:
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中,作为IMMS与IMS通讯的桥梁。
事实确实如此,详细的追寻细节这儿不在阐述了,简略地写一下调用链吧:
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;
}
这儿调用到了InputConnection的commitText办法里了。还记得前文提到的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处理方式即可。
来看一下TextView的onKeyDown办法,代码如下:
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有TextKeyListener、DialerKeyListener、DigitsKeyListener等,但它们都存在一个一起的父类,即BaseKeyListener。为了方便接下来的剖析,本文就以BaseKeyListener的onKeyOther办法来持续追寻。
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控件,将修改后的文本显现到界面上。
至此,将软键盘输入的文字传输到文本框控件的整个流程已履行完毕。

