一、字符输入起始:
LatinIME字符输入的初始方法是LatinIME类中的onCodeInput方法:
<span style="font-size:18px;"><span style="font-size:18px;"><span style="font-size:18px;"> // Implementation of {@link KeyboardActionListener}.
@Override
public void onCodeInput(final int codePoint, final int x, final int y,
final boolean isKeyRepeat) {
final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
// x and y include some padding, but everything down the line (especially native
// code) needs the coordinates in the keyboard frame.
// TODO: We should reconsider which coordinate system should be used to represent
// keyboard event. Also we should pull this up -- LatinIME has no business doing
// this transformation, it should be done already before calling onCodeInput.
final int keyX = mainKeyboardView.getKeyX(x);
final int keyY = mainKeyboardView.getKeyY(y);
final int codeToSend;
if (Constants.CODE_SHIFT == codePoint) {
// TODO: Instead of checking for alphabetic keyboard here, separate keycodes for
// alphabetic shift and shift while in symbol layout.
final Keyboard currentKeyboard = mKeyboardSwitcher.getKeyboard();
if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) {
codeToSend = codePoint;
} else {
codeToSend = Constants.CODE_SYMBOL_SHIFT;
}
} else {
codeToSend = codePoint;
}
if (Constants.CODE_SHORTCUT == codePoint) {
mSubtypeSwitcher.switchToShortcutIME(this);
// Still call the *#onCodeInput methods for readability.
}
final Event event = createSoftwareKeypressEvent(codeToSend, keyX, keyY, isKeyRepeat);
final InputTransaction completeInputTransaction =
mInputLogic.onCodeInput(mSettings.getCurrent(), event,
mKeyboardSwitcher.getKeyboardShiftMode(),
mKeyboardSwitcher.getCurrentKeyboardScriptId(), mHandler);
updateStateAfterInputTransaction(completeInputTransaction);
mKeyboardSwitcher.onCodeInput(codePoint, getCurrentAutoCapsState(),
getCurrentRecapitalizeState());
}</span></span></span>
首先通过createSoftwareKeypressEvent方法创建输入事件(Event),接着就开始调用InputLogic中的onCodeInput进行具体的字符输入操作(核心处理流程)
二、核心处理流程:
<span style="font-size:18px;"><span style="font-size:18px;"> public InputTransaction onCodeInput(final SettingsValues settingsValues, final Event event,
final int keyboardShiftMode,
// TODO: remove these arguments
final int currentKeyboardScriptId, final LatinIME.UIHandler handler) {
final Event processedEvent = mWordComposer.processEvent(event);
final InputTransaction inputTransaction = new InputTransaction(settingsValues,
processedEvent, SystemClock.uptimeMillis(), mSpaceState,
getActualCapsMode(settingsValues, keyboardShiftMode));
if (processedEvent.mKeyCode != Constants.CODE_DELETE
|| inputTransaction.mTimestamp > mLastKeyTime + Constants.LONG_PRESS_MILLISECONDS) {
mDeleteCount = 0;
}
mLastKeyTime = inputTransaction.mTimestamp;
mConnection.beginBatchEdit();
if (!mWordComposer.isComposingWord()) {
// TODO: is this useful? It doesn't look like it should be done here, but rather after
// a word is committed.
mIsAutoCorrectionIndicatorOn = false;
}
// TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state.
if (processedEvent.mCodePoint != Constants.CODE_SPACE) {
cancelDoubleSpacePeriodCountdown();
}
Event currentEvent = processedEvent;
while (null != currentEvent) {
if (currentEvent.isConsumed()) {
handleConsumedEvent(currentEvent, inputTransaction);
} else if (currentEvent.isFunctionalKeyEvent()) {
handleFunctionalEvent(currentEvent, inputTransaction, currentKeyboardScriptId,
handler);
} else {
handleNonFunctionalEvent(currentEvent, inputTransaction, handler);
}
currentEvent = currentEvent.mNextEvent;
}
if (!inputTransaction.didAutoCorrect() && processedEvent.mKeyCode != Constants.CODE_SHIFT
&& processedEvent.mKeyCode != Constants.CODE_CAPSLOCK
&& processedEvent.mKeyCode != Constants.CODE_SWITCH_ALPHA_SYMBOL)
mLastComposedWord.deactivate();
if (Constants.CODE_DELETE != processedEvent.mKeyCode) {
mEnteredText = null;
}
mConnection.endBatchEdit();
return inputTransaction;
}
</span></span>
这其中使用到的RichInputConnection类,是通过组合的方式引入了InputConnection类,InputConnection类是Android中连接输入法与调用输入法控件之间的关键类,它提供了诸如提交字符、获取光标左右文本、删除光标附近字符等重要的方法。
下面就来分析下onCodeInput方法,所有的Event最终都是通过CombinerChain这个类来管理的,而onCodeInput首先是调用WordComposer中的processEvent方法来处理第一步创建的Event。而WordComposer在源码中的解释是存储当前构成词以及类似于临近key code之类信息的地方。
<span style="font-size:18px;"><span style="font-size:18px;"> /**
* Process an event and return an event, and return a processed event to apply.
* @param event the unprocessed event.
* @return the processed event. Never null, but may be marked as consumed.
*/
@Nonnull
public Event processEvent(final Event event) {
final Event processedEvent = mCombinerChain.processEvent(mEvents, event);
// The retained state of the combiner chain may have changed while processing the event,
// so we need to update our cache.
refreshTypedWordCache();
mEvents.add(event);
return processedEvent;
}</span></span>
可以看到WordComposer中的processEvent是调用了CombinerChain中的processEvent来处理onCodeInput传入的Event(输入事件):
<span style="font-size:18px;"> /**
* Process an event through the combining chain, and return a processed event to apply.
* @param previousEvents the list of previous events in this composition
* @param newEvent the new event to process
* @return the processed event. It may be the same event, or a consumed event, or a completely
* new event. However it may never be null.
*/
@Nonnull
public Event processEvent(final ArrayList<Event> previousEvents, final Event newEvent) {
mLog.debug("CombinerChain mCombinedText : " + mCombinedText);
final ArrayList<Event> modifiablePreviousEvents = new ArrayList<>(previousEvents);
Event event = newEvent;
for (final Combiner combiner : mCombiners) {
// A combiner can never return more than one event; it can return several
// code points, but they should be encapsulated within one event.
event = combiner.processEvent(modifiablePreviousEvents, event);
mLog.debug("processEvent circle num : " + mCombiners.size());
if (event.isConsumed()) {
mLog.debug("event consumed");
// If the event is consumed, then we don't pass it to subsequent combiners:
// they should not see it at all.
break;
}
}
updateStateFeedback();
return event;
}</span>
可以看到,CombinerChain会将处理好的Event返回给WordComposer,而WordComposer会进一步处理返回的Event,最后将处理好的Event在返回给InputLogic(这两个类的具体逻辑后面再进行分析)。
InputLogic接到处理好的Event后,会创建一个InputTransaction(它封装了输入事件的一个单一事务)。接下来是判断是否是删除操作以及设定时间戳。
在上面的步骤都完成后,onCodeInput就调用RichInputConnection的beginBatchEdit方法开始进行字符的输入。
while语句中是具体的处理流程,首先是判断currentEvent的状态,并根据不同的状态调用不同的处理方法,handleConsumedEvent(暂置):好像是再次处理已经处理过的事件(?),打的log中没有出现过,所以不确定其处理的是什么类型的事件。
<span style="font-size:18px;">/**
* Handle a consumed event.
*
* Consumed events represent events that have already been consumed, typically by the
* combining chain.
*
* @param event The event to handle.
* @param inputTransaction The transaction in progress.
*/
private void handleConsumedEvent(final Event event, final InputTransaction inputTransaction) {
// A consumed event may have text to commit and an update to the composing state, so
// we evaluate both. With some combiners, it's possible than an event contains both
// and we enter both of the following if clauses.
final CharSequence textToCommit = event.getTextToCommit();
mLog.debug("handleConsumedEvent textToCommit : " + textToCommit + ", isComposing : " + mWordComposer.isComposingWord());
if (!TextUtils.isEmpty(textToCommit)) {
mConnection.commitText(textToCommit, 1);
inputTransaction.setDidAffectContents();
}
if (mWordComposer.isComposingWord()) {
setComposingTextInternal(mWordComposer.getTypedWord(), 1);
inputTransaction.setDidAffectContents();
// 通知更新建议词(候选词)
inputTransaction.setRequiresUpdateSuggestions();
}
}</span>
handleFunctionalEvent:处理的是一些功能性的字符(如CODE_DELETE),其实就是哪些值为负的那些CODE。isFunctionalKeyEvent是根据Event中的mCodePoint是否为NOT_A_CODE_POINT来判断的,而LatinIME的createSoftwareKeypressEvent方法中是根据待处理的CODE是否为负来设置Event的mCodePoint是否为NOT_A_CODE_POINT的。
/**
* Handle a functional key event.
*
* A functional event is a special key, like delete, shift, emoji, or the settings key.
* Non-special keys are those that generate a single code point.
* This includes all letters, digits, punctuation, separators, emoji. It excludes keys that
* manage keyboard-related stuff like shift, language switch, settings, layout switch, or
* any key that results in multiple code points like the ".com" key.
*
* @param event The event to handle.
* @param inputTransaction The transaction in progress.
*/
private void handleFunctionalEvent(final Event event, final InputTransaction inputTransaction,
// TODO: remove these arguments
final int currentKeyboardScriptId, final LatinIME.UIHandler handler) {
switch (event.mKeyCode) {
case Constants.CODE_DELETE:
handleBackspaceEvent(event, inputTransaction, currentKeyboardScriptId);
// Backspace is a functional key, but it affects the contents of the editor.
inputTransaction.setDidAffectContents();
break;
case Constants.CODE_SHIFT:
performRecapitalization(inputTransaction.mSettingsValues);
inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
if (mSuggestedWords.isPrediction()) {
inputTransaction.setRequiresUpdateSuggestions();
}
break;
case Constants.CODE_CAPSLOCK:
// Note: Changing keyboard to shift lock state is handled in
// {@link KeyboardSwitcher#onCodeInput(int)}.
break;
case Constants.CODE_SYMBOL_SHIFT:
// Note: Calling back to the keyboard on the symbol Shift key is handled in
// {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
break;
case Constants.CODE_SWITCH_ALPHA_SYMBOL:
// Note: Calling back to the keyboard on symbol key is handled in
// {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
break;
case Constants.CODE_SETTINGS:
onSettingsKeyPressed();
break;
case Constants.CODE_SHORTCUT:
// We need to switch to the shortcut IME. This is handled by LatinIME since the
// input logic has no business with IME switching.
break;
case Constants.CODE_ACTION_NEXT:
performEditorAction(EditorInfo.IME_ACTION_NEXT);
break;
case Constants.CODE_ACTION_PREVIOUS:
performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
break;
case Constants.CODE_LANGUAGE_SWITCH:
handleLanguageSwitchKey();
break;
case Constants.CODE_SHIFT_ENTER:
// TODO: remove this object
final Event tmpEvent = Event.createSoftwareKeypressEvent(Constants.CODE_ENTER,
event.mKeyCode, event.mX, event.mY, event.isKeyRepeat());
handleNonSpecialCharacterEvent(tmpEvent, inputTransaction, handler);
// Shift + Enter is treated as a functional key but it results in adding a new
// line, so that does affect the contents of the editor.
inputTransaction.setDidAffectContents();
break;
default:
throw new RuntimeException("Unknown key code : " + event.mKeyCode);
}
}
handleNonFunctionalEvent:处理一些普通的字符。
/**
* Handle an event that is not a functional event.
*
* These events are generally events that cause input, but in some cases they may do other
* things like trigger an editor action.
*
* @param event The event to handle.
* @param inputTransaction The transaction in progress.
*/
private void handleNonFunctionalEvent(final Event event,
final InputTransaction inputTransaction,
// TODO: remove this argument
final LatinIME.UIHandler handler) {
inputTransaction.setDidAffectContents();
switch (event.mCodePoint) {
case Constants.CODE_ENTER:
final EditorInfo editorInfo = getCurrentInputEditorInfo();
final int imeOptionsActionId =
InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo);
if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) {
// Either we have an actionLabel and we should performEditorAction with
// actionId regardless of its value.
performEditorAction(editorInfo.actionId);
} else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) {
// We didn't have an actionLabel, but we had another action to execute.
// EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast,
// EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it
// means there should be an action and the app didn't bother to set a specific
// code for it - presumably it only handles one. It does not have to be treated
// in any specific way: anything that is not IME_ACTION_NONE should be sent to
// performEditorAction.
performEditorAction(imeOptionsActionId);
} else {
// No action label, and the action from imeOptions is NONE: this is a regular
// enter key that should input a carriage return.
handleNonSpecialCharacterEvent(event, inputTransaction, handler);
}
break;
default:
handleNonSpecialCharacterEvent(event, inputTransaction, handler);
break;
}
}
1. 首先,看下
handleFunctionalEvent方法:它是根据Event的mKeyCode的类型分别进行处理:
1) Constatns.CODE_DELETE:删除键,通过handleBackspaceEvent进行处理
/**
* Handle a press on the backspace key.
* @param event The event to handle.
* @param inputTransaction The transaction in progress.
*/
private void handleBackspaceEvent(final Event event, final InputTransaction inputTransaction,
// TODO: remove this argument, put it into settingsValues
final int currentKeyboardScriptId) {
mLog.debug("handleBackspaceEvent");
mSpaceState = SpaceState.NONE;
mDeleteCount++;
// In many cases after backspace, we need to update the shift state. Normally we need
// to do this right away to avoid the shift state being out of date in case the user types
// backspace then some other character very fast. However, in the case of backspace key
// repeat, this can lead to flashiness when the cursor flies over positions where the
// shift state should be updated, so if this is a key repeat, we update after a small delay.
// Then again, even in the case of a key repeat, if the cursor is at start of text, it
// can't go any further back, so we can update right away even if it's a key repeat.
final int shiftUpdateKind =
event.isKeyRepeat() && mConnection.getExpectedSelectionStart() > 0
? InputTransaction.SHIFT_UPDATE_LATER : InputTransaction.SHIFT_UPDATE_NOW;
inputTransaction.requireShiftUpdate(shiftUpdateKind);
mLog.debug("SelectionStart : " + mConnection.getExpectedSelectionStart());
mLog.debug("ComposingWord : " + mWordComposer.isCursorFrontOrMiddleOfComposingWord());
if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
// If we are in the middle of a recorrection, we need to commit the recorrection
// first so that we can remove the character at the current cursor position.
resetEntireInputState(mConnection.getExpectedSelectionStart(),
mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */);
// When we exit this if-clause, mWordComposer.isComposingWord() will return false.
}
if (mWordComposer.isComposingWord()) {
if (mWordComposer.isBatchMode()) {
final String rejectedSuggestion = mWordComposer.getTypedWord();
mWordComposer.reset();
mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion);
if (!TextUtils.isEmpty(rejectedSuggestion)) {
mDictionaryFacilitator.removeWordFromPersonalizedDicts(rejectedSuggestion);
}
} else {
mWordComposer.applyProcessedEvent(event);
}
if (mWordComposer.isComposingWord()) {
setComposingTextInternal(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
} else {
mConnection.commitText("", 1);
}
inputTransaction.setRequiresUpdateSuggestions();
} else {
mLog.debug("not composing");
if (mLastComposedWord.canRevertCommit()) {
mLog.debug("canRevertCommit");
revertCommit(inputTransaction, inputTransaction.mSettingsValues);
return;
}
if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
// Cancel multi-character input: remove the text we just entered.
// This is triggered on backspace after a key that inputs multiple characters,
// like the smiley key or the .com key.
mConnection.deleteSurroundingText(mEnteredText.length(), 0);
mEnteredText = null;
// If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
// In addition we know that spaceState is false, and that we should not be
// reverting any autocorrect at this point. So we can safely return.
return;
}
if (SpaceState.DOUBLE == inputTransaction.mSpaceState) {
cancelDoubleSpacePeriodCountdown();
if (mConnection.revertDoubleSpacePeriod()) {
// No need to reset mSpaceState, it has already be done (that's why we
// receive it as a parameter)
inputTransaction.setRequiresUpdateSuggestions();
mWordComposer.setCapitalizedModeAtStartComposingTime(
WordComposer.CAPS_MODE_OFF);
return;
}
} else if (SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) {
if (mConnection.revertSwapPunctuation()) {
// Likewise
return;
}
}
// No cancelling of commit/double space/swap: we have a regular backspace.
// We should backspace one char and restart suggestion if at the end of a word.
if (mConnection.hasSelection()) {
// If there is a selection, remove it.
final int numCharsDeleted = mConnection.getExpectedSelectionEnd()
- mConnection.getExpectedSelectionStart();
mConnection.setSelection(mConnection.getExpectedSelectionEnd(),
mConnection.getExpectedSelectionEnd());
mConnection.deleteSurroundingText(numCharsDeleted, 0);
} else {
// There is no selection, just delete one character.
if (Constants.NOT_A_CURSOR_POSITION == mConnection.getExpectedSelectionEnd()) {
// This should never happen.
Log.e(TAG, "Backspace when we don't know the selection position");
}
if (inputTransaction.mSettingsValues.isBeforeJellyBean() ||
inputTransaction.mSettingsValues.mInputAttributes.isTypeNull()) {
// There are two possible reasons to send a key event: either the field has
// type TYPE_NULL, in which case the keyboard should send events, or we are
// running in backward compatibility mode. Before Jelly bean, the keyboard
// would simulate a hardware keyboard event on pressing enter or delete. This
// is bad for many reasons (there are race conditions with commits) but some
// applications are relying on this behavior so we continue to support it for
// older apps, so we retain this behavior if the app has target SDK < JellyBean.
sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) {
sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
}
} else {
final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
if (codePointBeforeCursor == Constants.NOT_A_CODE) {
// HACK for backward compatibility with broken apps that haven't realized
// yet that hardware keyboards are not the only way of inputting text.
// Nothing to delete before the cursor. We should not do anything, but many
// broken apps expect something to happen in this case so that they can
// catch it and have their broken interface react. If you need the keyboard
// to do this, you're doing it wrong -- please fix your app.
mConnection.deleteSurroundingText(1, 0);
return;
}
final int lengthToDelete =
Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1;
mConnection.deleteSurroundingText(lengthToDelete, 0);
if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) {
final int codePointBeforeCursorToDeleteAgain =
mConnection.getCodePointBeforeCursor();
if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) {
final int lengthToDeleteAgain = Character.isSupplementaryCodePoint(
codePointBeforeCursorToDeleteAgain) ? 2 : 1;
mConnection.deleteSurroundingText(lengthToDeleteAgain, 0);
}
}
}
}
if (inputTransaction.mSettingsValues
.isSuggestionsEnabledPerUserSettings()
&& inputTransaction.mSettingsValues.mSpacingAndPunctuations
.mCurrentLanguageHasSpaces
&& !mConnection.isCursorFollowedByWordCharacter(
inputTransaction.mSettingsValues.mSpacingAndPunctuations)) {
restartSuggestionsOnWordTouchedByCursor(inputTransaction.mSettingsValues,
true /* shouldIncludeResumedWordInSuggestions */, currentKeyboardScriptId);
}
}
}