最近項(xiàng)目需要,需要做一個(gè)輸入驗(yàn)證碼的組件,就如同下圖展示的樣子。

每個(gè)驗(yàn)證碼數(shù)字是個(gè)獨(dú)立的view,不能通過(guò)一個(gè)editText來(lái)完成,基于這種實(shí)現(xiàn)就需要獲取到每次鍵盤(pán)點(diǎn)擊事件的內(nèi)容,還有監(jiān)聽(tīng)刪除事件,這樣才能實(shí)現(xiàn)每次輸入完成自動(dòng)將焦點(diǎn)放到后一個(gè)view。
https://github.com/yan010/VerificationCodeInputView
這里是一個(gè)我寫(xiě)的驗(yàn)證碼開(kāi)源庫(kù),大家可以直接使用,下面開(kāi)始給搭建講解具體的鍵盤(pán)輸入法輸入流程。
看到這樣的ui跟要求,大家的第一反應(yīng)可能就是監(jiān)聽(tīng)view的setOnKeyListener,但是經(jīng)過(guò)實(shí)驗(yàn)發(fā)現(xiàn)在原生鍵盤(pán)上不能監(jiān)聽(tīng)到刪除事件,這樣就會(huì)導(dǎo)致刪除后不能控制當(dāng)前的焦點(diǎn)。
這時(shí)需要找到一種方法通用的方法來(lái)獲取鍵盤(pán)的點(diǎn)擊事件。在尋找這種方法之前需要完整的來(lái)看一下整個(gè)鍵盤(pán)喚起輸入到關(guān)閉的整個(gè)流程。
喚起鍵盤(pán)通過(guò)InputMethodManager類的showSoftInput方法,那么就從這方法往下看,
public boolean showSoftInput(View view, int flags, ResultReceiver resultReceiver) {
checkFocus();
synchronized (mH) {
if (mServedView != view && (mServedView == null
|| !mServedView.checkInputConnectionProxy(view))) {
return false;
}
try {
return mService.showSoftInput(mClient, flags, resultReceiver);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
}
mService = IInputMethodManager.Stub.asInterface(ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE));
上面代碼mService是InputMethodManagerService的代理類,實(shí)現(xiàn)了binder機(jī)制,所以mService是binder的客戶端類,執(zhí)行showSoftInput,這時(shí)就要進(jìn)入InputMethodManagerService類來(lái)看在服務(wù)端是怎么執(zhí)行的
InputMethodManagerService.class
@Override
public boolean showSoftInput(IInputMethodClient client, int flags,
ResultReceiver resultReceiver) {
if (!calledFromValidUser()) {
return false;
}
int uid = Binder.getCallingUid();
long ident = Binder.clearCallingIdentity();
try {
synchronized (mMethodMap) {
if (mCurClient == null || client == null
|| mCurClient.client.asBinder() != client.asBinder()) {
try {
// We need to check if this is the current client with
// focus in the window manager, to allow this call to
// be made before input is started in it.
if (!mIWindowManager.inputMethodClientHasFocus(client)) {
Slog.w(TAG, "Ignoring showSoftInput of uid " + uid + ": " + client);
return false;
}
} catch (RemoteException e) {
return false;
}
}
if (DEBUG) Slog.v(TAG, "Client requesting input be shown");
return showCurrentInputLocked(flags, resultReceiver);
}
} finally {
Binder.restoreCallingIdentity(ident);
}
}
boolean showCurrentInputLocked(int flags, ResultReceiver resultReceiver) {
...
executeOrSendMessage(mCurMethod, mCaller.obtainMessageIOO(
MSG_SHOW_SOFT_INPUT, getImeShowFlags(), mCurMethod,
resultReceiver));
...
}
看到到這里會(huì)給handler發(fā)一條消息通知展示鍵盤(pán)。
case MSG_SHOW_SOFT_INPUT:
args = (SomeArgs)msg.obj;
try {
if (DEBUG) Slog.v(TAG, "Calling " + args.arg1 + ".showSoftInput("
+ msg.arg1 + ", " + args.arg2 + ")");
((IInputMethod)args.arg1).showSoftInput(msg.arg1, (ResultReceiver)args.arg2);
} catch (RemoteException e) {
}
args.recycle();
return true;
可以看到在handler中會(huì)執(zhí)行一個(gè)IInputMethod的showSoftInput方法,IInputMethod是個(gè)接口他的實(shí)例類是前邊的mCurMethod,
mCurMethod = IInputMethod.Stub.asInterface(service);
mCurMethod是IInputMethod的代理類,這時(shí)我們就要看InputMethodService類中的showSoftInput實(shí)現(xiàn)了。
/**
* Handle a request by the system to show the soft input area.
*/
public void showSoftInput(int flags, ResultReceiver resultReceiver) {
if (DEBUG) Log.v(TAG, "showSoftInput()");
boolean wasVis = isInputViewShown();
if (dispatchOnShowInputRequested(flags, false)) {
try {
showWindow(true);
} catch (BadTokenException e) {
}
}
clearInsetOfPreviousIme();
// If user uses hard keyboard, IME button should always be shown.
boolean showing = isInputViewShown();
mImm.setImeWindowStatus(mToken, mStartInputToken,
IME_ACTIVE | (showing ? IME_VISIBLE : 0), mBackDisposition);
if (resultReceiver != null) {
resultReceiver.send(wasVis != isInputViewShown()
? InputMethodManager.RESULT_SHOWN
: (wasVis ? InputMethodManager.RESULT_UNCHANGED_SHOWN
: InputMethodManager.RESULT_UNCHANGED_HIDDEN), null);
}
}
public void showWindow(boolean showInput) {
...
showWindowInner(showInput);
...
}
void showWindowInner(boolean showInput) {
...
initialize();
updateFullscreenMode();
updateInputViewShown();
...
}
public void updateInputViewShown() {
boolean isShown = mShowInputRequested && onEvaluateInputViewShown();
if (mIsInputViewShown != isShown && mWindowVisible) {
mIsInputViewShown = isShown;
mInputFrame.setVisibility(isShown ? View.VISIBLE : View.GONE);
if (mInputView == null) {
initialize();
View v = onCreateInputView();
if (v != null) {
setInputView(v);
}
}
}
}
public View onCreateInputView() {
return null;
}
最終通過(guò)onCreateInputView方法返回一個(gè)view類,這個(gè)類在當(dāng)前類中是個(gè)空實(shí)現(xiàn),需要子類繼承并重寫(xiě)這個(gè)方法,這樣就展示出了一個(gè)鍵盤(pán)。
到這里鍵盤(pán)的創(chuàng)建過(guò)程已經(jīng)完成。
這時(shí)如果有點(diǎn)擊鍵盤(pán)的操作時(shí),事件是怎么完成的呢?
先看InputMethodService中獲取到鍵盤(pán)點(diǎn)擊事件的方法sendKeyChar
public void sendKeyChar(char charCode) {
switch (charCode) {
case '\n': // Apps may be listening to an enter key to perform an action
if (!sendDefaultEditorAction(true)) {
sendDownUpKeyEvents(KeyEvent.KEYCODE_ENTER);
}
break;
default:
// Make sure that digits go through any text watcher on the client side.
if (charCode >= '0' && charCode <= '9') {
sendDownUpKeyEvents(charCode - '0' + KeyEvent.KEYCODE_0);
} else {
InputConnection ic = getCurrentInputConnection();
if (ic != null) {
ic.commitText(String.valueOf(charCode), 1);
}
}
break;
}
}
public void sendDownUpKeyEvents(int keyEventCode) {
InputConnection ic = getCurrentInputConnection();
if (ic == null) return;
long eventTime = SystemClock.uptimeMillis();
ic.sendKeyEvent(new KeyEvent(eventTime, eventTime,
KeyEvent.ACTION_DOWN, keyEventCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE));
ic.sendKeyEvent(new KeyEvent(eventTime, SystemClock.uptimeMillis(),
KeyEvent.ACTION_UP, keyEventCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE));
}
方法中會(huì)根據(jù)傳過(guò)來(lái)的字符區(qū)分類型進(jìn)行不同處理,可以看到回退的處理會(huì)被單獨(dú)處理,其他字符如果是數(shù)字會(huì)在進(jìn)行一步處理,通過(guò)sendKeyEvent方法傳遞給InputConnection連接對(duì)象,其他的則執(zhí)行commitText。那么這時(shí)就需要關(guān)注這個(gè)InputConnection對(duì)象的內(nèi)部實(shí)現(xiàn),因?yàn)檫@是鍵盤(pán)返回參數(shù)的第一優(yōu)先級(jí)處理的對(duì)象,查看TextView會(huì)發(fā)現(xiàn)內(nèi)部會(huì)有一個(gè)onCreateInputConnection方法,會(huì)創(chuàng)建系統(tǒng)寫(xiě)好的EditableInputConnection對(duì)象,onCreateInputConnection這個(gè)方法是個(gè)可被重寫(xiě)的方法這也是完成上邊所說(shuō)需求的關(guān)鍵方法,我們自己創(chuàng)建一個(gè)InputConnection子類,重寫(xiě)其中方法就可以攔截鍵盤(pán)事件。
回到輸入流程我們查看EditableInputConnection的內(nèi)部實(shí)現(xiàn)sendKeyEvent的過(guò)程
public boolean sendKeyEvent(KeyEvent event) {
mIMM.dispatchKeyEventFromInputMethod(mTargetView, event);
return false;
}
方法中會(huì)執(zhí)行InputMethodManager的dispatchKeyEventFromInputMethod方法
public void dispatchKeyEventFromInputMethod(@Nullable View targetView,
@NonNull KeyEvent event) {
synchronized (mH) {
ViewRootImpl viewRootImpl = targetView != null ? targetView.getViewRootImpl() : null;
if (viewRootImpl == null) {
if (mServedView != null) {
viewRootImpl = mServedView.getViewRootImpl();
}
}
if (viewRootImpl != null) {
viewRootImpl.dispatchKeyFromIme(event);
}
}
}
看到上邊代碼會(huì)獲取到目標(biāo)view的ViewRootImpl對(duì)象,執(zhí)行ViewRootImpl的dispatchKeyFromIme方法
public void dispatchKeyFromIme(KeyEvent event) {
Message msg = mHandler.obtainMessage(MSG_DISPATCH_KEY_FROM_IME, event);
msg.setAsynchronous(true);
mHandler.sendMessage(msg);
}
case MSG_DISPATCH_KEY_FROM_IME: {
if (LOCAL_LOGV) Log.v(
TAG, "Dispatching key "
+ msg.obj + " from IME to " + mView);
KeyEvent event = (KeyEvent)msg.obj;
if ((event.getFlags()&KeyEvent.FLAG_FROM_SYSTEM) != 0) {
// The IME is trying to say this event is from the
// system! Bad bad bad!
//noinspection UnusedAssignment
event = KeyEvent.changeFlags(event, event.getFlags() &
~KeyEvent.FLAG_FROM_SYSTEM);
}
enqueueInputEvent(event, null, QueuedInputEvent.FLAG_DELIVER_POST_IME, true);
} break;
void enqueueInputEvent(InputEvent event,
InputEventReceiver receiver, int flags, boolean processImmediately) {
...
if (processImmediately) {
doProcessInputEvents();
} else {
scheduleProcessInputEvents();
}
}
void doProcessInputEvents() {
...
deliverInputEvent(q);
...
}
private void deliverInputEvent(QueuedInputEvent q) {
...
InputStage stage;
if (q.shouldSendToSynthesizer()) {
stage = mSyntheticInputStage;
} else {
stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
}
if (stage != null) {
stage.deliver(q);
} else {
finishInputEvent(q);
}
}
經(jīng)過(guò)一系列的傳遞最終stage對(duì)象進(jìn)行分發(fā),stage對(duì)象有很多子類,這里的stage的最終實(shí)現(xiàn)類是ViewPostImeInputStage
public final void deliver(QueuedInputEvent q) {
if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) {
forward(q);
} else if (shouldDropInputEvent(q)) {
finish(q, false);
} else {
apply(q, onProcess(q));
}
}
//重寫(xiě)了onProcess方法
@Override
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {
return processKeyEvent(q);
} else {
final int source = q.mEvent.getSource();
if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
return processPointerEvent(q);
} else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
return processTrackballEvent(q);
} else {
return processGenericMotionEvent(q);
}
}
}
private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent)q.mEvent;
// Deliver the key to the view hierarchy.
if (mView.dispatchKeyEvent(event)) {
return FINISH_HANDLED;
}
....
}
重寫(xiě)了onProcess方法并執(zhí)行了mView.dispatchKeyEvent(event),mView對(duì)象可以知道是DecorView對(duì)象,這時(shí)事件也就傳遞到了Decorview中
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
final int keyCode = event.getKeyCode();
final int action = event.getAction();
final boolean isDown = action == KeyEvent.ACTION_DOWN;
if (isDown && (event.getRepeatCount() == 0)) {
// First handle chording of panel key: if a panel key is held
// but not released, try to execute a shortcut in it.
if ((mWindow.mPanelChordingKey > 0) && (mWindow.mPanelChordingKey != keyCode)) {
boolean handled = dispatchKeyShortcutEvent(event);
if (handled) {
return true;
}
}
// If a panel is open, perform a shortcut on it without the
// chorded panel key
if ((mWindow.mPreparedPanel != null) && mWindow.mPreparedPanel.isOpen) {
if (mWindow.performPanelShortcut(mWindow.mPreparedPanel, keyCode, event, 0)) {
return true;
}
}
}
if (!mWindow.isDestroyed()) {
final Window.Callback cb = mWindow.getCallback();
final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event)
: super.dispatchKeyEvent(event);
if (handled) {
return true;
}
}
return isDown ? mWindow.onKeyDown(mFeatureId, event.getKeyCode(), event)
: mWindow.onKeyUp(mFeatureId, event.getKeyCode(), event);
}
在Decorview中獲取當(dāng)前window的callback,這個(gè)callback會(huì)回調(diào)到activity中,當(dāng)不存在回調(diào)時(shí)則會(huì)執(zhí)行父類的dispatchKeyEvent方法,如果window已經(jīng)被銷毀的情況,則執(zhí)行phonewindow的onKeyDown和onKeyUp,下邊分別分析下邊三種情況。
第一種回調(diào)activity的dispatchKeyEvent方法。
public boolean dispatchKeyEvent(KeyEvent event) {
onUserInteraction();
// Let action bars open menus in response to the menu key prioritized over
// the window handling it
final int keyCode = event.getKeyCode();
if (keyCode == KeyEvent.KEYCODE_MENU &&
mActionBar != null && mActionBar.onMenuKeyEvent(event)) {
return true;
}
Window win = getWindow();
if (win.superDispatchKeyEvent(event)) {
return true;
}
View decor = mDecor;
if (decor == null) decor = win.getDecorView();
return event.dispatch(this, decor != null
? decor.getKeyDispatcherState() : null, this);
}
方法中會(huì)優(yōu)先執(zhí)行window的superDispatchKeyEvent方法看是否處理掉了這個(gè)事件
PhoneWindow.java
@Override
public boolean superDispatchKeyEvent(KeyEvent event) {
return mDecor.superDispatchKeyEvent(event);
}
DecorView.java
public boolean superDispatchKeyEvent(KeyEvent event) {
// Give priority to closing action modes if applicable.
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
final int action = event.getAction();
// Back cancels action modes first.
if (mPrimaryActionMode != null) {
if (action == KeyEvent.ACTION_UP) {
mPrimaryActionMode.finish();
}
return true;
}
}
return super.dispatchKeyEvent(event);
}
ViewGroup.java
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onKeyEvent(event, 1);
}
if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
== (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
if (super.dispatchKeyEvent(event)) {
return true;
}
} else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
== PFLAG_HAS_BOUNDS) {
if (mFocused.dispatchKeyEvent(event)) {
return true;
}
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
}
return false;
}
這時(shí)其實(shí)就跟第二種情況super.dispatchKeyEvent(event)一樣了,第二種情況則是從這里開(kāi)始。執(zhí)行到ViewGroup中,如果此ViewGroup是focused或者具體的大小被設(shè)置了(有邊界),則執(zhí)行父類view的dispatchKeyEvent,如果子類中有設(shè)置了foucus的對(duì)象,則執(zhí)行foucus的dispatchKeyEvent,如果都沒(méi)有則返回false。
public boolean dispatchKeyEvent(KeyEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onKeyEvent(event, 0);
}
// Give any attached key listener a first crack at the event.
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
return true;
}
if (event.dispatch(this, mAttachInfo != null
? mAttachInfo.mKeyDispatchState : null, this)) {
return true;
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return false;
}
方法中會(huì)優(yōu)先調(diào)用onKeyListener,如果它非空且view是ENABLED狀態(tài),監(jiān)聽(tīng)器優(yōu)先觸發(fā),沒(méi)有設(shè)置監(jiān)聽(tīng)則調(diào)用KeyEvent.dispatch方法,并將view對(duì)象本身作為參數(shù)傳遞進(jìn)去,view的各種callback方法在這里被觸發(fā)。這里我們進(jìn)入event.dispatch方法來(lái)看下實(shí)現(xiàn)
public final boolean dispatch(Callback receiver, DispatcherState state,
Object target) {
switch (mAction) {
case ACTION_DOWN: {
mFlags &= ~FLAG_START_TRACKING;
if (DEBUG) Log.v(TAG, "Key down to " + target + " in " + state
+ ": " + this);
boolean res = receiver.onKeyDown(mKeyCode, this);
if (state != null) {
if (res && mRepeatCount == 0 && (mFlags&FLAG_START_TRACKING) != 0) {
if (DEBUG) Log.v(TAG, " Start tracking!");
state.startTracking(this, target);
} else if (isLongPress() && state.isTracking(this)) {
try {
if (receiver.onKeyLongPress(mKeyCode, this)) {
if (DEBUG) Log.v(TAG, " Clear from long press!");
state.performedLongPress(this);
res = true;
}
} catch (AbstractMethodError e) {
}
}
}
return res;
}
case ACTION_UP:
if (DEBUG) Log.v(TAG, "Key up to " + target + " in " + state
+ ": " + this);
if (state != null) {
state.handleUpEvent(this);
}
return receiver.onKeyUp(mKeyCode, this);
case ACTION_MULTIPLE:
final int count = mRepeatCount;
final int code = mKeyCode;
if (receiver.onKeyMultiple(code, count, this)) {
return true;
}
if (code != KeyEvent.KEYCODE_UNKNOWN) {
mAction = ACTION_DOWN;
mRepeatCount = 0;
boolean handled = receiver.onKeyDown(code, this);
if (handled) {
mAction = ACTION_UP;
receiver.onKeyUp(code, this);
}
mAction = ACTION_MULTIPLE;
mRepeatCount = count;
return handled;
}
return false;
}
return false;
}
會(huì)執(zhí)行相應(yīng)view的callback的onKeyDown,onKeyUp,onKeyMultiple方法,同時(shí)處理內(nèi)部的狀態(tài)信息。這時(shí)會(huì)執(zhí)行activity和View的onKeyDown方法。
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (getApplicationInfo().targetSdkVersion
>= Build.VERSION_CODES.ECLAIR) {
event.startTracking();
} else {
onBackPressed();
}
return true;
}
if (mDefaultKeyMode == DEFAULT_KEYS_DISABLE) {
return false;
} else if (mDefaultKeyMode == DEFAULT_KEYS_SHORTCUT) {
Window w = getWindow();
if (w.hasFeature(Window.FEATURE_OPTIONS_PANEL) &&
w.performPanelShortcut(Window.FEATURE_OPTIONS_PANEL, keyCode, event,
Menu.FLAG_ALWAYS_PERFORM_CLOSE)) {
return true;
}
return false;
} else if (keyCode == KeyEvent.KEYCODE_TAB) {
// Don't consume TAB here since it's used for navigation. Arrow keys
// aren't considered "typing keys" so they already won't get consumed.
return false;
} else {
// Common code for DEFAULT_KEYS_DIALER & DEFAULT_KEYS_SEARCH_*
boolean clearSpannable = false;
boolean handled;
if ((event.getRepeatCount() != 0) || event.isSystem()) {
clearSpannable = true;
handled = false;
} else {
handled = TextKeyListener.getInstance().onKeyDown(
null, mDefaultKeySsb, keyCode, event);
if (handled && mDefaultKeySsb.length() > 0) {
// something useable has been typed - dispatch it now.
final String str = mDefaultKeySsb.toString();
clearSpannable = true;
switch (mDefaultKeyMode) {
case DEFAULT_KEYS_DIALER:
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + str));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
break;
case DEFAULT_KEYS_SEARCH_LOCAL:
startSearch(str, false, null, false);
break;
case DEFAULT_KEYS_SEARCH_GLOBAL:
startSearch(str, false, null, true);
break;
}
}
}
if (clearSpannable) {
mDefaultKeySsb.clear();
mDefaultKeySsb.clearSpans();
Selection.setSelection(mDefaultKeySsb,0);
}
return handled;
}
}
經(jīng)過(guò)這些處理如果還是沒(méi)有完成,則開(kāi)始第三種情況mWindow.onKeyDown(mFeatureId, event.getKeyCode(), event)執(zhí)行window的onKeyDowm。
到這里也就完整的完成了整個(gè)輸入流程的講解。其中有很大篇幅講解的是android系統(tǒng)中key事件的傳遞流程,這個(gè)可以跟輸入法流程分開(kāi)來(lái)看。
View的各種KeyEvent.Callback接口早于Activity的對(duì)應(yīng)接口被調(diào)用;
整個(gè)處理環(huán)節(jié)中只要有一處表明處理掉了,則處理結(jié)束,不在往下傳遞;
各種Callback接口的處理優(yōu)先級(jí)低于監(jiān)聽(tīng)器,也就是說(shuō)各種onXXXListener的方法優(yōu)先被調(diào)用。
