一,寫在前面
相信大家一定遇到過這樣的問題:在滑動某一控件時,由於有多個控件都能處理當前滑動,比如:Viewpager中有一個子元素是Viewpager,那麼在左右滑動的時候,到底是哪個Viewpager控件去處理事件,並最終消費掉事件呢?這裏其實就是產生了滑動衝突了,就像遊戲裏打野搶ad資源,本來團隊是將資源給ad發育的,打野一個技能收掉了,於是需要一個服衆的領導者去處理衝突,讓該拿資源的人吃資源。而,事件滑動衝突發生了,作爲領導者的我們,就去處理衝突,於是要理解衝突發生的原因,這樣控件才能“心服口服”。
二,初識事件分發
當我們的手觸摸到屏幕上時,一般有這樣三個操作Action_down,Action_move,Action_up,分別對應手指的按下,移動,擡起操作。該三個操作加在一起是一個事件序列,按照Action_down,Action_move,Action_up的順序。當有一個事件發生時,最開始接受該事件的是應用程序的窗口-Activity,然後交給PhoneWindow處理,再傳遞給DecorView處理,最後會傳遞給setContentView(R.layout.xxx)的根View,然後傳遞給下一層的子元素(若子元素有多個,則會按時間逆序傳遞,即,最晚添加的子元素,最先傳遞),一層一層類此......直到傳遞到最後的葉子節點(必爲非容器控件)。
在前面,一層層傳遞的過程中,是假設傳遞過程中沒有控件攔截事件,這樣才能傳遞到最後的葉子節點。當事件傳遞到某一個view時,這個view的dispatchTouchEvent(ev)會被調用,若該view對該事件攔截,那麼onInterceptTouchEvent(ev)被調用並返回true,並執行onTouchEvent(ev)對事件進行具體的處理。完整的事件處理走向,這裏就不碼文字介紹了,後面會通過源碼來解釋事件分發的流程。
方法介紹
下面介紹三個方法:
dispatchTouchEvent(ev):只要事件傳遞到View,就會調用該方法。返回true,則事件被處理並消費;返回false,則事件沒有消費掉,如果是action_down,事件交給父view處理;如果是action_move,action_up,則事件直接交給Activity處理。
onInterceptTouchEvent(ev):該方法只有繼承了ViewGroup的容器控件纔有,原始的View(非容器控件)沒有子View,因此沒有該方法。該方法被調用後,如果返回true,代表要攔截事件,則view會調用onTouchEvent(ev)處理事件;如果返回false,代表不攔截,則事件會傳遞到子View。
onTouchEvent(ev):容器控件和原始的View處理事件的地方,如果返回true,則事件被消費,dispatchTouchEvent(ev)返回true;如果返回false,則事件沒有消費。
下面列出Activity,View,Viewgroup,AbsListView,TextView的關係圖,以便清晰認識哪些方法是繼承的,哪些方法是重寫。如下:
下面展示上述三個方法在處理事件時的僞代碼:
public boolean dispatchTouchEvent(MotionEvent ev) {
//是否消費標誌
boolean isConsume = false;
if (onInterceptTouchEvent(ev)) {
//如果攔截
isConsume = onTouchEvent(ev);
} else {
//子元素處理
isConsume = child.dispatchTouchEvent(ev);
}
return isConsume;
}
三,源碼分析
事件最開始交給Activity處理,查看Activity$dispatchTouchEvent源碼:
/**
* 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);
}
當getWindow().superDispatchTouchEvent(ev)爲true時,return true,dispatchTouchEvent對事件處理完畢,事件交給if條件裏對應的view處理了(具體是哪個view後面會分析)。如果getWindow().superDispatchTouchEvent(ev)返回false,即沒有view處理,會執行return onTouchEvent(ev),交給Activity的onTouchEvent(ev)處理,不再說明Activity的onTouchEvent(ev)如何處理事件,這不是本篇文章重點。
接下來看if 條件裏的內容,getWindow()對應Window的唯一子類PhoneWindow,查看PhoneWindow$superDispatchTouchEvent(ev)源碼:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
mDecor就是DecorView對象,事件於是交給DecorView的superDispatchTouchEvent(ev)處理,查看該源碼:
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
//...code
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
//...code
}
該方法裏調用了super.dispatchTouchEvent(event)對事件進行處理,分析DecorView的繼承關係,它是繼承FrameLayout,說明事件轉發肯定會調用View裏的dispatchTouchEvent(ev)。然後事件會傳遞到setContentView(R.layout.xxx)對應佈局文件裏的根View,然後一層層轉發。至於,事件是如何從DecorView傳遞到根View的,不作分析,可以肯定的是事件肯定是傳遞到了根View,不然如何能響應點擊事件呢,這裏不影響分析事件分發,主要是研究根View以後的轉發情況,來理解處理滑動衝突。
一般情況,根View是一個容器控件,然後向下傳遞事件,那麼就先拉出容器控件的爸爸->ViewGroup的代碼進行分析。查看ViewGroup$dispatchTouchEvent(ev)源碼如下:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// ...code
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
//A
// 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);
//設置變量mGroupFlags的最高位爲0
resetTouchState();
}
//B
// Check for interception.
final boolean intercepted;
//若子View處理並消費了action_down事件,mFirstTouchTarget不爲空
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;
}
//C
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
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 (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final View[] children = mChildren;
final boolean customOrder = isChildrenDrawingOrderEnabled();
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder ?
getChildDrawingOrder(childrenCount, i) : i;
final View child = children[childIndex];
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
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)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
mLastTouchDownIndex = childIndex;
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
// code...
}
}
//D
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
//code ...
return handled;
}
/**
* Resets all touch state in preparation for a new cycle.
*/
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
注意:在上面代碼中,註釋有ABCD4個區域,分4塊來分析,一塊塊解決。現在先以ACTION_DOWN事件爲例來分析源碼,然後再分析action_move,action_up事件。下面類似於A-13這樣的標號:代表A區域,代碼的第13行。
現在有一個action_down事件傳遞到了容器控件(縮寫爲vp),於是dispatchTouchEvent(ev)被調用。第5行定義的 boolean handled = false;會被return,它的值表明該事件是否被消費掉了。執行到A-13,進入if語句,會調用resetTouchState(),進入該方法發現有這樣一段代碼:mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;目的是設置變量mGroupFlags的最高位爲0,其中FLAG_DISALLOW_INTERCEPT爲0x8000;也就是說只要是action_down事件傳遞過來,那麼會對字段mGroupFlags進行一個最高位爲0的處理(後面自有用處)。
程序繼續指定到A-25行,由於這裏是action_down事件,那麼actionMasked == MotionEvent.ACTION_DOWN爲true,變量mFirstTouchTarget是指當vp的子元素處理消費了action_down/move/up事件時,mFirstTouchTarget指向一個對象地址,不爲空(後面自有用處)。
B-25值爲true,進入if語句,分析B-27行:mGroupFlags值受上面提到resetTouchState()的改變,還會受ViewGroup$requestDisallowInterceptTouchEvent(boolean)的改變,該方法會使mGroupFlags的最高位爲1。action_down事件在設置了mGroupFlags最高位爲0,會使disallowIntercept值爲false,進入B-28的if語句中,執行intercepted = onInterceptTouchEvent(ev)。requestDisallowInterceptTouchEvent一般在子View中調用,使mGroupFlags的最高位爲1,會使disallowIntercept值爲true,進入B-31的else語句中,不讓vp執行onInterceptTouchEvent(ev),而直接設置intercepted = false,不讓父控件vp攔截該事件。注意:若事件爲action_down,子控件調用requestDisallowInterceptTouchEvent方法無法阻止父控件vp對事件進行攔截,原因:前面A-13的作用就是爲了讓父控件始終能有機會攔截到action_down事件。至於,最終vp是否攔截事件,還要看onInterceptTouchEvent方法裏面的具體處理。於是,查看ViewGroup$onInterceptTouchEvent(ev)源碼:
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
哇,ViewGroup$onInterceptTouchEvent(ev)始終返回false,不攔截事件。所以一般容器控件了爲了處理一些滑動,會重寫onInterceptTouchEvent(ev)對事件進行攔截,例如AbsListView。
那麼B-32行的else語句什麼時候執行呢?第一種情況:若前面的action_down/move/up事件被vp攔截了(onInterceptTouchEvent(ev)返回true),那麼intercepted = true,不會再執行onInterceptTouchEvent(ev)方法,vp會將一個事件序列全部攔截處理;第二種情況:若vp沒有攔截事件action_down/move/up,但是子view處理事件後,沒有消費掉事件,會使mFirstTouchTarget爲null,執行intercepted = true,後面的事件交給父控件vp處理了。
結論:
1,事件爲action_down時,容器控件Viewgroup肯定會執行onInterceptTouchEvent(ev)方法,判斷是否要攔截,;若攔截了,則後面傳遞的action_move/up事件不會再執行onInterceptTouchEvent(ev)方法判斷是否攔截,而是都攔截給vp自己處理,這裏就沒有子View什麼事啦。
2,若action_down事件能傳遞到子View,即vp沒有攔截,若子View及其子元素沒有一個view能消費掉action_down事件,那麼該事件會交給子View的父控件vp處理(後面有代碼分析證明),同時一個序列事件中的action_move/up也不會再傳遞給子View處理,而是都交給它的父View處理。(上面一段已證明)
3,在2的基礎上,子View及其子元素有一個view能消費掉action_down事件時,但後面的action_move/up事件沒有消費,那麼該事件不會再給父控件vp處理,而是直接交給Activity處理。(後面有分析證明這點,這裏先一起提出來)
繼續分析源碼(這裏還是先分析action_down):如果vp不攔截action_down事件,那麼C-48中值爲true,進入if語句;C-49,C-50,C-51都是判斷action_down事件的,其他事件無法進入if語句。執行到C-61,若vp的子元素不爲0,則進入if語句;繼續執行到C-69,遍歷子元素,執行到C-73,C-74。isTransformedTouchPointInView(x, y, child, null):代表觸摸點是否在該子元素佈局內,canViewReceivePointerEvents(child):是否在執行動畫。若兩者均爲true,則繼續執行到C-87;否則執行continue,當前循環結束,遍歷下一個子元素。
代碼執行到C-87,調用dispatchTransformedTouchEvent方法,這裏就是子View處理action_down的地方,查看ViewGroup$dispatchTransformedTouchEvent源碼,裏面有這樣一段代碼:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
簡單解釋下:當child爲null時,事件由vp處理,並調用super.dispatchTouchEvent(event)處理。(後面會分析這個調用)
child不爲空時,上面C-87的child就不爲空,調用child.dispatchTouchEvent(event),這就是前面說的事件傳遞到view,那麼會調用view的dispatchTouchEvent(event)方法。若child爲原始的View(非容器控件),無法再傳遞事件了,那麼直接交給child處理。若child爲容器控件,則繼續重複上面的流程,將事件依次轉發下去,直到有一個view消費了事件,child.dispatchTouchEvent(event)返回true,handled爲true;如果沒有一個view消費事件,那麼child.dispatchTouchEvent(event)返回false,handled爲false。
這裏的handled對應C-87的if語句的條件,若條件爲true,會執行newTouchTarget = addTouchTarget(child, idBitsToAssign),alreadyDispatchedToNewTouchTarget = true。若條件爲false,則不執行上面兩步。進入ViewGroup$addTouchTarget源碼:
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
*/
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
addTouchTarget方法內部會對mFirstTouchTarget設置值,使其不爲空。於是證明前面的分析:當子View消費了action_down/move/up事件時,mFirstTouchTarget不爲空。另外,還設置標誌位alreadyDispatchedToNewTouchTarget
爲true。(後面自有用處)
繼續分析源碼,現在分析到了D區域,這裏將對ViewGroup$dispatchTouchEvent(ev)方法確定返回值,反應事件在容器控件vp的處理情況。如果返回值handled爲true,則該事件(action_down/move/up)被消費了,會使得Activity$dispatchTouchEvent(ev)中的getWindow().superDispatchTouchEvent(ev))
的值爲true,執行if語句,return true,消費了事件,事件處理結束;反之,繼續執行Activity$dispatchTouchEvent(ev)中的return onTouchEvent(ev);將事件交給Activity處理。
流程執行到D-105,若action_down事件被vp攔截,或者能傳遞到子View但是沒消費。那麼,if語句值爲true,執行handled = dispatchTransformedTouchEvent(ev, canceled, null,ouchTarget.ALL_POINTER_IDS),參數裏的null,就是前面說的child爲null的情況,執行super.dispatchTouchEvent(event)。這裏ViewGroup的super就是View啦,所以容器控件vp處理事件是調用View$dispatchTouchEvent(ev),裏面調用onTouchEvent(ev)處理事件,下篇文章會分析View$dispatchTouchEvent(ev)如何決定是否消費事件,這裏就打住了。事件處理最終會交給onTouchEvent方法(下一篇文章會講),若onTouchEvent(ev)返回true,則super.dispatchTouchEvent(ev)返回true,消費了事件,那麼此事件的處理就結束了;反之,交給Activity處理。執行了D-105的if語句內容,那麼else中不會執行。
若子view消費了action_down事件,D-116行if條件表達式值爲true,action_down事件消費了,該事件處理流程結束。若子view已經消費了action_down事件,現在傳遞的是action_move/up,則代碼會執行到D-121,D-122行,如果能消費action_move/up事件,return true,該事件處理流程結束;若不能消費,即D-121,D-122行條件表達式爲false,那麼handled不設置爲true,return默認的值false,事件不會交給父View處理,直接交給Activity處理。(此處證明前面的一個結論)
四,另外
最後,分析到這裏,相信不管是action_down事件,還是其他action_move/up事件,這些事件在View中的分發流程基本理清楚了。但是,還沒有介紹事件交給View$onTouchEvent(ev)處理的流程,放在下一篇博客中分析吧。本篇文章基於源碼的分析,並沒有列出利用其解決滑動衝突的方案,但是相信理清了事件分發的流程,稍微記住些小結論用於實踐,處理滑動衝突就可以知其所以然的。其實滑動衝突有兩種解決方案:1,外部攔截法;2,內部攔截法。這裏先作一個引子,後面會碼一篇blog展示如何處理滑動衝突。