Android AOSP输入法(LatinIME)输入流程二
一、字符输入起始:
LatinIME字符输入的初始方法是LatinIME类中的onCodeInput方法:
// 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()); }
首先通过createSoftwareKeypressEvent方法创建输入事件(Event),接着就开始调用InputLogic中的onCodeInput进行具体的字符输入操作(核心处理流程) 二、核心处理流程:
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; }
这其中使用到的RichInputConnection类,是通过组合的方式引入了InputConnection类,InputConnection类是Android中连接输入法与调用输入法控件之间的关键类,它提供了诸如提交字符、获取光标左右文本、删除光标附近字符等重要的方法。
下面就来分析下onCodeInput方法,所有的Event最终都是通过CombinerChain这个类来管理的,而onCodeInput首先是调用WordComposer中的processEvent方法来处理第一步创建的Event。而WordComposer在源码中的解释是存储当前构成词以及类似于临近key code之类信息的地方。
/** * 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; }
可以看到WordComposer中的processEvent是调用了CombinerChain中的processEvent来处理onCodeInput传入的Event(输入事件): /** * 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 previousEvents, final Event newEvent) { mLog.debug("CombinerChain mCombinedText : " + mCombinedText); final ArrayList 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; }
可以看到,CombinerChain会将处理好的Event返回给WordComposer,而WordComposer会进一步处理返回的Event,最后将处理好的Event在返回给InputLogic(这两个类的具体逻辑后面再进行分析)。
InputLogic接到处理好的Event后,会创建一个InputTransaction(它封装了输入事件的一个单一事务)。接下来是判断是否是删除操作以及设定时间戳。
在上面的步骤都完成后,onCodeInput就调用RichInputConnection的beginBatchEdit方法开始进行字符的输入。
while语句中是具体的处理流程,首先是判断currentEvent的状态,并根据不同的状态调用不同的处理方法,handleConsumedEvent(暂置):好像是再次处理已经处理过的事件(?),打的log中没有出现过,所以不确定其处理的是什么类型的事件。
/** * 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(); } }
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); } } }
更多相关文章
- Android简单获取经纬度的方法
- Android拨打电话的两种实现方法
- android 基本的画图方法
- 事件总线EventBus Android开源库的使用
- Handler 内部类导致的内存泄露修改方法
- android listview的item里面的imageview的点击事件
- Android 在程序中重启APP的方法
- android studio AppCompatActivity中onCreate的两种方法
- Android截图方法