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
中做了具体的解析,包括parseKeyboardAttributes
、parseKeyboardContent
等,解析的参数在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
方法可以完成面板切换,通过oldKeyboard
和newKeyboard
来记录,其中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回调做了具体的绘制: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标签添加,
切换输入法的时候会回调到LatinIME的onCurrentInputMethodSubtypeChanged
的方法,其中会分别调用RichInputMethodManager的onSubtypeChange、updateCurrentSubtype、updateShortcutIme
做视图的切换,和InputLogic的onSubtypeChanged
做逻辑的切换,主要是重新启动输入,然后会调用loadKeyboard
方法,在该方法中,首选通过mHander.postReopenDictionaries()
来加载词典,为了重新确定联想词,接着通过loadSetting
方法加载更新设置,最后通过KeyboardSwitcher
的loadKeyobard
方法来重新加载键盘。流程如下:
输入流程
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
方法处理第一步创建的Event
,WordComposer
是对当前出词做调整的一个封装类。经过该方法后,事件被封装和解析,然后创建InputTransaction
来开启一个输入事件的事务,在while
的循环中处理具体的处理流程,根据currentEvent
的状态调用不同的处理方法,handleConsumedEvent
处理自定义的事件,handleFunctionalEvent
处理功能键的事件,类似我们的handleFkey
,handleNonFunctionalEvent
来处理普通的字符
。