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<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中做了具体的解析,包括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<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中,里面调用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<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回调做了具体的绘制: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来处理普通的字符

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容