LatinIME相關(guān)調(diào)研

LatinIME相關(guān)調(diào)研

核心類

  • LatinIME: 同我們的ImeService,繼承InputMethodService,處理輸入法的系統(tǒng)回調(diào)。
  • InputLogic: 輸入事件的邏輯層,結(jié)合LatinIME和InputConnection,處理輸入的邏輯。
  • KeyboardView: 主鍵盤的view,負責面板和按鍵的繪制
  • SuggestionStripView: 候選條的view,類似我們的candView
  • KeyboardBuilder: 解析xml結(jié)構(gòu)的鍵盤布局
  • KeyboardSwitcher: 控制面板的顯示和切換

面板布局解析和繪制

面板的布局

LatinIME的面板布局都是內(nèi)置的xml結(jié)構(gòu),在res的xml目錄下,以keyboard_layout_set開頭的表示一種語言下的布局,kbd開頭的表示一個面板的布局,rows開頭的表示一行的布局
以qwerty布局為例結(jié)構(gòu)如下:


圖片

面板解析

面板解析的核心邏輯都在KeyobardBuilder中,其中的load方法為加載具體的一個xml,該方法的參數(shù)xmlId指明了面板加載的布局文件id:
KeyobardBuilder

public KeyboardBuilder<KP> load(final int xmlId, final KeyboardId id) {
    mParams.mId = id;
    final XmlResourceParser parser = mResources.getXml(xmlId);
    try {
        parseKeyboard(parser);
    } catch (XmlPullParserException e) {
        Log.w(BUILDER_TAG, "keyboard XML parse error", e);
        throw new IllegalArgumentException(e.getMessage(), e);
    } catch (IOException e) {
        Log.w(BUILDER_TAG, "keyboard XML parse error", e);
        throw new RuntimeException(e.getMessage(), e);
    } finally {
        parser.close();
    }
    return this;
}

其中主要就是parseKeyboard中做了具體的解析,包括parseKeyboardAttributes、parseKeyboardContent等,解析的參數(shù)在KeyboardParams類中定義,包括各種padding、gap、key等
KeyboardParams

public class KeyboardParams {
public KeyboardId mId;
public int mThemeId;

/** Total height and width of the keyboard, including the paddings and keys */
public int mOccupiedHeight;
public int mOccupiedWidth;

/** Base height and width of the keyboard used to calculate rows' or keys' heights and
 *  widths
 */
public int mBaseHeight;
public int mBaseWidth;

public int mTopPadding;
public int mBottomPadding;
public int mLeftPadding;
public int mRightPadding;

@Nullable
public KeyVisualAttributes mKeyVisualAttributes;

public int mDefaultRowHeight;
public int mDefaultKeyWidth;
public int mHorizontalGap;
public int mVerticalGap;

public int mMoreKeysTemplate;
public int mMaxMoreKeysKeyboardColumn;

public int GRID_WIDTH;
public int GRID_HEIGHT;

// Keys are sorted from top-left to bottom-right order.
@Nonnull
public final SortedSet<Key> mSortedKeys = new TreeSet<>(ROW_COLUMN_COMPARATOR);
@Nonnull
public final ArrayList<Key> mShiftKeys = new ArrayList<>();
@Nonnull
public final ArrayList<Key> mAltCodeKeysWhileTyping = new ArrayList<>();
@Nonnull
public final KeyboardIconsSet mIconsSet = new KeyboardIconsSet();
@Nonnull
public final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
@Nonnull
public final KeyStylesSet mKeyStyles = new KeyStylesSet(mTextsSet);

@Nonnull
private final UniqueKeysCache mUniqueKeysCache;
public boolean mAllowRedundantMoreKeys;

public int mMostCommonKeyHeight = 0;
public int mMostCommonKeyWidth = 0;

public boolean mProximityCharsCorrectionEnabled;

........

面板的加載

面板加載的入口同我們輸入法在onStartInputView中,里面調(diào)用onStartInputViewInternal方法,該方法中會調(diào)用KeyboardSwitcher的loadKeyboard方法來加載面板,KeyboardSwitcher類主要負責面板的更新和切換

void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) {
    super.onStartInputView(editorInfo, restarting);
    ................
    final KeyboardSwitcher switcher = mKeyboardSwitcher;
    ................
    if (isDifferentTextField) {
       ...............
        switcher.loadKeyboard(editorInfo, currentSettingsValues, getCurrentAutoCapsState(),
                getCurrentRecapitalizeState());
        if (needToCallLoadKeyboardLater) {
            // If we need to call loadKeyboard again later, we need to save its state now. The
            // later call will be done in #retryResetCaches.
            switcher.saveKeyboardState();
        }
    }

KeyboardSwitcher的setKeyboard方法可以完成面板切換,通過oldKeyboardnewKeyboard來記錄,其中newKeyboard通過KeyboardLayoutSet的getKeyboard獲取,該方法又會調(diào)用KeyboardBuilder的load方法來解析xml文件,如上面的面板解析所述。
KeyboardSwitcher

private void setKeyboard(
    ..........
    final Keyboard oldKeyboard = keyboardView.getKeyboard();
    final Keyboard newKeyboard = mKeyboardLayoutSet.getKeyboard(keyboardId);
    keyboardView.setKeyboard(newKeyboard);
    .......
}

KeyboardLayoutSet

private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) {
.......................
    final KeyboardBuilder<KeyboardParams> builder =
            new KeyboardBuilder<>(mContext, new KeyboardParams(sUniqueKeysCache));
    sUniqueKeysCache.setEnabled(id.isAlphabetKeyboard());
    builder.setAllowRedundantMoreKes(elementParams.mAllowRedundantMoreKeys);
    final int keyboardXmlId = elementParams.mKeyboardXmlId;
    builder.load(keyboardXmlId, id);
.....................

面板的繪制

面板的所有元素(不包括cand)都在KeyboardView中定義,onDraw回調(diào)做了具體的繪制:onDrawKeyboardonDrawKey
KeyboardView

protected void onDraw(final Canvas canvas) {
    super.onDraw(canvas);
    if (canvas.isHardwareAccelerated()) {
        onDrawKeyboard(canvas);
        return;
    }
    ........
private void onDrawKeyboard(@Nonnull final Canvas canvas) {
    final Keyboard keyboard = getKeyboard();
    final Paint paint = mPaint;
    final Drawable background = getBackground();
    // Calculate clip region and set.
    final boolean drawAllKeys = mInvalidateAllKeys || mInvalidatedKeys.isEmpty();
    final boolean isHardwareAccelerated = canvas.isHardwareAccelerated();
    // TODO: Confirm if it's really required to draw all keys when hardware acceleration is on.
    if (drawAllKeys || isHardwareAccelerated) {
        if (!isHardwareAccelerated && background != null) {
            // Need to draw keyboard background on {@link #mOffscreenBuffer}.
            canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
            background.draw(canvas);
        }
        // Draw all keys.
        for (final Key key : keyboard.getSortedKeys()) {
            onDrawKey(key, canvas, paint);
        }
    } else {
        for (final Key key : mInvalidatedKeys) {
            if (!keyboard.hasKey(key)) {
                continue;
            }
            if (background != null) {
                // Need to redraw key's background on {@link #mOffscreenBuffer}.
                final int x = key.getX() + getPaddingLeft();
                final int y = key.getY() + getPaddingTop();
                mClipRect.set(x, y, x + key.getWidth(), y + key.getHeight());
                canvas.save();
                canvas.clipRect(mClipRect);
                canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
                background.draw(canvas);
                canvas.restore();
            }
            onDrawKey(key, canvas, paint);
        }
    }

    mInvalidatedKeys.clear();
    mInvalidateAllKeys = false;
}

切換語言

LatinIME的輸入法定義在res下的method.xml,通過subtype標簽添加,
切換輸入法的時候會回調(diào)到LatinIME的onCurrentInputMethodSubtypeChanged的方法,其中會分別調(diào)用RichInputMethodManager的onSubtypeChange、updateCurrentSubtype、updateShortcutIme做視圖的切換,和InputLogic的onSubtypeChanged做邏輯的切換,主要是重新啟動輸入,然后會調(diào)用loadKeyboard方法,在該方法中,首選通過mHander.postReopenDictionaries()來加載詞典,為了重新確定聯(lián)想詞,接著通過loadSetting方法加載更新設置,最后通過KeyboardSwitcherloadKeyobard方法來重新加載鍵盤。流程如下:

圖片

輸入流程

KeyboardActionListener提供了整個面板的事件響應的監(jiān)聽,在有事件輸入時會回調(diào)到LatinIME的onCodeInput,該方法中,首先通過createSoftwareKeypressEvent創(chuàng)建輸入事件Event,然后調(diào)用InputLogic中的onCodeInput進行具體的字符輸入操作,該方法處理核心的輸入流程:

public InputTransaction onCodeInput(final SettingsValues settingsValues,
        @Nonnull final Event event, final int keyboardShiftMode,
        final int currentKeyboardScriptId, final LatinIME.UIHandler handler) {
    mWordBeingCorrectedByCursor = null;
    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;
    }
    // Try to record the word being corrected when the user enters a word character or
    // the backspace key.
    if (!mConnection.hasSlowInputConnection() && !mWordComposer.isComposingWord()
            && (settingsValues.isWordCodePoint(processedEvent.mCodePoint) ||
                    processedEvent.mKeyCode == Constants.CODE_DELETE)) {
        mWordBeingCorrectedByCursor = getWordAtCursor(
               settingsValues, currentKeyboardScriptId);
    }
    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;
}

可以看到該方法先是調(diào)用WordComposer中的processEvent方法處理第一步創(chuàng)建的EventWordComposer是對當前出詞做調(diào)整的一個封裝類。經(jīng)過該方法后,事件被封裝和解析,然后創(chuàng)建InputTransaction來開啟一個輸入事件的事務,在while的循環(huán)中處理具體的處理流程,根據(jù)currentEvent的狀態(tài)調(diào)用不同的處理方法,handleConsumedEvent處理自定義的事件,handleFunctionalEvent處理功能鍵的事件,類似我們的handleFkey,handleNonFunctionalEvent來處理普通的字符
。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容