當用戶觸摸屏幕或者按鍵操做,首次觸發的是硬件驅動,驅動收到事件後,將該相應事件寫入到輸入設備節點交由內核處理, 產生最原生態的內核事件。內核會將觸摸事件包裝成EVENT做爲文件存儲到"/dev/input/event[x]"
目錄中。java
而後,InputReaderThread
會不斷的從"/dev/input/event[x]"
目錄中讀取事件,並會把事件交給InputDispatch
處理。InputDispatch
會把事件分發到須要的地方。android
以上的這些步驟都是C/C++代碼實現,只須要對流程作一些簡單的瞭解便可。而真正java開始拿到事件的起點是在ViewRootImpl
的內部類WindowInputEventReceiver
之中。git
WindowInputEventReceiver.onInputEvent()
經過InputDispatch
調用接受到事件以後,首先將事件傳給了enqueueInputEvent()
,將新接受的事件插入到事件隊列中,接着調用doProcessInputEvents()
。緩存
class ViewRootImpl {
...
final class WindowInputEventReceiver extends InputEventReceiver {
...
@Override
public void onInputEvent(InputEvent event, int displayId) {
enqueueInputEvent(event, this, 0, true);
}
}
void enqueueInputEvent(InputEvent event, InputEventReceiver receiver, int flags, boolean processImmediately) {
...
QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);
QueuedInputEvent last = mPendingInputEventTail;
last.mNext = q;
...
doProcessInputEvents();
}
}
複製代碼
doProcessInputEvents()
循環着取出全部的事件,按照順序將事件交給deliverInputEvent()
,而它的工做就是將事件發送到InputStage的表頭中。安全
void doProcessInputEvents() {
...
while (mPendingInputEventHead != null) {
QueuedInputEvent q = mPendingInputEventHead;
mPendingInputEventHead = q.mNext;
...
deliverInputEvent(q);
}
}
private void deliverInputEvent(QueuedInputEvent q) {
...
InputStage stage;
if (q.shouldSendToSynthesizer()) {
stage = mSyntheticInputStage;
} else {
stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
}
stage.deliver(q);
...
}
複製代碼
InputStage
是一個單向鏈表結構,這個鏈表中有一系列的InputStage
,他們都能對事件作出立,事件就在它們上面傳遞,若是事件沒有被前面的InputStage
標記Finished
,當前的InputStage
就會嘗試消費它。其中ViewPostImeInputStage
就會將事件發送到View處理處理。markdown
ViewPostImeInputStage.onProcess()
拿到事件後,交給ProcessPointerEvent()
,這就調用到mView.dispatchPointerEvent()
,而ViewRootImpl.mView
衆所周知,也就是DecorView
,也就是終於走到了View中進行處理了!(這裏還有疑問的翻閱一下上一篇文章)。ide
final class ViewPostImeInputStage extends InputStage {
...
@Override
protected int onProcess(QueuedInputEvent q) {
...
return processPointerEvent(q);
}
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
...
boolean handled = mView.dispatchPointerEvent(event);
...
return handled ? FINISH_HANDLED : FORWARD;
}
}
複製代碼
首先ViewRootImpl
將事件傳遞到DecorView.dispatchPointerEvent()
,DecorView
沒有實現這個方法,實際調用View.dispatchPointerEvent()
,而它又調用了DecorView.dispatchTouchEvent()
將事件傳回DecorView
。函數
緊接着DecorView
經過Window.getCallback()
將事件傳遞到了Acitivity.dispatchTouchEvent()
。oop
Activity
又將事件又傳給了PhoneWindow
,PhoneWindow
將事件直接透傳回DecorView
,最終調用到super.dispatchTouchEvent()
也就是ViewGroup.dispatchTouchEvent()
。post
///View
public final boolean dispatchPointerEvent(MotionEvent event) {
...
return dispatchTouchEvent(event);
}
///DecorView
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
...
return cb.dispatchTouchEvent(ev);
}
///Activity
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
///PhoneWindow
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
///DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
複製代碼
這一步看起來會有點疑惑行爲的感受:
第一點,爲何DecorView
要把事件傳遞給Activity
而後又傳遞回來?
Activity.dispatchTouchEvent
中首先將事件又交給View樹,若是沒有被消費,Activity
就能夠拿到這個事件並作處理,官方註釋寫到在其中能夠處理在窗口範圍外發生的觸摸事件效果最佳。如此,就能夠理解爲,Activity
但願作事件分發的起點,因此須要傳給Activity
由它來啓動。
第二個問題,傳遞的目標是交給Activity
而後再傳遞給DecorView
,爲何ViewRootImpl
將事件傳給了DecorView
而不是直接傳遞給Activity
?而Activity
爲何經過Window
去傳遞?並且PhoneWindow
中就只是將事件直接透傳給DecorView
。
這個問題在上一篇文章就討論過,這樣設計的目的是單一原則以及解耦,ViewRootImpl
並不知道Activity的存在,它只有DecorView
的對象,同理Activity
並不知道DecorView
的存在,它只有Window
的對象,上一次說到Window
存在的意義就是承載和處理視圖,Activity
並不關心它內部是怎樣實現的,只須要將事件交給他就好了,另外一方面,這幾個類儘可能最少的持有對方,最大程度上的實現解耦,而解耦是爲了什麼,就麼有必要再累贅了。
上一節分析到,事件被分發到了DecorView
,而且調用了super.dispatchTouchEvent()
,也就是ViewGroup.dispatchTouchEvent()
。這一節主要分析一下ViewGroup.dispatchTouchEvent()
都作了那些事情,又是如何完成了事件的向下傳遞。
ViewGroup.dispatchTouchEvent()
比較長,將這個方法大體拆分爲三個步驟來進行分析:1. 攔截 2. 尋找分發目標 3. 分發
首先先了解一下流程中十分重要的兩個概念:
PointerId
:觸摸點Id,多指操做時,每根手指從按下、移動到離開屏幕,都會擁有一個固定PointerId與對應的手指進行綁定。Id的範圍是0..31,官方目前假設這是安全的,由於底層的管道也是這樣設計的。
32個數字範圍的優勢就是能夠用32位的int
來存儲一組pointerId
,而且方便去計算和查詢。
TouchTarget
:緩存觸摸的child
以及它所消費的觸摸點Id集合,也就表示事件派發目標。
TouchTarget
是一個單向鏈表結構,ViewGroup經過mFirstTouchTarget
持有表頭。
TouchTarget
自身維護了一個複用池,池子的容量也是32個。
分發最首先的操做是看本身是否要攔截這一次事件,若是自身選擇了攔截,事件就再也不下發,而是交給本身處理。
public boolean dispatchTouchEvent(MotionEvent ev) {
...
///消費標誌
boolean handled = false;
///onFilterTouchEventForSecurity():安全檢查
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
///若是是ACTION_DOWN,就表示一組新的事件的開始,清除舊的TouchTarget以及重置滾動狀態
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 攔截標誌
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
///判斷是否容許進行攔截
///子view調用parent.requestDisallowInterceptTouchEvent()影響的就是這裏
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
///onInterceptTouchEvent():攔截的方法,子類通常經過實現這個方法來進行事件攔截
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
///不是ACTION_DOWN而且沒有touchTarget
///也就表示已經以前全部的view都接受到以前ACTION_DOWN事件,但都沒有消費
///也就是沒有視圖會願意接受這個事件,直接攔截掉
intercepted = true;
}
...
}
}
複製代碼
子view經過調用parent.requestDisallowInterceptTouchEvent()
阻止parent
攔截事件分析一下前置條件actionMasked == MotionEvent.ACTION_DOWN
和mFirstTouchTarget != null
。
ACTION_DOWN
事件,那麼在執行判斷以前就會先執行clear
操做,不只清除了TouchTarget
,也清除了mGroupFlags
標誌位,而又能夠看到阻止攔截就是經過mGroupFlags
標誌,因此child
設置的攔截標誌直接被重置掉了。也就是說當是ACTION_DOWN
事件的時候,子view的阻止攔截會直接失效,還能夠說,child
阻止parent
攔截最多到下一次ACTION_DOWN
事件的時候。mFirstTouchTarget
不爲空,而且不是ACTION_DOWN
事件,這個時候纔會根據標誌判斷是否走攔截的方法。整體分析,若是想要進行攔截,首先就是child
要拿到拿到ACTION_DOWN
或者稍後的事件,這個時候再調用parent.requestDisallowInterceptTouchEvent()
更改標誌位來阻止parent
攔截這個ACTION_DOWN
同一組的事件事件。
當事件不是ACTION_CANCEL
而且沒有被攔截,並且必須是一個DOWN
事件,纔會去尋找派發的目標View。首先若是是ACTION_CANCEL
或者被攔截,事件都沒有了繼續傳遞的必要,其次,DOWN
之類的事件標記着一組事件的起始,若是想要對一組事件進行處理,必須在DOWN
產生的時候就拿到並處理過它,而這種狀況就已經存在了TouchTarget
,也是沒有查找派發目標的必要。
其次就是遍歷全部的子view,尋找符合條件的view。而遍歷時的順序首先遵循Z軸的順序,也就是視覺上的從前面到後面,其次是遵循繪製順序,最後繪製的優先級最高,這樣在視圖有重疊的時候,也是視覺上的從前面到後面。
遍歷的時候,須要根據多個條件來篩選判斷當前view是否會接受這個事件,篩選的前後順序:
TouchTarget
鏈表是否已經有了child
的TouchTarget
,若是有了只須要將觸摸點Id存入這個TouchTarget
,而後跳出循環,走後續的派發流程就可。這種狀況會發生在多指操做的時候,第一根手指落在一個view上面以後,第二根手指也落在這個view上面,就只須要繼續講這個事件發送給這個view便可。child
去處理,即將事件向下傳遞,若是當前child
或者它的child
處理了這個事件,那麼當前這個child
就是派發目標,新建一個TouchTarget
存入到TouchTarget
鏈表的頭部。當遍歷完全部的child
沒有找到派發的目標,而且TouchTarget
鏈表不是空的,那麼就會將最先添加的TouchTarget
做爲派發目標。
public boolean dispatchTouchEvent(MotionEvent ev) {
...
///消費標誌
boolean handled = false;
///onFilterTouchEventForSecurity():安全檢查
if (onFilterTouchEventForSecurity(ev)) {
...
///ACTION_CANCEL事件標誌
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
///分裂標誌(是否支持多指操做)
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;///已經分配給新的TouchTarget標誌
///1. 不是取消而且沒被攔截才進行派發
///2. 是一個DOWN事件(ACTION_HOVER_MOVE是指針懸浮在view上)
/// 表示是一個新的事件序列開始了,須要從新找Target
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
///觸摸點ID轉換爲32位標誌
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
///清除TouchTarget列表中的相同的觸摸點Id,由於是一個新的事件隊列,以前的就已經失效了
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
///構建一個子view的列表在原有的順序基礎上,優先根據z軸排序
///這個列表會在全部子view都沒有z軸返回空值
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
///view是否按規定正常繪製的標誌
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
///preorderedList可能爲null,經過這兩個方法獲的實際的index和child
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...
///判斷是否要將事件發給child
///canViewReceivePointerEvents():子類是否能夠接收,條件是view可見 或者 正在播放動畫
///isTransformedTouchPointInView():觸摸點是否命中view範圍
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
///查詢已有的TouchTarget列表中是否已經有該child的TouchTarget
///若是有的話只須要給觸摸點Id集合添加新的Id便可
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
///重置child的detached標誌
///(child未將事件處理完成就detached,就會將後續的事件設爲CANCEL)
resetCancelNextUpFlag(child);
///dispatchTransformedTouchEvent():將事件向下傳遞交給view去處理
///返回true就是view成功消費了這個事件
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
///若是返回了true,更新一下相關數據,中止循環
///而且建立一個TouchTarget,插入到mFirstTouchTarget前面做爲表頭
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;///已經派發了事件的標誌
break;
}
}
if (preorderedList != null) preorderedList.clear();
}
///若是沒有找到派發的目標,可是存在舊的TouchTarget
///將事件派發給最先添加的Target
if (newTouchTarget == null && mFirstTouchTarget != null) {
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
...
}
}
複製代碼
須要注意的是,當是一次ACTION_DOWN
事件的時候,大機率在這個過程當中並不會存在TouchTarget
,那麼在查找派發目標的過程當中,就已經完成了派發的動做,也就是說這個事件到這裏已經完成了它的分發流程。
分發的邏輯比較清晰,遍歷TouchTraget
,嘗試給它分發事件。有幾個比較特殊的狀況:
TouchTarget
鏈表爲空,就直接將事件發送給本身處理。TouchTarget
發送取消事件。public boolean dispatchTouchEvent(MotionEvent ev) {
...
///消費標誌
boolean handled = false;
///onFilterTouchEventForSecurity():安全檢查
if (onFilterTouchEventForSecurity(ev)) {
...
if (mFirstTouchTarget == null) {
///沒找到派發目標就發給本身來處理
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
///給TouchTarget派發事件
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
///已經在上一個步驟派發過
handled = true;
} else {
///派發給對應的TouchTarget
///若是攔截了事件,就會給全部的TouchTarget發送CANCEL事件
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
/// 當是取消事件或者是ACTION_UP之類的事件,執行清除的操做。
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
return handled;
}
複製代碼
這裏發覺到的一個比較有意思的邏輯是,事件按理說應該只交給一個child
去處理,爲何要給全部的TouchTarget
分發?奧祕就在TouchTarget
存儲的觸摸點Id集合中,在查找分發目標的過程當中,首先的步驟就是清除了TouchTarget
鏈表中相同的觸摸點Id,而分發的函數dispatchTransformedTouchEvent()
中,又會根據Id集合是否包含當前事件的觸摸點Id去選擇是否下發,因此實際上只將事件發送到了惟一的持有當前事件觸摸點Id的TouchTarget
上面。而對於攔截了的狀況,就會給全部的TouchTarget
發送取消事件而且清除掉TouchTarget
。
在上一節中,時常會提到這個方法ViewGroup.dispatchTransformedTouchEvent()
,當肯定了分發目標的時候,就會調用這個方法進行分發,作一些預處理以後進行分發。主要的步驟:
ACTION_CANCEL
事件直接下發。TouchTarget
分發的問題。若是沒有交集,說明這個TouchTarget
不是分發目標,直接返回。TouchTarget
表示這個child只接受其中的一部分,也就是,多個手指落在了不一樣的view上面,須要拆開進行分發。private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
final boolean handled;
///取消事件直接下發
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
///觸摸點id過濾
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
///取和等與0,沒有交集,不須要下發,直接返回
if (newPointerIdBits == 0) {
return false;
}
///根據狀況,對事件進行必定的分裂
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
///當觸摸點id不相等的時候,須要對事件進行過濾
///只拿到須要下發的觸摸點id的事件進行下發
transformedEvent = event.split(newPointerIdBits);
}
///分發
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
transformedEvent.recycle();
return handled;
}
複製代碼
這個方法中,有不少的child == null
判斷,而後調用super.dispatchTouchEvent()
,而這時候ViewGroup
執行super.dispatchTouchEvent()
調用的是View.dispatchTouchEvent()
,也就是事件交給了本身來處理。
認真看代碼還會發現一個奇怪的事情,在分裂的時候,有一段和後面分發徹底一致的代碼,也就是在分裂直接進行了分發。爲何重複的代碼要寫兩遍?
分析一下走第一部分不走第二部分的條件:newPointerIdBits == oldPointerIdBits
和child.hasIdentityMatrix()==true(變換矩陣是單位矩陣,也就是沒有變化)
。
簡單分析不考慮多指操做不一樣的view,也就是newPointerIdBits == oldPointerIdBits
的狀況下, 剩下的條件就只有child.hasIdentityMatrix()
。
沒有變換矩陣走第一段代碼,有變換矩陣走第二段代碼,兩段代碼的區別也就是後面的那段代碼使用的是複製的MotionEvent
,進行了矩陣映射,而且使用結束了就直接回收了;而前面的那段代碼是直接使用的傳入的MotionEvent
,使用後進行了座標系的復原。
而這就體現出兩端代碼的區別:若是有矩陣映射的狀況下,第一段代碼會多一個反映射的復原操做,而第二段代碼多的是一個MotionEvent
建立銷燬的操做。我猜想,這可能就是在變換矩陣反映射和MotionEvent
建立銷燬之間取得的一點性能上得優化吧。
走到這也就到了一次觸摸事件分發的尾聲了,當View.dispatchTouchEvent()
執行的時候,只須要將事件分發給自身處理便可,自身又不止一個處理方法,因此也就會有個優先級來決定前後順序:
onTouchListener
處理,也就是經過外部設置的監聽。onTouchEvent()
處理。public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
///滾動條處理
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
///onTouchListener處理
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
///onTouchEvent()處理
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
複製代碼
此次嘗試將手指觸摸到屏幕上到控件獲取到此次觸摸的流程作一次完整的剖析,沒有想到工做量居然如此大,文章長度達到了5000詞。收穫頗豐,不只學到了不少源碼設計的巧妙之處,同時是源碼的閱讀能力仍是事件分發的理解,都上了一個大臺階。
入行淺,起步要穩,一步一個腳印,作大作強,再創輝煌。