一、事件傳遞的整體過程
當用戶手指觸摸手機屏幕時最先將事件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ā)就簡要分享到此,有需要補充的請大神們留言哦。