LatinIME相关调研

LatinIME相关调研

核心类

  • LatinIME: 同我们的ImeService,继承InputMethodService,处理输入法的系统回调。
  • InputLogic: 输入事件的逻辑层,结合LatinIME和InputConnection,处理输入的逻辑。
  • KeyboardView: 主键盘的view,负责面板和按键的绘制
  • SuggestionStripView: 候选条的view,类似我们的candView
  • KeyboardBuilder: 解析xml结构的键盘布局
  • KeyboardSwitcher: 控制面板的显示和切换

面板布局解析和绘制

面板的布局

LatinIME的面板布局都是内置的xml结构,在res的xml目录下,以keyboard_layout_set开头的表示一种语言下的布局,kbd开头的表示一个面板的布局,rows开头的表示一行的布局
以qwerty布局为例结构如下:


图片

面板解析

面板解析的核心逻辑都在KeyobardBuilder中,其中的load方法为加载具体的一个xml,该方法的参数xmlId指明了面板加载的布局文件id:
KeyobardBuilder

public KeyboardBuilder 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中做了具体的解析,包括parseKeyboardAttributesparseKeyboardContent等,解析的参数在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 mSortedKeys = new TreeSet<>(ROW_COLUMN_COMPARATOR);
@Nonnull
public final ArrayList mShiftKeys = new ArrayList<>();
@Nonnull
public final ArrayList 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中,里面调用onStartInputViewInternal方法,该方法中会调用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获取,该方法又会调用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 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回调做了具体的绘制: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标签添加,
切换输入法的时候会回调到LatinIME的onCurrentInputMethodSubtypeChanged的方法,其中会分别调用RichInputMethodManager的onSubtypeChange、updateCurrentSubtype、updateShortcutIme做视图的切换,和InputLogic的onSubtypeChanged做逻辑的切换,主要是重新启动输入,然后会调用loadKeyboard方法,在该方法中,首选通过mHander.postReopenDictionaries()来加载词典,为了重新确定联想词,接着通过loadSetting方法加载更新设置,最后通过KeyboardSwitcherloadKeyobard方法来重新加载键盘。流程如下:

图片

输入流程

KeyboardActionListener提供了整个面板的事件响应的监听,在有事件输入时会回调到LatinIME的onCodeInput,该方法中,首先通过createSoftwareKeypressEvent创建输入事件Event,然后调用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;
}

可以看到该方法先是调用WordComposer中的processEvent方法处理第一步创建的EventWordComposer是对当前出词做调整的一个封装类。经过该方法后,事件被封装和解析,然后创建InputTransaction来开启一个输入事件的事务,在while的循环中处理具体的处理流程,根据currentEvent的状态调用不同的处理方法,handleConsumedEvent处理自定义的事件,handleFunctionalEvent处理功能键的事件,类似我们的handleFkeyhandleNonFunctionalEvent来处理普通的字符

你可能感兴趣的:(LatinIME相关调研)