Android事件分發之子View駁回ViewGroup攔截原理分析
雖然網上關於這一塊的博文很多,但是找了很久都沒有找到比較全面的分析,所以想自己也開始寫一些部落格,一來讓自己加深印象,二來希望能夠給大家多多少少帶來一些幫助。好了廢話不多說,直接進入主題。
Android事件分發圖
如果用一張圖來描述事件的流程走向的話,那麼下面這張圖可以說是比較全面的且通俗易懂。首先先解釋下三個方法的意思,dispatchTouchEvent()–是否分發事件,onInterceptTouchEvent()–是否攔截事件,僅ViewGroup,onTouchEvent()–當事件觸控是否進行操作。
看完上圖是否已經有一定的理解?在這裡簡單總結一下:
- Activity中有dispatchTouchEvent()及onTouchEvent()方法
- ViewGroup中有dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()方法。
- View中有dispatchTouchEvent()、onTouchEvent()方法。
- 如果不重寫任何方法的情況下系統預設呼叫的父類的方法,事件的走向為Activity - ViewGroup - View - ViewGroup - Activity,最後由Activity自己消費掉。
- 如果任何類中重寫dispatchTouchEvent()方法,return false,那麼表示不繼續向下分發,而是直接流向上一級的onTouchEvent()方法中,如果return true,那麼表示自己消費,本次事件在此終結,不再向任何地方分發。
- ViewGroup中有一個onInterceptTouchEvent()方法,表示是否需要攔截子類的觸控事件,如果該方法中return true,那麼表示該事件由ViewGroup自己消費,事件不會再向下傳遞,而是直接有ViewGroup的onTouchEvent()自己處理,如果return false,則表示不攔截,事件繼續分發至子View中。
- 同樣,首先流到子View時,需要先走子View的dispatchTouchEvent()方法,如果不重寫則該事件交由子View的onTouchEvent()處理消費。
- 如果子View的onTouchEvent()重寫返回true,那麼表示該該事件由子View處理,事件就此終結,反之,如果不重寫該方法或者返回false,那麼事件繼續流向上一級的onTouchEvent()處理,直到Activity終結。
以上就是事件分發的大致流向路線,看起來雖然複雜但其實結合圖形分析的話,還是很容易理解的。
結合原始碼分析
當然瞭解到這裡肯定還不夠,以上只是事件的大致傳遞流程,基於不考慮觸控動作的情況下,那麼我們結合原始碼繼續分析,當觸控的動作不同的情況下,事件的走向又是什麼樣的呢?
在以上的所有類回撥的所有方法中的引數會提供一個Event物件,該物件包含三種事件的動作,根據滑鼠的動作,Event.getAction對應值如下:
- Event.ACTION_DOWN(滑鼠點下去的那一瞬間呼叫)
- Event.ACTION_MOVE(當滑鼠移動時)
- Event.ACTION_UP(當滑鼠擡起時)
根據getAction判斷滑鼠的動作,當滑鼠動作不一樣時,事件走向又是怎麼走的呢?先看原始碼我們再進行分析
dispatchTouchEvent()中ACTION_DOWN:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!onFilterTouchEventForSecurity(ev)) {
return false;
}
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
// this is weird, we got a pen down, but we thought it was
// already down!
// XXX: We should probably send an ACTION_UP to the current
// target.
mMotionTarget = null;
}
// If we're disallowing intercept or if we're allowing and we didn't
// intercept
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// reset this event's action (just to protect ourselves)
ev.setAction(MotionEvent.ACTION_DOWN);
// We know we want to dispatch the event down, find a child
// who can handle it, start with the front-most child.
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
// offset the event to the view's coordinate system
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
if (child.dispatchTouchEvent(ev)) {
// Event handled, we have a target now.
mMotionTarget = child;
return true;
}
// The event didn't get handled, try the next view.
// Don't reset the event's location, it's not
// necessary here.
}
}
}
}
}
dispatchTouchEvent()中ACTION_MOVE:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//...ACTION_DOWN
//...ACTIN_UP or ACTION_CANCEL
// The event wasn't an ACTION_DOWN, dispatch it to our target if
// we have one.
final View target = mMotionTarget;
// if have a target, see if we're allowed to and want to intercept its
// events
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
//....
}
// finally offset the event to the target's coordinate system and
// dispatch the event.
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
return target.dispatchTouchEvent(ev);
}
dispatchTouchEvent()中ACTION_UP:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!onFilterTouchEventForSecurity(ev)) {
return false;
}
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {...}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
if(target ==null ){...}
if (!disallowIntercept && onInterceptTouchEvent(ev)) {...}
if (isUpOrCancel) {
mMotionTarget = null;
}
// finally offset the event to the target's coordinate system and
// dispatch the event.
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
return target.dispatchTouchEvent(ev);
}
正常情況下,即我們上例整個程式碼的流程我們已經走完了:
1、ACTION_DOWN中,ViewGroup捕獲到事件,然後判斷是否攔截,如果沒有攔截,則找到包含當前x,y座標的子View,賦值給mMotionTarget,然後呼叫 mMotionTarget.dispatchTouchEvent
2、ACTION_MOVE中,ViewGroup捕獲到事件,然後判斷是否攔截,如果沒有攔截,則直接呼叫mMotionTarget.dispatchTouchEvent(ev)
3、ACTION_UP中,ViewGroup捕獲到事件,然後判斷是否攔截,如果沒有攔截,則直接呼叫mMotionTarget.dispatchTouchEvent(ev)
當然了在分發之前都會修改下座標系統,把當前的x,y分別減去child.left 和 child.top ,然後傳給child;
也就是說,三個動作根據返回的值來決定是否繼續分發至下一級,如果繼續分發,那就將此動作的座標值作為引數傳給自己的onInterceptTouchEvent(ev)方法。這裡值得注意的是返回值的關鍵在 if(disallowIntercept || !onInterceptTouchEvent(ev))這一句程式碼,通過這一句程式碼判斷是否需要進行攔截,區域性boolean型別的變數disallowIntercept (是否駁回攔截,通過該類中的viewGroup.requestDisallowInterceptTouchEvent(boolean)方法進行賦值,後面繼續講)及onInterceptTouchEvent(ev)的返回值,預設是進入該if語句中的方法,並通過遍歷得到子View並呼叫子View的dispatchTouchEvent(ev)方法將事件傳遞下去
事件攔截原始碼
上面事件傳遞到子View的dispatchTouchEvent(ev)方法的前提是當ViewGroup.onInterceptTouchEvent(ev)不攔截的時候(disallowIntercept的預設為false,不重新賦值的情況下)
如果當ViewGroup.onInterceptTouchEvent(ev)不一樣呢,我們複寫下面這一段程式碼:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev)
{
int action = ev.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
//如果你覺得需要攔截
return true ;
case MotionEvent.ACTION_MOVE:
//如果你覺得需要攔截
return true ;
case MotionEvent.ACTION_UP:
//如果你覺得需要攔截
return true ;
}
return false;
}
預設是不攔截的,即返回false;如果你需要攔截,只要return true就行了,這要該事件就不會往子View傳遞了,並且如果你在DOWN retrun true ,則DOWN,MOVE,UP子View都不會捕獲事件;如果你在MOVE return true , 則子View在MOVE和UP都不會捕獲事件。
原因很簡單,當onInterceptTouchEvent(ev) return true的時候,會把mMotionTarget 置為null 。
好的,重點來了,當MOVE時return true,子View還想拿到MOVE和UP事件怎麼辦呢?方法很簡單,我們回顧下一開始的ViewGroup中的dispatchTouchEvent(ev)–ACTION_DOWN中的原始碼:
- 在原始碼裡如果不重寫,該方法預設會找到被點選的子View。
- 並呼叫該子View的dispatchTouchEvent(ev)方法
- 如果子View想禁止ViewGroup在MOVE的時候攔截事件,那麼機會來了,當呼叫子類的dispatchTouchEvent(ev)方法的時候,我們在子類的方法中新增這句程式碼getParent().requestDisallowInterceptTouchEvent(boolean)傳入true即將ViewGroup中的ViewGroup的變數disallowIntercept的值 設定為true了,也就是駁回攔截,那麼當ViewGroup再次MOVE和UP時,就直接忽略ViewGroup的攔截方法的返回值了,直接進入IF語句將事件往下傳遞。
- 當然,如果ViewGroup一開始DOWN時就直接反饋true,那麼上面的方式是無效的
總結
事件分發的流程大致情況如上,本次重點想讓大家理解是當ViewGroup的MOVE時事件被攔截了,View也可以獲取事件的方法及原理。個人見解,如果有誤,歡迎指正,謝謝!