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方法可以完成面板切換,通過oldKeyboard和newKeyboard來記錄,其中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)做了具體的繪制:onDrawKeyboard和onDrawKey等
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方法加載更新設置,最后通過KeyboardSwitcher的loadKeyobard方法來重新加載鍵盤。流程如下:
輸入流程
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)建的Event,WordComposer是對當前出詞做調(diào)整的一個封裝類。經(jīng)過該方法后,事件被封裝和解析,然后創(chuàng)建InputTransaction來開啟一個輸入事件的事務,在while的循環(huán)中處理具體的處理流程,根據(jù)currentEvent的狀態(tài)調(diào)用不同的處理方法,handleConsumedEvent處理自定義的事件,handleFunctionalEvent處理功能鍵的事件,類似我們的handleFkey,handleNonFunctionalEvent來處理普通的字符
。