想寫點什么東西放在這里,到后來發現沒什么用,直接進入正題吧,本文主要是由于工作的遇到的一個問題,之后自己閑的蛋疼研究研究之后一發不可收拾,整篇文章從 Framework層開始一直到我們平時處理事件的View進行了整體的介紹,說是分析,其實就是梳理一遍事件的傳遞的流程,不要說我是標題黨就好。由于時間及經驗有限,文中可能存在錯誤與不足,歡迎大家指出,我會第一時間對文章進行修改糾正。
在說正文之前,必須要感謝幾個人:以我遇到的時間排序。
- Android按鍵事件處理流程 -KeyEvent,作者tmp_zhao目前已經遷到簡書。這篇文章介紹了事件,準確的說是按鍵事件(KeyEvent)從DecorView到View的整體流程。
- Android 中keyEvent的消息處理,作者是轉載的,沒有找到原作者。這篇文章介紹事件是怎么從連接層傳到DecorView的,簡單介紹,沒有過多說明。
- Android源碼解析(三十)-->觸摸事件分發流程,作者一片楓葉_劉超,為我之前的猜想提供了充分有力的證據。
- Android中事件傳遞分析,作者zjutkz提供了從 Framework層到ViewRootImpl的證據,大神。
非常感謝以上四位大神的分享。
整體流程:系統核心->Native層->ViewRootImpl->DecorView->Activity->Window->DecorView->ViewGroup->View,無論是觸摸事件還是點擊時間都是一樣的。
系統核心傳到Framework的不去討論了,我也不會,這篇文章是從ViewRootImpl開始說,關于怎么從Framework傳到ViewRootImpl的,有興趣的可以去看上面推薦的文章。
ViewRootImpl
首先從Framework中傳遞過來的事件會到InputEventReceiver的onInputEvent方法,在ViewRootImpl中的它的實現類是:
final class WindowInputEventReceiver extends InputEventReceiver {
public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {
super(inputChannel, looper);
}
@Override
public void onInputEvent(InputEvent event) {
enqueueInputEvent(event, this, 0, true);
}
@Override
public void onBatchedInputEventPending() {
if (mUnbufferedInputDispatch) {
super.onBatchedInputEventPending();
} else {
scheduleConsumeBatchedInput();
}
}
@Override
public void dispose() {
unscheduleConsumeBatchedInput();
super.dispose();
}
}
當一個事件產生,通過一系列的傳遞,最終會到WindowInputEventReceiver的onInputEvent方法中進行排隊處理,之后會調到ViewRootImpl的enqueueInputEvent方法:
void enqueueInputEvent(InputEvent event,
InputEventReceiver receiver, int flags, boolean processImmediately) {
adjustInputEventForCompatibility(event);
QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);
// Always enqueue the input event in order, regardless of its time stamp.
// We do this because the application or the IME may inject key events
// in response to touch events and we want to ensure that the injected keys
// are processed in the order they were received and we cannot trust that
// the time stamp of injected events are monotonic.
QueuedInputEvent last = mPendingInputEventTail;
if (last == null) {
mPendingInputEventHead = q;
mPendingInputEventTail = q;
} else {
last.mNext = q;
mPendingInputEventTail = q;
}
mPendingInputEventCount += 1;
Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
mPendingInputEventCount);
<------Look here------>
if (processImmediately) {
doProcessInputEvents();
} else {
scheduleProcessInputEvents();
}
}
不考慮延遲的情況,直接看doProcessInputEvents():
void doProcessInputEvents() {
// Deliver all pending input events in the queue.
while (mPendingInputEventHead != null) {
QueuedInputEvent q = mPendingInputEventHead;
mPendingInputEventHead = q.mNext;
if (mPendingInputEventHead == null) {
mPendingInputEventTail = null;
}
q.mNext = null;
mPendingInputEventCount -= 1;
Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
mPendingInputEventCount);
long eventTime = q.mEvent.getEventTimeNano();
long oldestEventTime = eventTime;
if (q.mEvent instanceof MotionEvent) {
MotionEvent me = (MotionEvent)q.mEvent;
if (me.getHistorySize() > 0) {
oldestEventTime = me.getHistoricalEventTimeNano(0);
}
}
mChoreographer.mFrameInfo.updateInputEventTime(eventTime, oldestEventTime);
deliverInputEvent(q);
}
// We are done processing all input events that we can process right now
// so we can clear the pending flag immediately.
if (mProcessInputEventsScheduled) {
mProcessInputEventsScheduled = false;
mHandler.removeMessages(MSG_PROCESS_INPUT_EVENTS);
}
}
上面的代碼中最重要的就是deliverInputEvent(q)這句,其他的都是一些排列啊,獲得事件觸發的事件啊,之類額,不是很懂,也不過介紹了。繼續往下看:
private void deliverInputEvent(QueuedInputEvent q) {
Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent",
q.mEvent.getSequenceNumber());
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);
}
InputStage stage;
if (q.shouldSendToSynthesizer()) {
stage = mSyntheticInputStage;
} else {
stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
}
<------------重點----------------->
if (stage != null) {
stage.deliver(q);
} else {
finishInputEvent(q);
}
}
在這個方法中會創建一個InputStage對象,有興趣的可以去看一下具體這個類是怎么寫的,蠻有意思的,這里就不過多介紹了,只說兩個方法,一個是onProcess這個是處理event的方法,另一個是forward,讓下一個InputStage去處理。我們可以在ViewRootImpl的setView方法中找到上面出現的三種InputStage:
mSyntheticInputStage = new SyntheticInputStage();
InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
"aq:native-post-ime:" + counterSuffix);
InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
InputStage imeStage = new ImeInputStage(earlyPostImeStage,
"aq:ime:" + counterSuffix);
InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
"aq:native-pre-ime:" + counterSuffix);
mFirstInputStage = nativePreImeStage;
mFirstPostImeInputStage = earlyPostImeStage;
重點是在ViewPostImeInputStage的onProcess方法中:
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);
}
}
}
可以看到在這里對于InputEvent事件進行了分別的處理,KeyEvent事件調用java processKeyEvent(q)
,MotionEvent事件調用java processPointerEvent
。我們目前只關注KeyEvent,其實兩條路目前是一樣的。
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;
}
if (shouldDropInputEvent(q)) {
return FINISH_NOT_HANDLED;
}
//省略一部分代碼
}
這里的mView,其實就是DecorView。事件就從連接層傳到了視圖層(這么說可能不太準確,個人理解吧)。
我們來總結一下在ViewRootImpl中都做了哪些事情:
- 在WindowInputEventReceiver這個接口中接受從Framework層傳遞過來的輸入事件。
- 使用QueuedInputEvent對所有的事件進行排隊,然后根據不同的條件,狀態把事件分發下去。
- 在處理事件的時候使用InputStage能夠實現對事件的按照順序處理。
- 在ViewPostImeInputStage對事件進行分發,根據事件類別分發到DecorView中。
以上就是在ViewRootImpl中進行的處理。
DecorView
接著上文,我們繼續往下分析,這里只針對KeyEvent。先來看看java dispatchKeyEvent(p)
這個方法:
使用的是API24的版本的源碼,跟之前版本的可能會有所區別,不過大體上是一樣的。
<!--DecorView.java-->
public boolean dispatchKeyEvent(KeyEvent event) {
final int keyCode = event.getKeyCode();
final int action = event.getAction();
final boolean isDown = action == KeyEvent.ACTION_DOWN;
/// 1. 第一次down事件的時候,處理panel的快捷鍵
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;
}
}
}
/// 2. 這里是我們本文的重點,當window沒destroy且其Callback非空的話,交給其Callback處理
/// 其中 mFeatureId < 0 表示是Application的DecorView
if (!mWindow.isDestroyed()) {
final Window.Callback cb = mWindow.getCallback();// Activity、Dialog都是Callback接口的實現
final boolean handled = cb != null && mFeatureId < 0 ?
cb.dispatchKeyEvent(event)//Activity,Dialog。
: super.dispatchKeyEvent(event);//否則直接派發到ViewGroup#dispatchKeyEvent(View層次結構)
if (handled) {
return true;
}
}
/// 3. 這是key事件的最后一步,如果到這一步還沒處理掉,則派發到PhoneWindow對應的onKeyDown, onKeyUp方法
return isDown ? mWindow.onKeyDown(mFeatureId, event.getKeyCode(), event)
: mWindow.onKeyUp(mFeatureId, event.getKeyCode(), event);
}
代碼上都有相應的注釋,簡單解釋一下就是如果當前的DecorView是Application的DecorView的話,并且存在Activity或者Dialog,那么會執行Activity或Dialog的java dispatchKeyEvent()
方法,否則的話就直接執行ViewGroup的java dispatchKeyEvent()
方法。并且如果,沒有消耗掉這個事件的話就回去去執行PhoneWindow的onKeyDown或onKeyUp方法。
接下來按照正常的邏輯會調用Activity的java dispatchKeyEvent()
方法:
<!-- Activity -- >
public boolean dispatchKeyEvent(KeyEvent event) {
onUserInteraction();
// Let action bars open menus in response to the menu key prioritized over
// the window handling it
//處理ActionBar和Menu 略過
final int keyCode = event.getKeyCode();
if (keyCode == KeyEvent.KEYCODE_MENU &&
mActionBar != null && mActionBar.onMenuKeyEvent(event)) {
return true;
} else if (event.isCtrlPressed() &&
event.getUnicodeChar(event.getMetaState() & ~KeyEvent.META_CTRL_MASK) == '<') {
// Capture the Control-< and send focus to the ActionBar
final int action = event.getAction();
if (action == KeyEvent.ACTION_DOWN) {
final ActionBar actionBar = getActionBar();
if (actionBar != null && actionBar.isShowing() && actionBar.requestFocus()) {
mEatKeyUpEvent = true;
return true;
}
} else if (action == KeyEvent.ACTION_UP && mEatKeyUpEvent) {
mEatKeyUpEvent = false;
return true;
}
}
//重點在這里,調用Window的superDispatchKeyEvent(event)方法
Window win = getWindow();
if (win.superDispatchKeyEvent(event)) {
return true;
}
View decor = mDecor;
if (decor == null) decor = win.getDecorView();
//如果View層次沒有處理的話,就交給KeyEvent本身的dispatch方法,Activity的各種回調方法會被觸發
return event.dispatch(this, decor != null
? decor.getKeyDispatcherState() : null, this);
}
在Activity中,先進行判斷,之后調用Window的java superDispatchKeyEvent(event)
,將事件向下派發到View層次,如果View層次沒有處理的話,會調用Activity中KeyEvent的各種回調方法。沿著主路線繼續看:
<!-- Window.java -->
/**
* Used by custom windows, such as Dialog, to pass the key press event
* further down the view hierarchy. Application developers should
* not need to implement or call this.
*
*/
public abstract boolean superDispatchKeyEvent(KeyEvent event);
<!-- 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.
//如果有設置ActionMode,則優先調用ActionMode
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;
}
}
//繼續下發至View層。
return super.dispatchKeyEvent(event);
}
通過一系列的調用最終調用到java super.dispatchKeyEvent(event);
,因為DecorView的父類是FrameLayout;所以事件從這里開始就正式進入View層次了。
我們來總結一下在DecorView中都做了哪些事情:
- 在
java DecorView.dispatchKeyEvent(ev)
方法中接受從ViewRootImpl傳過來的事件,判斷,分發至Activity或View處理,如果都沒有處理的話,則執行PhoneWindow的相關方法。 - 在
java Activity.dispatchKeyEvent(ev)
方法中調用Window的處理方法,如果沒有被處理的話,會觸發Activity實現KeyEvent.Callback的相關方法。 - 通過Window->PhoneWindow->DecorView,事件再次傳遞到
java DecorView.superDispatchKeyEvent(ev)
方法中,直接調用父類的java dispatchKeyEvent(event)
事件被傳遞到View層。
在DecorView層接收事件,再通過DecorView層將事件發下去。
View
相對于MotionEvent,KeyEvent的最大的不同點就是在View層的處理,其實以上的步驟同樣適用于MotionEvent。有興趣的童鞋可以從ViewRootImpl中的InputStage找到線索。
繼續我們的主題,當事件傳遞到View層之后首先被觸發的應該是java ViewGroup.dispatchKeyEvent(ev)
方法:
<!--ViewGroup.java -- >
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
//KeyEvent一致性檢測,忽略
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onKeyEvent(event, 1);
}
if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
== (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
//如果此ViewGroup是focused或者具體的大小被設置了(有邊界),則交給它處理,即調用View的實現
if (super.dispatchKeyEvent(event)) {
return true;
}
} else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
== PFLAG_HAS_BOUNDS) {
//否則,如果此ViewGroup中有focused的child,且child有具體的大小,則交給mFocused處理
if (mFocused.dispatchKeyEvent(event)) {
return true;
}
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
}
return false;
}
這里我們可以看出對KeyEvent來說在View層次結構中,如果ViewGroup條件滿足則會優先處理事件而不是先派發給其孩子view,這第一點是和MotionEvent最大的不同,在處理MotionEvent的時候ViewGroup是默認不處理的,只有當子View不處理的時候才會交給ViewGroup處理。接下來看看java View.dispatchKeyEvent(ev)
:
<! -- View.java -- >
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;
//調用onKeyListener,如果它非空且view是ENABLED狀態,監聽器優先觸發
if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
return true;
}
//調用KeyEvent.dispatch方法,并將view對象本身作為參數傳遞進去,view的各種callback方法在這里被觸發
if (event.dispatch(this, mAttachInfo != null
? mAttachInfo.mKeyDispatchState : null, this)) {
return true;
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return false;
}
這里可以看到處理的邏輯還是蠻清晰的,對ViewGroup進行進行Flag的判斷,決定是自己處理還是向下派發,如果View是可用的并且有對應的監聽器,則觸發監聽器,否則觸發View實現的KeyEvent.Callback的回調。其中在View處理的時候有一個 mAttachInfo.mKeyDispatchState變量,這個類是KeyEvent中的DispatcherState,靜態類,作用是在KeyEvent.dispatch()中處理并追蹤事件。這里不過多介紹了,詳情可以參考這篇文章。
關于ViewGroup的flag可以參考這篇文章
我們來總結一下在View中都做了哪些事情:
- 在ViewGroup中判斷Flag來決定是否處理事件,不處理則交給子View處理;
- 在View中先判斷有沒有監聽器,有則觸發,沒有則觸發KeyEvent.dispatch()回調;
到這里關于事件的派發流程其實就算是告一段落了,從ViewRootImpl到DecorView再到View層。接下來還有一些要說的那就是回調,也就是返回路線。
返回路線
通過上面的內容我們了解到了事件從ViewRootImpl傳遞到View層,但是如果到View層沒有被消耗處理的話,那這個事件接下來應該怎么派發?
對于MotionEvent會傳遞到父布局,最終傳遞到Activity。那對于KeyEvent是怎么處理這種情況的那?
三個主要的方法:
- View中的KeyEvent.dispatch()方法;
- Activity中的KeyEvent.dispatch()方法;
- DecorView中PhoneWindow的onKeyDown和onKeyUp;
先來看看KeyEvent.dispatch()方法都做了哪些事情:
<!--KeyEvent.java -- >
/**
* Deliver this key event to a {@link Callback} interface. If this is
* an ACTION_MULTIPLE event and it is not handled, then an attempt will
* be made to deliver a single normal event.
*
* @param receiver The Callback that will be given the event.
* @param state State information retained across events.
* @param target The target of the dispatch, for use in tracking.
*
* @return The return value from the Callback method that was called.
*/
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);
// 回調Callback接口的onKeyDown方法,View和Activity都是此接口的實現者
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);
}
//回調Callback接口的onKeyUp方法,View和Activity都是此接口的實現者
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;
}
簡單說一下這段代碼,在dispatch方法中對于keyevent的action進行了判斷,分別調用Callback接口的實現類相對應的方法。其他的一下是之前說過的KeyEvent中的DispatcherState這個類對于事件的處理,本人也不是很清楚,簡單來說就是對事件進行一些標記,跟蹤之類的。
分別看一下View和Activity對于KeyEvent.Callback的實現方法:
<!-- View.java -- >
/**
* Default implementation of {@link KeyEvent.Callback#onKeyDown(int, KeyEvent)
* KeyEvent.Callback.onKeyDown()}: perform press of the view
* when {@link KeyEvent#KEYCODE_DPAD_CENTER} or {@link KeyEvent#KEYCODE_ENTER}
* is released, if the view is enabled and clickable.
* <p>
* Key presses in software keyboards will generally NOT trigger this
* listener, although some may elect to do so in some situations. Do not
* rely on this to catch software key presses.
*
* @param keyCode a key code that represents the button pressed, from
* {@link android.view.KeyEvent}
* @param event the KeyEvent object that defines the button action
*/
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (KeyEvent.isConfirmKey(keyCode)) {// 只處理KEYCODE_DPAD_CENTER、KEYCODE_ENTER這2個按鍵
if ((mViewFlags & ENABLED_MASK) == DISABLED) {
return true;// 針對disabled View直接返回true表示處理過了
}
// Long clickable items don't necessarily have to be clickable.
if (((mViewFlags & CLICKABLE) == CLICKABLE
|| (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
&& (event.getRepeatCount() == 0)) { // clickable或者long_clickable且是第一次down事件
// For the purposes of menu anchoring and drawable hotspots,
// key events are considered to be at the center of the view.
final float x = getWidth() / 2f;
final float y = getHeight() / 2f;
setPressed(true, x, y);// 標記pressed,你可能設置了View不同的background,這時候就會有所體現(比如高亮效果)
checkForLongClick(0, x, y);// 啟動View的long click檢測
return true;// 到達這一步就表示KeyEvent被處理掉了
}
}
return false;
}
private void checkForLongClick(int delayOffset, float x, float y) {
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { // 必須得是LONG_CLICKABLE的View
mHasPerformedLongPress = false;// 設置初始值
if (mPendingCheckForLongPress == null) {// 只非空的時候才new一個
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.setAnchor(x, y);
mPendingCheckForLongPress.rememberWindowAttachCount();
postDelayed(mPendingCheckForLongPress,/ post一個Runnable,注意延遲是個差值,而不是delayOffset
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}
private final class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
private float mX;
private float mY;
@Override
public void run() {
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {// 當時間到了,此Runnable沒被移除掉的話,并且這些條件都滿足的時候,
if (performLongClick(mX, mY)) {// 客戶端定義的onLongClickListener監聽器被觸發
mHasPerformedLongPress = true;
}
}
}
public void setAnchor(float x, float y) {
mX = x;
mY = y;
}
public void rememberWindowAttachCount() {
mOriginalWindowAttachCount = mWindowAttachCount;
}
}
/**performLongClick()最終會調到這個方法
* Calls this view's OnLongClickListener, if it is defined. Invokes the
* context menu if the OnLongClickListener did not consume the event,
* optionally anchoring it to an (x,y) coordinate.
*
* @param x x coordinate of the anchoring touch event, or {@link Float#NaN}
* to disable anchoring
* @param y y coordinate of the anchoring touch event, or {@link Float#NaN}
* to disable anchoring
* @return {@code true} if one of the above receivers consumed the event,
* {@code false} otherwise
*/
private boolean performLongClickInternal(float x, float y) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
boolean handled = false;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLongClickListener != null) {// 優先觸發監聽器
handled = li.mOnLongClickListener.onLongClick(View.this);
}
if (!handled) {// 如果還沒處理,顯示ContextMenu如果定義了的話
final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y);
handled = isAnchored ? showContextMenu(x, y) : showContextMenu();
}
if (handled) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
return handled;// 返回處理結果
}
/**
* Default implementation of {@link KeyEvent.Callback#onKeyUp(int, KeyEvent)
* KeyEvent.Callback.onKeyUp()}: perform clicking of the view
* when {@link KeyEvent#KEYCODE_DPAD_CENTER}, {@link KeyEvent#KEYCODE_ENTER}
* or {@link KeyEvent#KEYCODE_SPACE} is released.
* <p>Key presses in software keyboards will generally NOT trigger this listener,
* although some may elect to do so in some situations. Do not rely on this to
* catch software key presses.
*
* @param keyCode A key code that represents the button pressed, from
* {@link android.view.KeyEvent}.
* @param event The KeyEvent object that defines the button action.
*/
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (KeyEvent.isConfirmKey(keyCode)) {// 同onKeyDown,默認也只處理confirm key
if ((mViewFlags & ENABLED_MASK) == DISABLED) {// 同樣的邏輯,如果是DISABLED view,直接返回true表示處理過了
return true;
}
if ((mViewFlags & CLICKABLE) == CLICKABLE && isPressed()) {
setPressed(false); // 重置pressed狀態
if (!mHasPerformedLongPress) {// 長按沒發生的話,
// This is a tap, so remove the longpress check
removeLongPressCallback();// 當up事件發生的時候,移除這些已經沒用的callback
return performClick();// 調用單擊onClick監聽器
}
}
}
return false;// 其他所有的Key默認不處理
}
/**
* Sets the pressed state for this view.
*
* @see #isClickable()
* @see #setClickable(boolean)
*
* @param pressed Pass true to set the View's internal state to "pressed", or false to reverts
* the View's internal state from a previously set "pressed" state.
*/
public void setPressed(boolean pressed) {
final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
if (pressed) {
mPrivateFlags |= PFLAG_PRESSED;
} else {
mPrivateFlags &= ~PFLAG_PRESSED;
}
if (needsRefresh) {
refreshDrawableState();// 這行代碼會刷新View的顯示狀態
}
dispatchSetPressed(pressed);
}
接下來,看看Activity對應的onKeyDown,onKeyUp方法:
<! -- Activity.java -- >
/**
* Called when a key was pressed down and not handled by any of the views
* inside of the activity. So, for example, key presses while the cursor
* is inside a TextView will not trigger the event (unless it is a navigation
* to another object) because TextView handles its own key presses.
*
* <p>If the focused view didn't want this event, this method is called.
*
* <p>The default implementation takes care of {@link KeyEvent#KEYCODE_BACK}
* by calling {@link #onBackPressed()}, though the behavior varies based
* on the application compatibility mode: for
* {@link android.os.Build.VERSION_CODES#ECLAIR} or later applications,
* it will set up the dispatch to call {@link #onKeyUp} where the action
* will be performed; for earlier applications, it will perform the
* action immediately in on-down, as those versions of the platform
* behaved.
*
* <p>Other additional default key handling may be performed
* if configured with {@link #setDefaultKeyMode}.
*
* @return Return <code>true</code> to prevent this event from being propagated
* further, or <code>false</code> to indicate that you have not handled
* this event and it should continue to be propagated.
* @see #onKeyUp
* @see android.view.KeyEvent
*/
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (getApplicationInfo().targetSdkVersion
>= Build.VERSION_CODES.ECLAIR) {
event.startTracking(); // 標記追蹤這個key event
} else {
onBackPressed();// 直接調用onBackPressed
}
return true; // 返回true表示被activity處理掉了
}
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 {
// 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;
}
}
/**
* Called when a key was released and not handled by any of the views
* inside of the activity. So, for example, key presses while the cursor
* is inside a TextView will not trigger the event (unless it is a navigation
* to another object) because TextView handles its own key presses.
*
* <p>The default implementation handles KEYCODE_BACK to stop the activity
* and go back.
*
* @return Return <code>true</code> to prevent this event from being propagated
* further, or <code>false</code> to indicate that you have not handled
* this event and it should continue to be propagated.
* @see #onKeyDown
* @see KeyEvent
*/
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (getApplicationInfo().targetSdkVersion
>= Build.VERSION_CODES.ECLAIR) {// 同onKeyDown,2.0之后的版本
if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
&& !event.isCanceled()) {
onBackPressed();// 在這種情況下執行onBackPressed表示處理掉了
return true;
}
}
return false;
}
還有的就是在DecorView中最下面的一句PhoneWindow的回調方法:
<! -- PhoneWindow.java -- >
/**
* A key was pressed down and not handled by anything else in the window.
*
* @see #onKeyUp
* @see android.view.KeyEvent
*/
protected boolean onKeyDown(int featureId, int keyCode, KeyEvent event) {
/* ****************************************************************************
* HOW TO DECIDE WHERE YOUR KEY HANDLING GOES.
*
* If your key handling must happen before the app gets a crack at the event,
* it goes in PhoneWindowManager.
*
* If your key handling should happen in all windows, and does not depend on
* the state of the current application, other than that the current
* application can override the behavior by handling the event itself, it
* should go in PhoneFallbackEventHandler.
*
* Only if your handling depends on the window, and the fact that it has
* a DecorView, should it go here.
* ****************************************************************************/
final KeyEvent.DispatcherState dispatcher =
mDecor != null ? mDecor.getKeyDispatcherState() : null;
//Log.i(TAG, "Key down: repeat=" + event.getRepeatCount()
// + " flags=0x" + Integer.toHexString(event.getFlags()));
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_UP:
case KeyEvent.KEYCODE_VOLUME_DOWN:
case KeyEvent.KEYCODE_VOLUME_MUTE: {
int direction = 0;
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_UP:
direction = AudioManager.ADJUST_RAISE;
break;
case KeyEvent.KEYCODE_VOLUME_DOWN:
direction = AudioManager.ADJUST_LOWER;
break;
case KeyEvent.KEYCODE_VOLUME_MUTE:
direction = AudioManager.ADJUST_TOGGLE_MUTE;
break;
}
// If we have a session send it the volume command, otherwise
// use the suggested stream.
if (mMediaController != null) {
mMediaController.adjustVolume(direction, AudioManager.FLAG_SHOW_UI);
} else {
MediaSessionLegacyHelper.getHelper(getContext()).sendAdjustVolumeBy(
mVolumeControlStreamType, direction,
AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_VIBRATE
| AudioManager.FLAG_FROM_KEY);
}
return true;
}
// These are all the recognized media key codes in
// KeyEvent.isMediaKey()
case KeyEvent.KEYCODE_MEDIA_PLAY:
case KeyEvent.KEYCODE_MEDIA_PAUSE:
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
case KeyEvent.KEYCODE_MUTE:
case KeyEvent.KEYCODE_HEADSETHOOK:
case KeyEvent.KEYCODE_MEDIA_STOP:
case KeyEvent.KEYCODE_MEDIA_NEXT:
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
case KeyEvent.KEYCODE_MEDIA_REWIND:
case KeyEvent.KEYCODE_MEDIA_RECORD:
case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: {
if (mMediaController != null) {
if (mMediaController.dispatchMediaButtonEvent(event)) {
return true;
}
}
return false;
}
case KeyEvent.KEYCODE_MENU: {
onKeyDownPanel((featureId < 0) ? FEATURE_OPTIONS_PANEL : featureId, event);
return true;
}
case KeyEvent.KEYCODE_BACK: {
if (event.getRepeatCount() > 0) break;
if (featureId < 0) break;
// Currently don't do anything with long press.
if (dispatcher != null) {
dispatcher.startTracking(event, this);
}
return true;
}
}
return false;
}
/**
* A key was released and not handled by anything else in the window.
*
* @see #onKeyDown
* @see android.view.KeyEvent
*/
protected boolean onKeyUp(int featureId, int keyCode, KeyEvent event) {
final KeyEvent.DispatcherState dispatcher =
mDecor != null ? mDecor.getKeyDispatcherState() : null;
if (dispatcher != null) {
dispatcher.handleUpEvent(event);
}
//Log.i(TAG, "Key up: repeat=" + event.getRepeatCount()
// + " flags=0x" + Integer.toHexString(event.getFlags()));
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_UP:
case KeyEvent.KEYCODE_VOLUME_DOWN: {
final int flags = AudioManager.FLAG_PLAY_SOUND | AudioManager.FLAG_VIBRATE
| AudioManager.FLAG_FROM_KEY;
// If we have a session send it the volume command, otherwise
// use the suggested stream.
if (mMediaController != null) {
mMediaController.adjustVolume(0, flags);
} else {
MediaSessionLegacyHelper.getHelper(getContext()).sendAdjustVolumeBy(
mVolumeControlStreamType, 0, flags);
}
return true;
}
case KeyEvent.KEYCODE_VOLUME_MUTE: {
// Similar code is in PhoneFallbackEventHandler in case the window
// doesn't have one of these. In this case, we execute it here and
// eat the event instead, because we have mVolumeControlStreamType
// and they don't.
getAudioManager().handleKeyUp(event, mVolumeControlStreamType);
return true;
}
// These are all the recognized media key codes in
// KeyEvent.isMediaKey()
case KeyEvent.KEYCODE_MEDIA_PLAY:
case KeyEvent.KEYCODE_MEDIA_PAUSE:
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
case KeyEvent.KEYCODE_MUTE:
case KeyEvent.KEYCODE_HEADSETHOOK:
case KeyEvent.KEYCODE_MEDIA_STOP:
case KeyEvent.KEYCODE_MEDIA_NEXT:
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
case KeyEvent.KEYCODE_MEDIA_REWIND:
case KeyEvent.KEYCODE_MEDIA_RECORD:
case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: {
if (mMediaController != null) {
if (mMediaController.dispatchMediaButtonEvent(event)) {
return true;
}
}
return false;
}
case KeyEvent.KEYCODE_MENU: {
onKeyUpPanel(featureId < 0 ? FEATURE_OPTIONS_PANEL : featureId,
event);
return true;
}
case KeyEvent.KEYCODE_BACK: {
if (featureId < 0) break;
if (event.isTracking() && !event.isCanceled()) {
if (featureId == FEATURE_OPTIONS_PANEL) {
PanelFeatureState st = getPanelState(featureId, false);
if (st != null && st.isInExpandedMode) {
// If the user is in an expanded menu and hits back, it
// should go back to the icon menu
reopenMenu(true);
return true;
}
}
closePanel(featureId);
return true;
}
break;
}
case KeyEvent.KEYCODE_SEARCH: {
/*
* Do this in onKeyUp since the Search key is also used for
* chording quick launch shortcuts.
*/
if (getKeyguardManager().inKeyguardRestrictedInputMode()) {
break;
}
if (event.isTracking() && !event.isCanceled()) {
launchDefaultSearch(event);
}
return true;
}
case KeyEvent.KEYCODE_WINDOW: {
if (mSupportsPictureInPicture && !event.isCanceled()) {
getWindowControllerCallback().enterPictureInPictureModeIfPossible();
}
return true;
}
}
return false;
}
至此所有按鍵事件的處理就分析完畢了。最后我們來個整體的總結一下。
- View的各種KeyEvent.Callback接口早于Activity的對應接口被調用;
- 整個處理環節中只要有一處表明處理掉了,則處理結束,不在往下傳遞;
- 各種Callback接口的處理優先級低于監聽器,也就是說各種onXXXListener的方法優先被調用。
總結
梳理一遍整體流程:
- ViewRootImpl中的WindowInputEventReceiver接受時間,排列分發。
- ViewRootImpl中的InputStage對事件進行順序處理。
- 通過ViewPostImeInputStage將事件傳遞到DecorView中。
- DecorView進行判斷當前狀態,將事件傳遞給Activity或ViewGroup。
- Activity將事件傳遞給Window再傳遞給DecorView,然后傳遞到View層。
- ViewGroup先判斷是否消耗,消耗則事件終止,不消耗則傳遞給包括的子View。
- View先判斷是否有監聽器,優先觸發監聽,沒有則觸發View實現KeyEvent.Callback方法。
- 如果View監聽器和回調都返回false,事件往回傳遞至Activity。
- 執行Activity實現KeyEvent.Callback的相關方法。
- 如果Activity中仍返回false,不消耗,則繼續往回返,傳遞至DecorView。
- 執行PhoneWindow的onKeyDown和onKeyUp。
整體的流程應該就是這樣,如果有不對的地方歡迎批評指正,我會第一時間更改糾正