一辆布、事件傳遞的整體過程
當用戶手指觸摸手機屏幕時最先將事件MotionEvent傳遞給activity中的dispatchTouchEvent瞬矩,然后是將事件交給window去處理,window再將事件交給頂層的View锋玲,也就是DecorView處理景用,一級級地將事件向下傳遞下去。
層級關(guān)系如下:
-activity
-PhoneWindow
-DocorView
-ViewGroup
-view
在整個事件傳遞過程中比較關(guān)鍵的幾個方法:
dispatchTouchEvent
1.對事件進行分發(fā)惭蹂,如果事件能傳遞到當前 View 那么該方法一定會被調(diào)用
2.該方法的調(diào)用受當前 View#onTouchEvent 和 下級的 dispatchTouchEvent 影響
3.返回結(jié)果表示是否消費當前事件
onInterceptEvent
只有 ViewGroup 這個方法伞插,表示是否攔截當前事件,當前 View 攔截當前事件之后盾碗,那么同一事件序列的其他事件都會交給該 View 去處理媚污,并且該方法不會再調(diào)用
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類型的揽涮,所以在第二行代碼可以看出他是調(diào)用ViewGroup中的dispatchTouchEvent方法。
以上代碼片段中表達的是一個點擊事件的整體傳遞過程: Activity -> Window -> DecorView -> ViewGroup -> View
二饿肺、下面就從 ViewGroup 開始蒋困,分析 ViewGroup 是怎么進行事件分發(fā)的:
因為事件能傳遞到當前 View 的話,那么該 View 的dispatchTouchEvent 就會被調(diào)用唬格,查閱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 若是調(diào)用
requestDisallowInterceptTouchEvent(true) 的話,那么 disallowIntercept 的值就是 true 绎签,表示不要當前事件,默
認情況這個標記返回值為 false 枯饿,表示子 View 沒有請求父容器不要攔截當前事件。
現(xiàn)在看看ViewGroup#onInterceptTouchEvent方法是什么诡必,顧名思義它是一個攔截事件的方法奢方,發(fā)現(xiàn)這個方法在ViewGroup中直接返回的是false,這就說明了爸舒,ViewGroup在默認情況下是不會去攔截事件的蟋字。
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
若是當前事件是DOWN事件,那么就會去調(diào)用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);
//找到對應(yīng)接收事件的子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行調(diào)用dispatchTransformedTouchEvent將找到的child作為參數(shù)傳入骑祟, 當前child不為null,就調(diào)用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事件不會再調(diào)用onInterceptEvent方法去判斷是否需要攔截绘雁,因為判斷條件中的mFirstTouchTarget!=null條件不成立橡疼。
現(xiàn)在有個問題:現(xiàn)在代碼流程走到這里,有兩種情況庐舟,第一是事件被攔截了并且找到了合適的孩子去接收該事件欣除,并且將該事件進行消費,那么這種情況是最好的挪略,但是如果沒有找到合適的孩子去接收該事件历帚,那么該事件該怎么處理?第二種情況是事件沒有被攔截杠娱,那么當前事件該怎么處理呢挽牢?怎么處理,不能中途迷路吧墨辛,所以得找到宿主卓研。好的,接下來繼續(xù)跟進代碼睹簇。
先解決第一個問題:就是事件被攔截了奏赘,但是沒有找到合適的孩子去傳遞這個事件,剛才分析了太惠,若是孩子沒有消費調(diào)用這個事件的話那么dispatchTransformedTouchEvent(ev,false,child,idBitsToAssign)返回false磨淌,也就是mFirstTouchTarget沒有被賦值,那么這種情況ViewGroup自己會去處理這個點擊事件凿渊,接下來看一段代碼:
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
//遍歷所有的孩子之后都沒有找到可以傳遞事件的子View
//這里注意dispatchTransformedTouchEvent的第三個參數(shù)梁只,傳遞的是null,所以會去調(diào)用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)在有進入之前調(diào)用的這個方法dispatchTransformedTouchEvent不過注意觀察這次第三個參數(shù)傳遞的是null,也就child參數(shù)為null彩掐,這是因為沒有找到孩子的原因构舟。看看之前的該方法的源碼堵幽,就在上面貼出來了狗超,super.dispatchTouchEvent(transformedEvent);可以看到當child參數(shù)為null時,它調(diào)用的是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是否設(shè)置了OnTouchListener厨钻,并且判斷該Viw是否為enable的扼雏,接下來就是OnTouchListener#onTouch方法了,可以看出只有這三個條件都成立了才能返回true夯膀,也才能說是事件到此被消費掉诗充,這是條件成立的情況。若是不成立诱建,那么就會調(diào)用onTouchEvent方法蝴蜓,所以默認情況下ViewGroup若是攔截了當前的事件,就會調(diào)用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 而言,可以為其設(shè)置 clickable 為 true 標記當前 View 是可以點擊的驱敲,那么 onTouchEvent 方法就會返回 true铁蹈。否則返回false,事件沒被消費癌佩。雖然代碼比較冗長木缝,但是只要觀察返回值即可!我們都知道點擊事件的發(fā)生是具備DOWN和UP事件围辙,如果只有DOWN沒有UP事件那就不是點擊事件了我碟,所以點擊事件的觸發(fā)就在UP事件發(fā)生,可以看看第27行代碼姚建,調(diào)用performClick方法去執(zhí)行點擊事件矫俺,55~65行為代碼實現(xiàn)。首先判斷是是否通過setOnClickListener事件,如果設(shè)置了厘托,那么就調(diào)用onclick方法友雳。
還有一點,上面代碼展示的若是當前 View 設(shè)置了 OnTouchListener 并且該 View 是 enable 的铅匹,那么事件就會傳遞給 OnTouchListener#onTouch 方法押赊,如果該方法返回 true 那么就不會再去調(diào)用 該 View 的 onTouchEvent 方法了,若是返回 false 那么該 View 的 onTouchEvent 方法就會被調(diào)用包斑,因此可以知道只要滿足條件那么 onTouch() 方法會被 onTouchEvent 方法先執(zhí)行流礁。
在閱讀《Android開發(fā)藝術(shù)探討》一書后的一些結(jié)論:
同一個事件序列是從手指觸摸屏幕的那一刻開始到手指離開屏幕的那一刻起結(jié)束,在這個過程所產(chǎn)生的一系列事件就屬于同一個事件系列罗丰。
一旦一個 View 攔截了某一個事件之后神帅,那么在接下來的同一事件序列中的其他事件都會交給該 View 去處理(前提是事件能傳遞到該 View),并且它的 onInterceptTouchEvent 方法將不會被調(diào)用萌抵。
onInterceptTouchEvent 方法若是返回 true 表示需要當前事件是被攔截的找御,因此 mFirstTouchTarget 就沒有被賦值,因此在同一事件序列的其他事件到來時绍填,就不會再去調(diào)用?onInterceptTouchEvent 方法霎桅。
某一個 View 一旦不消耗 ACTION_DOWN 事件,也就是 onTouchEvent 方法返回 false 讨永,那么在同一事件序列的其他事件也不會傳遞給該 View 去處理了哆档。因為該 View 的 onTouchEvent 方法返回 false 也就意味著 dispatchTouchEvent 方法返回 false ,表示當前事件并沒有被該 View 成功處理住闯,那么在其他事件傳遞到父 View 的時就會判斷父 View 的 mFirstTouchTarget 為 null瓜浸,就不會去將該事件分發(fā)到子 view 中去。
如果一個事件沒有被處理比原,那么最終會回調(diào)到 Activity#onTouchEvent 方法中去處理插佛。
View 是沒有 onInterceptTouchEvent 方法的,因此一點有事件傳遞到該 View 的dispathTouchEvent 方法中那么它的 onTouchEvent 方法就會被調(diào)用量窘。
View 的 clickable 或 longClickable 屬性若是為 true 那么 onTouchEvent 方法默認就是返回true 也就是事件默認就會被處理調(diào)雇寇。
View 的 enable 屬性不會影響 onTouchEvent 方法的執(zhí)行,但是會影響 OnTouchEvent#onTouch 方法的執(zhí)行蚌铜。
事件的傳遞方向是有外往內(nèi)傳遞的锨侯,即事件先傳遞給父 View 然后再傳遞給子 View ,可以通過 requestDisallowInterceptTouchEvent(boolean) 方法請求父容器不要攔截當前事件冬殃。
至此事件分發(fā)就簡要分享到此囚痴,有需要補充的請大神們留言哦。