前言
前段時間找工作,看了好多關于事件分發機制的書,各路大牛從不同的角度進行了分析。本人受益匪淺,于是有了這篇吸取天地之精華的解析。
本文章會從什么是事件分發機制開始,一直深入到源碼分析。
主要目的是讓自己理解更深入,也希望能讓讀者更容易讀懂而不覺干澀。
概念
本節都是基礎,我化身十萬個為什么提出以下幾個問題!如果讀者都明了那就直接跳向下一節!
事件分發機制是什么?
事件分發機制就是點擊事件的分發。那么點擊事件又是什么?
在手指接觸屏幕后產生的同一個事件序列都是點擊事件。-
點擊事件分為哪幾種類型?
- 手指剛接觸屏幕
- 手指在屏幕上滑動
- 手指從屏幕上松開的一瞬間
同一個事件序列是什么?
是從手指接觸屏幕的一瞬間起,直到手指從屏幕上松開的一瞬間所產生的一切事件。點擊事件用代碼如何表示?
在源碼中MotionEvent就是點擊事件,對點擊事件的分發就是對MotionEvent對象的分發傳遞過程。-
MotionEvent的點擊事件類型?
- ACTION_DOWN:手指剛接觸屏幕
- ACTION_MOVE:手指在屏幕上滑動
- ACTION_UP:手指從屏幕上松開的一瞬間
那這個MotionEvent到底是如何傳遞的?
那就來看下一節!
事件分發機制
所謂事件分發機制,其實就是對MotionEvent(點擊事件)的分發過程。
當一個MotionEvent(點擊事件)產生之后,系統需要把它傳遞給一個具體的View,這個傳遞過程就是事件分發機制。
1. 我們來簡單描述一次點擊事件(不涉及方法調用,先有個大概的體系)
- 用戶接觸屏幕產生MotionEvent(點擊事件)
- MotionEvent(點擊事件)總是由Activity先接收
- Activity接收后將MotionEvent(點擊事件)進行傳遞:Activity->Window->DecorView(DecorView是當前界面的底層容器,就是setContentView所設置View的父容器)
- DecorView是一個ViewGroup,將MotionEvent(點擊事件)分發向各個子View
2. 三個方法
相信大家對點擊事件已經有所了解,那接下來我們介紹事件分發機制很重要的三個方法,點擊事件的分發機制都是根據這三個方法共同完成的:dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent()。
-
dispatchTouchEvent():用來進行事件的分發,如果MotionEvent(點擊事件)能夠傳遞給該View,那么該方法一定會被調用。返回值由 本身的onTouchEvent() 和 子View的dispatchTouchEvent()的返回值 共同決定。
- 返回值為true,則表示該點擊事件被本身或者子View消耗。
- 返回值為false,則表示該ViewGroup沒有子元素,或者子元素沒有消耗該事件。
onInterceptTouchEvent():在dispatchTouchEvent()中調用,用來判斷是否攔截某個事件,如果當前View攔截了某個事件,那么在同一個事件序列中不會再訪問該方法。
onTouchEvent():在dispatchTouchEvent()中調用,返回結果表示是否消耗當前事件,如果不消耗(返回false),則在同一個事件序列中View不會再次接收到事件。
3. 三個方法的關系
這么多概念,別頭疼!咱們用偽代碼看一下三個方法的關系!
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onInterceptTouchEvent(ev)) {
handled = onTouchEvent(ev);
} else {
handled = child.dispatchTouchEvent(ev)
}
return handled;
}
這段偽代碼可以很好地理解事件的傳遞機制:
用戶點擊屏幕產生MotionEvent(點擊事件),View的dispatchTouchEvent()接收MotionEvent(點擊事件)后,先執行該View的onInterceptTouchEvent()判斷是否攔截該事件,若攔截執行該View的onTouchEvent()方法,若不攔截則調用子View的dispatchTouchEvent()。在事件傳遞的源碼中,使用的就是類似的邏輯。
4. 事件傳遞順序
- 用戶點擊屏幕產生MotionEvent(點擊事件)
- Activity接收MotionEvent(點擊事件)—>傳遞給Window—>傳遞給DecorView(ViewGroup)—>執行ViewGroup的dispatchTouchEvent()
- ViewGroup接收到MotionEvent(點擊事件)之后,按照事件分發機制去分發事件。
- 若當子View不消耗事件,onTouchEvent()返回false,那么這個事件會傳遞回其父View的onTouchEvent(),如若父View也不消耗,最后會傳遞回給Activity進行處理。
總的來說點擊事件的傳遞順序是由父到子,再由子到父的。
圖解事件傳遞機制
現在網上的大部分文章都是通過源碼和log講解事件的傳遞,對看文章的人來說體驗并沒有那么好,看的云里霧里摸不出個頭。在這獻上一本葵花寶典!看了這張圖媽媽再也不用擔心我的學習啦!
友情提示:
- 還是不理解的同學可以對照上一部分一起看效果更佳。
- 圖中View的onTouchEvent返回false,將事件傳遞給ViewGroup的過程,并不是直接傳遞。是上級ViewGroup的dispatchTouchEvent()方法接收到子View的onTouchEvent()返回的false,再將事件分發給自己(ViewGroup)的onTouchEvent。
- ViewGroup里面沒有復寫onTouchEvent,然而ViewGroup本身就是View,View中有onToucheEvent。
源碼解析
看了這么久咱們終于來看源碼啦!不多廢話!一庫!
1. Activity對點擊事件的分發
先來看Activity的dispatchTouEvent,所有點擊事件接收的源頭
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
這段代碼中我們著重看getWindow().superDispatchTouchEvent(ev),方法將點擊事件傳遞給了Window。返回值表示是否消耗掉了該點擊事件。如果所有的View都沒有消耗掉點擊事件,則Activity調用自己的onTouchEvent。
再來看Window的源碼:
public abstract boolean superDispatchTouchEvent(MotionEvent event);
發現其實是一個接口,那實現方法在哪?不急,不難找,源碼的最上方注釋里寫道
The only existing implementation of this abstract class is android.view.PhoneWindow,
該接口的唯一實現方法是PhoneWindow,那咱們再去看PhoneWindow的源碼:
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
是不是很熟!其實他也把這個鍋直接甩給了DecorView ,之前介紹過,DecorView是當前界面的底層容器,就是setContentView所設置View的父容器。所以再來看DecorView:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
碼個蛋!竟然又傳遞出去了,這次是調用了super,而DecorView是繼承自ViewGroup,所以調用了ViewGroup的dispatchTouchEvent!那這樣咱們就先來瞧一瞧ViewGroup里的源碼!
2.ViewGroup對事件的分發
先來看ViewGroup中的dispatchTouchEvent中的一小段
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
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.
intercepted = true;
}
咱們從頭開始看,MotionEvent.ACTION_DOWN 這個之前介紹過,那mFirstTouchTarget 是什么?后面的代碼表示,當ViewGroup的點擊事件被子View消耗,那mFirstTouchTarget就會指向該子View。所以如果事件被子View消耗 或者 是ACTION_DOWN事件,那就訪問該ViewGroup的onInterceptTouchEvent,如果不那就全部被當前ViewGroup攔截。換句話說,如果View決定攔截事件,那么這一個事件序列都會由這個View來處理。
那么大家也注意到FLAG_DISALLOW_INTERCEPT這個標志位,看起來它可以影響ViewGroup是否攔截該事件。這個標志位是通過requestDisallowInterceptTouchEvent()方法來設置的,一般用于子View中。當標志位設置之后ViewGroup將無法攔截除了ACTION_DOWN以外的事件了。為啥說除了ACTION_DOWN以外呢?因為dispatchTouchEvent每次接收到ACTION_DOWN都會初始化狀態,代碼如下。
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
綜上所述,requestDisallowInterceptTouchEvent()方法不能影響ACTION_DOWN事件。
總結一點,onInterceptTouchEvent()方法不一定會每次都執行,如果想對每個事件都進行處理,那還是在dispatchTouchEvent()里面處理吧。
咱們繼續往下走,當該ViewGroup不攔截點擊事件的時候,事件會傳遞給他的子View:
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
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;
}
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
以上這段是ViewGroup進行事件分發的主要代碼,看起來比較簡單。當ViewGroup有子View的時候,進行子View的遍歷,其中有一個判斷條件:
canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)
判斷當前點擊事件是否在子View的坐標范圍內,且子View沒有在坐標系中移動(執行動畫),如果子View符合以上兩個情況那么就把點擊事件傳遞給他處理。往下走,會看到這么一個判斷條件:
dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
這個方法其實就是用來將事件分發給子View的,來看一下這個方法的其中一段源碼你就會清晰很多:
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;
}
如果子View為null那就交給該ViewGroup的dispatchTouchEvent(),反之就將點擊事件交給該子View(也有可能是ViewGroup)處理,一次分發就完成了。
再跳回到之前那段超長代碼,如果dispatchTransformedTouchEvent()返回true,表明點擊事件被子View消耗,執行addTouchTarget()方法給最開始的mFirstTouchTarget賦值。
如果遍歷完了所有的子View,點擊事件都沒有被消耗掉,可能有兩種情況:一、ViewGroup下面沒有子View。二、子View沒有消耗點擊事件。這兩種情況下,ViewGroup會自己處理點擊事件。當子View不消耗點擊事件,那點擊事件將交由給他的父View去處理。
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
代碼里面child參數賦值為null,當child為null時,訪問當前ViewGroup的super.dispatchTouchEvent(event),因為ViewGroup是繼承自View,所以其實訪問的就是View的dispatchTouchEvent()方法。
3.View對事件的分發
再來看看View的dispatchTouchEvent()方法的其中一段代碼,注意知識其中一段,篇幅不能太長,想要全部查看一定打開Studio看看源碼!
public boolean dispatchTouchEvent(MotionEvent event) {
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
View的dispatchTouchEvent()就比較簡單了,onFilterTouchEventForSecurity(event)是用來判斷點擊事件來到時,窗口有沒有被遮擋住,如果被遮擋住則直接返回false,不消耗事件。
反之,接收到事件后看到一個類ListenerInfo,那這是個啥?看源碼啊!
static class ListenerInfo {
public OnClickListener mOnClickListener;
protected OnLongClickListener mOnLongClickListener;
private OnKeyListener mOnKeyListener;
private OnTouchListener mOnTouchListener;
......
}
看完源碼發現它是一個View的靜態內部類,定義了一系列的Listener。
繼續看View的dispatchTouchEvent()的源碼發現,View會先判斷自己是否有設置OnTouchListener,如果所設置的OnTouchListener得onTouch返回true,則直接消耗點擊事件,不再執行onTouchEvent()方法。
得出一個結論,OnTouchListener的優先級高于onTouchEvent()。這樣做的好處是方便在外部處理事件。
如果沒有設置OnTouchListener那就會執行到View的onTouchEvent(),繼續看下onTouchEvent()的源碼,咱們一段一段來,有點長:
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
當我們把View設置為不可用狀態,View依然會消耗點擊事件,只是看起來不可用。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
之后如果View設置有代理,那么就會直接執行代理的onTouchEvent()。下面再來看一下點擊事件的主要代碼:
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
......
break;
case MotionEvent.ACTION_CANCEL:
......
break;
case MotionEvent.ACTION_MOVE:
......
break;
}
return true;
}
當View的CLICKABLE、LONG_CLICKABLE和CONTEXT_CLICKABLE有其中一個為true那么View就會消耗掉這個事件。并且在ACTION_UP的時候會執行performClick()方法:
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
如果View設置了OnClickListener,performClick()這個方法就會去執行這個監聽事件。
再來一個結論,OnTouchListener的優先級高于OnClickListener,OnClickListener是在ACTION_UP的時候執行的。
看到這里事件傳遞機制的源碼分析終于結束了!!!
結論
- 事件分發機制就是點擊事件的分發,在手指接觸屏幕后產生的同一個事件序列都是點擊事件。
- 點擊事件的傳遞順序是由父到子,再由子到父的。
- 正常情況下事件只能被一個View攔截。
- 如果View決定攔截事件,那么這一個事件序列都會由這個View來處理。
- 當子View不消耗點擊事件,那點擊事件將交由給他的父View去處理,如果所有的View都沒有消耗掉點擊事件,則Activity調用自己的onTouchEvent。
- onInterceptTouchEvent()方法不一定會每次都執行,如果想對每個事件都進行處理,那還是在dispatchTouchEvent()里面處理吧。
- OnTouchListener的優先級高于onTouchEvent()。這樣做的好處是方便在外部處理事件。
- 當我們把View設置為不可用狀態,View依然會消耗點擊事件,只是看起來不可用。
最后給大家推薦一篇View源碼分析的文章,里面有Log日志分析。大家可以看一看增深理解。《Android View 事件分發機制源碼詳解(View篇)》
最最后再給大家推薦一本書《Android開發藝術探索》,各大網站都有賣,對于突破瓶頸有很大的意義。
結后談
博主花了一段時間終于理順完了這篇文章,當然由于博主的技術原因,文章并不是十全十美的,只希望給還處在迷茫期的朋友們指引一條方向。
希望我的文章能給大家帶來一點點的福利,那在下就足夠開心了。
下次再見!