android_事件分發(fā)源碼解析

一、事件傳遞的整體過程

當用戶手指觸摸手機屏幕時最先將事件MotionEvent傳遞給activity中的dispatchTouchEvent,然后是將事件交給window去處理,window再將事件交給頂層的View,也就是DecorView處理,一級級地將事件向下傳遞下去。

層級關系如下:

-activity

-PhoneWindow

-DocorView

-ViewGroup

-view

在整個事件傳遞過程中比較關鍵的幾個方法:

dispatchTouchEvent

1.對事件進行分發(fā),如果事件能傳遞到當前 View 那么該方法一定會被調用

2.該方法的調用受當前 View#onTouchEvent 和 下級的 dispatchTouchEvent 影響

3.返回結果表示是否消費當前事件

onInterceptEvent

只有 ViewGroup 這個方法,表示是否攔截當前事件,當前 View 攔截當前事件之后,那么同一事件序列的其他事件都會交給該 View 去處理,并且該方法不會再調用

onTouchEvent

表示是否消費當前事件

首先看看Activity如何去處理事件的,Activity#dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {

if (ev.getAction() == MotionEvent.ACTION_DOWN) {

onUserInteraction();

}

if (getWindow().superDispatchTouchEvent(ev)) {

return true;

}

return onTouchEvent(ev);

}

第五行代碼中通過getWindow()將事件交給Window去處理,在Activity源碼getWindow返回一個Window對象,該對象就是Window的子類PhoneWindow對象。

@Override

public boolean superDispatchTouchEvent(MotionEvent event) {

return mDecor.superDispatchTouchEvent(event);

}

在Window#superDispatchTouchEvent方法中將事件交給了mDecor去處理,mDecor是什么?

// This is the top-level view of the window, containing the window decor.

private DecorView mDecor;

看到這里就知道事件就從Window交給DecorView去處理了,從注釋可以看出,該View是top-level的view也就是最頂層的View了。我們一般在Activity中setContentView中的View就是其子View。

接下來看DecorView中的superDispatchTouchEvent是怎么處理這個事件的?

public boolean superDispatchTouchEvent(MotionEvent event) {

return super.dispatchTouchEvent(event);

}

由于DecorView是繼承至FrameLayout的,是ViewGroup類型的,所以在第二行代碼可以看出他是調用ViewGroup中的dispatchTouchEvent方法。

以上代碼片段中表達的是一個點擊事件的整體傳遞過程: Activity -> Window -> DecorView -> ViewGroup -> View

二、下面就從 ViewGroup 開始,分析 ViewGroup 是怎么進行事件分發(fā)的:

因為事件能傳遞到當前 View 的話,那么該 View 的dispatchTouchEvent 就會被調用,查閱ViewGroup的dispatchTouchEvent源碼,看看是怎么實現(xiàn)的?

if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {

//addTouchTarget中初始化了mFirstTouchTarget對象,前提就是事件ACTION_DOWN沒有被攔截,并且有子View成功處理了.

//同一個事件序列中,當前事件的上一個事件已經(jīng)被處理了

final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

if (!disallowIntercept) {

intercepted = onInterceptTouchEvent(ev);

ev.setAction(action); // restore action in case it was changed

} else {

intercepted = false;

}

} else {

// There are no touch targets and this action is not an initial down

// so this view group continues to intercept touches.

//mFirstTouchTarget== null就默認攔截除action_down之外的所有事件

intercepted = true;

}

在第一行代碼中判斷當前事件是否為ACTION_DOWN事件或者mFirstTouchTarget!=null其中一個條件成立就會將事件傳遞給onInterceptTouchEvent。但是這里說法不是很明確,因為當前事件能不能攔截,還需要判斷mGroupFlags 標記,它是子 View 若是調用

requestDisallowInterceptTouchEvent(true) 的話,那么 disallowIntercept 的值就是 true ,表示不要當前事件,默

認情況這個標記返回值為 false ,表示子 View 沒有請求父容器不要攔截當前事件。

現(xiàn)在看看ViewGroup#onInterceptTouchEvent方法是什么,顧名思義它是一個攔截事件的方法,發(fā)現(xiàn)這個方法在ViewGroup中直接返回的是false,這就說明了,ViewGroup在默認情況下是不會去攔截事件的。

public boolean onInterceptTouchEvent(MotionEvent ev) {

return false;

}

若是當前事件是DOWN事件,那么就會去調用onInterceptEvent方法,若是當前ViewGroup沒有重寫該方法,那么默認就返回false,也就是intercept=false;表示不攔截!代碼往下走:

if (!canceled && !intercepted) {

//ViewGroup不攔截事件的情況

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

final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)

: TouchTarget.ALL_POINTER_IDS;

// Clean up earlier touch targets for this pointer id in case they

// have become out of sync.

removePointersFromTouchTargets(idBitsToAssign);

final int childrenCount = mChildrenCount;

if (childrenCount != 0) {

// Find a child that can receive the event.

// Scan children from front to back.

final View[] children = mChildren;

final float x = ev.getX(actionIndex);

final float y = ev.getY(actionIndex);

//找到對應接收事件的子View

for (int i = childrenCount - 1; i >= 0; i--) {

final View child = children[i];

if (!canViewReceivePointerEvents(child)//判斷該view是否可見

|| !isTransformedTouchPointInView(x, y, child, null)) {//判斷坐標是否在該view上

continue;

}

newTouchTarget = getTouchTarget(child);

if (newTouchTarget != null) {

// Child is already receiving touch within its bounds.

// Give it the new pointer in addition to the ones it is handling.

newTouchTarget.pointerIdBits |= idBitsToAssign;

break;

}

resetCancelNextUpFlag(child);

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

//子View已經(jīng)處理了事件,則給mFirstTouchTarget賦值

// Child wants to receive touch within its bounds.

mLastTouchDownTime = ev.getDownTime();

mLastTouchDownIndex = i;

mLastTouchDownX = ev.getX();

mLastTouchDownY = ev.getY();

//為mFirstTouchTarget賦值,并為其指向child對象

//mFirstTouchTarget是否被賦值,直接影響到ViewGroup對事件的攔截策略

newTouchTarget = addTouchTarget(child, idBitsToAssign);

alreadyDispatchedToNewTouchTarget = true;

break;//跳出循環(huán)

}

}

}

if (newTouchTarget == null && mFirstTouchTarget != null) {

// Did not find a child to receive the event.

// Assign the pointer to the least recently added target.

newTouchTarget = mFirstTouchTarget;

while (newTouchTarget.next != null) {

newTouchTarget = newTouchTarget.next;

}

newTouchTarget.pointerIdBits |= idBitsToAssign;

}

}

}

dispatchTransformedTouchEvent方法部分代碼:

// Perform any necessary transformations and dispatch.

if (child == null) {

handled = super.dispatchTouchEvent(transformedEvent);

} else {

...

handled = child.dispatchTouchEvent(transformedEvent);

}

addTouchTarget方法部分代碼

/**

* Adds a touch target for specified child to the beginning of the list.

* Assumes the target child is not already present.

*/

//給指定的View添加一個TouchTarget,并返回TouchTarget

//并且更新mFirstTouchTarget為target.

private TouchTarget addTouchTarget(View child, int pointerIdBits) {

TouchTarget target = TouchTarget.obtain(child, pointerIdBits);

target.next = mFirstTouchTarget;

mFirstTouchTarget = target;

return target;

}

intercepted為false,那么就會進入第一行的if語句,既然ViewGroup不攔截這個事件,那么就得找一個孩子去接收這個事件啊,所以在代碼15~52行中就是找孩子的過程。24~27行遍歷所有的孩子,判斷孩子是否為可見的,是否正在做動畫,然后判斷當前的觸摸坐標是否在當前的孩子上面,若都符合條件,這個child就是可以向下傳遞事件的孩子,然后在38行調用dispatchTransformedTouchEvent將找到的child作為參數(shù)傳入, 當前child不為null,就調用child.dispatchTouchEvent方法,將事件傳遞給孩子的dispatchTouchEvent方法。到此事件就從父容器傳遞給了子容器了,完成了一輪事件的傳遞。想太多了,還沒有完呢,先看看child.dispatchTouchEvent方法返回值是什么,接下來分為兩種情況分析:

若是子容器dispatchTouchEvent返回true,表示事件已經(jīng)被成功的消費了,也就是第38行返回true,那么接下來代碼走到47行,為當前的child添加一個target,進入addToTarget方法瞧瞧,它為mFirstTouchTarget進行賦值,到了這里為止,回想之前進入onInterceptEvent方法的那個if條件,要么需要是DOWN事件,要么是mFirstTouchTarget!=null,至此mFirstTouchTarget!=null成立了,那么接下來的MOVE,UP事件都會進入if條件,也就是onInterceptEvent方法去詢問ViewGroup是否要攔截事件。還有一步?jīng)]走完,那就是若是子容器的dispatchTouchEvent返回false的情況。

若是子容器的dispatchTouchEvent返回false,這就說明事件沒有被消費,也就是第38行代碼返回的是false,這種情況有可能是ViewGroup沒有孩子,或者說孩子的onTouchEvent方法返回了false,在這里可以看到mFirstTouchTartget就沒有被賦值了,換句話說,接下的MOVE和UP事件不會再調用onInterceptEvent方法去判斷是否需要攔截,因為判斷條件中的mFirstTouchTarget!=null條件不成立。

現(xiàn)在有個問題:現(xiàn)在代碼流程走到這里,有兩種情況,第一是事件被攔截了并且找到了合適的孩子去接收該事件,并且將該事件進行消費,那么這種情況是最好的,但是如果沒有找到合適的孩子去接收該事件,那么該事件該怎么處理?第二種情況是事件沒有被攔截,那么當前事件該怎么處理呢?怎么處理,不能中途迷路吧,所以得找到宿主。好的,接下來繼續(xù)跟進代碼。

先解決第一個問題:就是事件被攔截了,但是沒有找到合適的孩子去傳遞這個事件,剛才分析了,若是孩子沒有消費調用這個事件的話那么dispatchTransformedTouchEvent(ev,false,child,idBitsToAssign)返回false,也就是mFirstTouchTarget沒有被賦值,那么這種情況ViewGroup自己會去處理這個點擊事件,接下來看一段代碼:

// Dispatch to touch targets.

if (mFirstTouchTarget == null) {

//遍歷所有的孩子之后都沒有找到可以傳遞事件的子View

//這里注意dispatchTransformedTouchEvent的第三個參數(shù),傳遞的是null,所以會去調用super.dispatchTouchEvent方法

//到了View.dispatchTouchEvent方法處理了。

// No touch targets so treat this as an ordinary view.

handled = dispatchTransformedTouchEvent(ev, canceled, null,

TouchTarget.ALL_POINTER_IDS);

}

看到?jīng)]有,這里對mFirstTouchTarget做了判空處理,現(xiàn)在有進入之前調用的這個方法dispatchTransformedTouchEvent不過注意觀察這次第三個參數(shù)傳遞的是null,也就child參數(shù)為null,這是因為沒有找到孩子的原因。看看之前的該方法的源碼,就在上面貼出來了,super.dispatchTouchEvent(transformedEvent);可以看到當child參數(shù)為null時,它調用的是super.dispatchTouchEvent方法,將ev事件向傳遞View,先處理第二個問題,再來看看View層是怎么處理這個事件。

解決第二個問題:這個問題跟第一問題差不多,也就是mFirstTouchTarget沒有被賦值,接下來的邏輯代碼跟上面的一樣。

三、看完 ViewGroup 事件分發(fā)的過程,接下來分析 View 的是怎么處理事件的:

public boolean dispatchTouchEvent(MotionEvent event) {

if (mInputEventConsistencyVerifier != null) {

mInputEventConsistencyVerifier.onTouchEvent(event, 0);

}

if (onFilterTouchEventForSecurity(event)) {

//noinspection SimplifiableIfStatement

ListenerInfo li = mListenerInfo;

if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED

&& li.mOnTouchListener.onTouch(this, event)) {

//就算是onTouch方法返回false,只要是控件是clickable的,那么

//dispatchTouchEvent方法一定會被返回true

return true;

}

if (onTouchEvent(event)) {

return true;

}

}

if (mInputEventConsistencyVerifier != null) {

mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);

}

return false;

}

在dispatchTouchEvent方法中,第9行首先判斷當前View是否設置了OnTouchListener,并且判斷該Viw是否為enable的,接下來就是OnTouchListener#onTouch方法了,可以看出只有這三個條件都成立了才能返回true,也才能說是事件到此被消費掉,這是條件成立的情況。若是不成立,那么就會調用onTouchEvent方法,所以默認情況下ViewGroup若是攔截了當前的事件,就會調用onTouchEvent方法就體現(xiàn)在這里了。接下來進入onTouchEvent方法看看源碼:

public boolean onTouchEvent(MotionEvent event) {

final int viewFlags = mViewFlags;

...

if (((viewFlags & CLICKABLE) == CLICKABLE ||

(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {

switch (event.getAction()) {

case MotionEvent.ACTION_UP:

boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;

if ((mPrivateFlags & PRESSED) != 0 || prepressed) {

...

if (!mHasPerformedLongPress) {

// This is a tap, so remove the longpress check

removeLongPressCallback();

// Only perform take click actions if we were in the pressed state

if (!focusTaken) {

// Use a Runnable and post this rather than calling

// performClick directly. This lets other visual state

// of the view update before click actions start.

if (mPerformClick == null) {

mPerformClick = new PerformClick();

}

if (!post(mPerformClick)) {

performClick();

}

}

}

...

}

break;

case MotionEvent.ACTION_DOWN:

...

break;

case MotionEvent.ACTION_CANCEL:

...

break;

case MotionEvent.ACTION_MOVE:

...

break;

}

return true;

}

return false;

}

public boolean performClick() {

sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

ListenerInfo li = mListenerInfo;

if (li != null && li.mOnClickListener != null) {

playSoundEffect(SoundEffectConstants.CLICK);

li.mOnClickListener.onClick(this);

return true;

}

return false;

在第6,7行中判斷該View是否為可點擊的,看方法返回值可以知道,只要是可點擊的,一律都會返回true,也就是事件會被消費掉,也就是說若是當前 View 是 TextView 等不可點擊的 View ,那么 onTouchEvent 方法都會返回 false,像 Button 這些可點擊的 View ,它們的 onTouchEvent 方法默認返回 true,不過對于 TextView 等不可點擊的 View 而言,可以為其設置 clickable 為 true 標記當前 View 是可以點擊的,那么 onTouchEvent 方法就會返回 true。否則返回false,事件沒被消費。雖然代碼比較冗長,但是只要觀察返回值即可!我們都知道點擊事件的發(fā)生是具備DOWN和UP事件,如果只有DOWN沒有UP事件那就不是點擊事件了,所以點擊事件的觸發(fā)就在UP事件發(fā)生,可以看看第27行代碼,調用performClick方法去執(zhí)行點擊事件,55~65行為代碼實現(xiàn)。首先判斷是是否通過setOnClickListener事件,如果設置了,那么就調用onclick方法。

還有一點,上面代碼展示的若是當前 View 設置了 OnTouchListener 并且該 View 是 enable 的,那么事件就會傳遞給 OnTouchListener#onTouch 方法,如果該方法返回 true 那么就不會再去調用 該 View 的 onTouchEvent 方法了,若是返回 false 那么該 View 的 onTouchEvent 方法就會被調用,因此可以知道只要滿足條件那么 onTouch() 方法會被 onTouchEvent 方法先執(zhí)行。

在閱讀《Android開發(fā)藝術探討》一書后的一些結論:

同一個事件序列是從手指觸摸屏幕的那一刻開始到手指離開屏幕的那一刻起結束,在這個過程所產(chǎn)生的一系列事件就屬于同一個事件系列。

一旦一個 View 攔截了某一個事件之后,那么在接下來的同一事件序列中的其他事件都會交給該 View 去處理(前提是事件能傳遞到該 View),并且它的 onInterceptTouchEvent 方法將不會被調用。

onInterceptTouchEvent 方法若是返回 true 表示需要當前事件是被攔截的,因此 mFirstTouchTarget 就沒有被賦值,因此在同一事件序列的其他事件到來時,就不會再去調用?onInterceptTouchEvent 方法。

某一個 View 一旦不消耗 ACTION_DOWN 事件,也就是 onTouchEvent 方法返回 false ,那么在同一事件序列的其他事件也不會傳遞給該 View 去處理了。因為該 View 的 onTouchEvent 方法返回 false 也就意味著 dispatchTouchEvent 方法返回 false ,表示當前事件并沒有被該 View 成功處理,那么在其他事件傳遞到父 View 的時就會判斷父 View 的 mFirstTouchTarget 為 null,就不會去將該事件分發(fā)到子 view 中去。

如果一個事件沒有被處理,那么最終會回調到 Activity#onTouchEvent 方法中去處理。

View 是沒有 onInterceptTouchEvent 方法的,因此一點有事件傳遞到該 View 的dispathTouchEvent 方法中那么它的 onTouchEvent 方法就會被調用。

View 的 clickable 或 longClickable 屬性若是為 true 那么 onTouchEvent 方法默認就是返回true 也就是事件默認就會被處理調。

View 的 enable 屬性不會影響 onTouchEvent 方法的執(zhí)行,但是會影響 OnTouchEvent#onTouch 方法的執(zhí)行。

事件的傳遞方向是有外往內傳遞的,即事件先傳遞給父 View 然后再傳遞給子 View ,可以通過 requestDisallowInterceptTouchEvent(boolean) 方法請求父容器不要攔截當前事件。

至此事件分發(fā)就簡要分享到此,有需要補充的請大神們留言哦。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,739評論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,634評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,653評論 0 377
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,063評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,835評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,235評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,315評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,459評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,000評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,819評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,004評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,560評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,257評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,676評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,937評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,717評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,003評論 2 374

推薦閱讀更多精彩內容