Android View 的事件分發原理解析
作為一名 Android 開發者,每天接觸最多的就是 View 了。Android View 雖然不是四大組件,但其並不比四大組件的地位低。而 View 的核心知識點事件分發機制則是不少剛入門同學的攔路虎,也是面試過程中基本上都會問的。理解 View 的事件能夠讓你寫出更好自定義 View 以及解決滑動沖突。
1、 View 事件認識
1.1 MotionEvent 事件
當你用手指輕觸屏幕,這個過程在 Android 中主要可以分為以下三個過程:
-
ACTION_DOWN:手指剛接觸屏幕,按下去的那一瞬間產生該事件
-
ACTION_MOVE:手指在屏幕上移動時候產生該事件
-
ACTION_UP:手指從屏幕上松開的瞬間產生該事件
從 ACTION_DOWN 開始到 ACTION_UP 結束我們稱為一個事件序列
正常情況下,無論你手指在屏幕上有多麽騷的操作,最終呈現在 MotionEvent 上來講無外乎下面兩種動作。
-
點擊(點擊後擡起,也就是單擊操作):ACTION_DOWN -> ACTION_UP
-
滑動(點擊後再滑動一段距離,再擡起):ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP
1.2 理論知識
- public boolean dispatchTouchEvent(MotionEvent ev)
return true: 表示消耗了當前事件,有可能是當前 View 的 onTouchEvent
或者是子 View 的 dispatchTouchEvent
消費了,事件終止,不再傳遞。
return false: 調用父 ViewGroup 或 Activity 的 onTouchEvent
。 (不再往下傳)。
return super.dispatherTouchEvent: 則繼續往下(子 View )傳遞,或者是調用當前 View 的 onTouchEvent 方法;
總結:用來分發事件,即事件序列的大門,如果事件傳遞到當前 View 的 onTouchEvent
dispatchTouchEvent
,即該方法被調用了。 另外如果不消耗 ACTION_DOWN 事件,那麽 down, move, up 事件都與該 View 無關,交由父類處理(父類的 onTouchEvent
方法)
- public boolean onInterceptTouchEvent(MotionEvent ev)
return true: ViewGroup 將該事件攔截,交給自己的onTouchEvent
處理。
return false: 繼續傳遞給子元素的dispatchTouchEvent
處理。
return super.dispatherTouchEvent: 事件默認不會被攔截。
總結:在 dispatchTouchEvent
內部調用,顧名思義就是判斷是否攔截某個事件。(註:ViewGroup 才有的方法,View 因為沒有子View了,所以不需要也沒有該方法) 。而且這一個事件序列(當前和其它事件)都只能由該 ViewGroup 處理,並且不會再調用該 onInterceptTouchEvent
方法去詢問是否攔截。
- public boolean onTouchEvent(MotionEvent ev)
return true: 事件消費,當前事件終止。
return false: 交給父 View 的 onTouchEvent
。
return super.dispatherTouchEvent: 默認處理事件的邏輯和返回 false 時相同。
總結:在dispatchTouchEvent
內部調用
上面三個方法之間的調用關系可以用下面的代碼表示:
public boolean dispatchTouchEvent(MotionEvent ev) { boolean consume = false;//事件是否被消費 if (onInterceptTouchEvent(ev)){//調用 onInterceptTouchEvent 判斷是否攔截事件 consume = onTouchEvent(ev);//如果攔截則調用自身的onTouchEvent方法 }else{ consume = child.dispatchTouchEvent(ev);//不攔截調用子View的dispatchTouchEvent方法 } return consume;//返回值表示事件是否被消費,true事件終止,false調用父View的onTouchEvent方法 }
1.3 事件傳遞順序
對於一個點擊事件,Activity 會先收到事件的通知,接著再將其傳給 DecorView(根 view),通過 DecorView 在將事件逐級進行傳遞。具體傳遞邏輯見下圖:
可以看出事件的傳遞過程都是從父 View 到子 View。但是這裏有三點需要特別強調一下
-
子 View 可以通過 requestDisallowInterceptTouchEvent 方法幹預父 View 的事件分發過程( ACTION_DOWN 事件除外),而這就是我們處理滑動沖突常用的關鍵方法。
-
對於 View(註意!ViewGroup 也是 View)而言,如果設置了onTouchListener,那麽 OnTouchListener 方法中的 onTouch 方法會被回調。onTouch 方法返回 true,則 onTouchEvent 方法不會被調用(onClick 事件是在 onTouchEvent 中調用)所以三者優先級是 onTouch->onTouchEvent->onClick
-
View 的 onTouchEvent 方法默認都會消費掉事件(返回 true),除非它是不可點擊的(clickable 和 longClickable 同時為 false),View 的longClickable 默認為 false,clickable 需要區分情況,如 Button 的 clickable 默認為 true,而TextView的 clickable 默認為 false。
2、View 事件分發源碼
先從 Activity 中的 dispatchTouchEvent 方法出發:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { return super.dispatchTouchEvent(ev); }
Activity 將事件傳給父 Activity 來處理,下面看父 Activity 是怎麽處理的。
/** * Called to process touch screen events. You can override this to * intercept all touch screen events before they are dispatched to the * window. Be sure to call this implementation for touch screen events * that should be handled normally. * * @param ev The touch screen event. * * @return boolean Return true if this event was consumed. */ public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
其中有個 onUserInteraction 方法,該方法是只要用戶在 Activity 的任何一處點擊或者滑動都會響應,一般不使用。接下去看getWindow().superDispatchTouchEvent(ev) 所代表的具體含義。getWindow() 返回對應的 Activity 的 window。一個Activity 對應一個 Window 也就是 PhoneWindow, 一個 PhoneWindow 持有一個 DecorView 的實例, DecorView 本身是一個 FrameLayout。這句話一定要牢記。
/** * Retrieve the current {@link android.view.Window} for the activity. * This can be used to directly access parts of the Window API that * are not available through Activity/Screen. * * @return Window The current window, or null if the activity is not * visual. */ public Window getWindow() { return mWindow; }
Window 的源碼有說明 The only existing implementation of this abstract class is
android.view.PhoneWindow,Window 的唯一實現類是 PhoneWindow。那麽去看 PhoneWindow 對應的代碼。
public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
PhoneWindow 又調用了 DecorView 的 superDispatchTouchEvent 方法。而這個 DecorView 就是 Window 的根 View,我們通過 setContentView 設置的 View 是它的子 View(Activity 的 setContentView,最終是調用 PhoneWindow 的 setContentView )
到這裏事件已經被傳遞到根 View 中,而根 View 其實也是 ViewGroup。那麽事件在 ViewGroup 中又是如何傳遞的呢?
2.1 ViewGroup 事件分發
public boolean dispatchTouchEvent(MotionEvent ev) { ...... final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); //清除FLAG_DISALLOW_INTERCEPT,並且設置mFirstTouchTarget為null resetTouchState(){ if(mFirstTouchTarget!=null){mFirstTouchTarget==null;} mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; ...... }; } final boolean intercepted;//ViewGroup是否攔截事件 // mFirstTouchTarget是ViewGroup中處理事件(return true)的子View //如果沒有子View處理則mFirstTouchTarget=null,ViewGroup自己處理 if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev);//onInterceptTouchEvent ev.setAction(action); } else { intercepted = false; //如果子類設置requestDisallowInterceptTouchEvent(true) //ViewGroup將無法攔截MotionEvent.ACTION_DOWN以外的事件 } } else { intercepted = true; //actionMasked != MotionEvent.ACTION_DOWN並且沒有子View處理事件,則將事件攔截 //並且不會再調用onInterceptTouchEvent詢問是否攔截 } ...... ...... }
先看標紅的代碼,這句話的意思是:當 ACTION_DOWN 事件到來時,或者有子元素處理事件( mFirstTouchTarget != null ),如果子 view 沒有調用 requestDisallowInterceptTouchEvent 來阻止 ViewGroup 的攔截,那麽 ViewGroup 的 onInterceptTouchEvent 就會被調用,來判斷是否是要攔截。所以,當子 View 不讓父 View 攔截事件的時候,即使父 View onInterceptTouchEvent 中返回true 也沒用了。
另外,FLAG_DISALLOW_INTERCEPT 這個
標記位是通過子 View requestDisallowInterceptTouchEvent
方法設置的。 具體可參看如下代碼。
@Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { // We‘re already in this state, assume our ancestors are too return; } if (disallowIntercept) { mGroupFlags |= FLAG_DISALLOW_INTERCEPT; } else { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // Pass it up to our parent if (mParent != null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); } }
同時,如果這個 ViewGroup 有父 View 的時候,還得讓父父 View 不能攔截。繼續看 ViewGroup 的 dispatchTouchEvent 方法。
public boolean dispatchTouchEvent(MotionEvent ev) { 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 (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); //如果子View沒有播放動畫,而且點擊事件的坐標在子View的區域內,繼續下面的判斷 continue; } //判斷是否有子View處理了事件 newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { //如果已經有子View處理了事件,即mFirstTouchTarget!=null,終止循環。 newTouchTarget.pointerIdBits |= idBitsToAssign; break; } if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { //點擊dispatchTransformedTouchEvent代碼發現其執行方法實際為 //return child.dispatchTouchEvent(event); (因為child!=null) //所以如果有子View處理了事件,我們就進行下一步:賦值 ...... newTouchTarget = addTouchTarget(child, idBitsToAssign); //addTouchTarget方法裏完成了對mFirstTouchTarget的賦值 alreadyDispatchedToNewTouchTarget = true; break; } } } private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; } private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { ...... if (child == null) { //如果沒有子View處理事件,就自己處理 handled = super.dispatchTouchEvent(event); } else { //有子View,調用子View的dispatchTouchEvent方法 handled = child.dispatchTouchEvent(event); ...... return handled; }
上面為 ViewGroup 對事件的分發,主要有 2 點
-
如果有子 View,則調用子 View 的 dispatchTouchEvent 方法判斷是否處理了事件,如果處理了便賦值 mFirstTouchTarget,賦值成功則跳出循環。
-
ViewGroup 的事件分發最終還是調用 View 的
dispatchTouchEvent
方法,具體如上代碼所述。
2.2 View 的事件分發
public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } return onTouchEvent(event); }
上述方法只有以下3個條件都為真,dispatchTouchEvent() 才返回 true;否則執行 onTouchEvent()。
-
mOnTouchListener != null
-
(mViewFlags & ENABLED_MASK) == ENABLED
-
mOnTouchListener.onTouch(this, event)
這也就說明如果調用了 setOnTouchListener 設置了 listener, 就會先調用 onTouch 方法。沒有的話才會去調用
onTouchEvent 方法。接下去,我們看 onTouchEvent 源碼。
public boolean onTouchEvent(MotionEvent event) { final int viewFlags = mViewFlags; if ((viewFlags & ENABLED_MASK) == DISABLED) { return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); }
// 如果進行了事件代理,就會被攔截,不會在往下面走了 if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } // 若該控件可點擊,則進入switch判斷中 if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { switch (event.getAction()) { // a. 若當前的事件 = 擡起View(主要分析) case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PREPRESSED) != 0; ...// 經過種種判斷,此處省略 // 執行performClick() ->>分析1 performClick(); break; // b. 若當前的事件 = 按下View case MotionEvent.ACTION_DOWN: if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPrivateFlags |= PREPRESSED; mHasPerformedLongPress = false; postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); break; // c. 若當前的事件 = 結束事件(非人為原因) case MotionEvent.ACTION_CANCEL: mPrivateFlags &= ~PRESSED; refreshDrawableState(); removeTapCallback(); break; // d. 若當前的事件 = 滑動View case MotionEvent.ACTION_MOVE: final int x = (int) event.getX(); final int y = (int) event.getY(); int slop = mTouchSlop; if ((x < 0 - slop) || (x >= getWidth() + slop) || (y < 0 - slop) || (y >= getHeight() + slop)) { // Outside button removeTapCallback(); if ((mPrivateFlags & PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); // Need to switch from pressed to not pressed mPrivateFlags &= ~PRESSED; refreshDrawableState(); } } break; } // 若該控件可點擊,就一定返回true return true; } // 若該控件不可點擊,就一定返回false return false; } /** * 分析1:performClick() */ public boolean performClick() { if (mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); return true; // 只要我們通過setOnClickListener()為控件View註冊1個點擊事件 // 那麽就會給mOnClickListener變量賦值(即不為空) // 則會往下回調onClick() & performClick()返回true } return false; }
從上面的代碼我們可以知道,當手指擡起的時候,也就是處於 MotionEvent.ACTION_UP 時,才會去調用 performClick()。而 performClick 中會調用 onClick 方法。
也就說明了:三者優先級是 onTouch->onTouchEvent->onClick
至此 View 的事件分發機制講解完畢。
參考文獻:
1、Android View的事件分發機制和滑動沖突解決
2、一文讀懂Android View事件分發機制
3、Android事件分發機制詳解:史上最全面、最易懂
Android View 的事件分發原理解析