概述
在介紹點擊事件規則之前,我們需要知道我們分析的是MotionEvent,即點擊事件,所謂的事件分發就是對MotionEvent事件的分發過程,即當一個MotionEvent生成以后,系統需要把這個事件傳遞給具體的View,而這個傳遞過程就是分發過程,MotionEvent我們上節已經介紹過
事件分發主要涉及以下幾個方法:
- dispatchTouchEvent:用來進行事件的分發,如果事件可以傳遞到當前View那么此方法一定會被調用,返回結果受當前View的onTouchEvent和子View的dispatchTouchEvent方法影響,表示是否消耗當前事件
- onInterceptTouchEvent:在上個方法內部調用,用來判斷是否攔截事件,如果當前View攔截了事件,那么在同一時間序列內,此方法不會再次被調用,返回結果表示是否攔截事件
- onTouchEvent:在dispatchTouchEvent方法中調用,用于事件的處理,返回值表示是否消耗事件,如果不消耗當前View無法再次接受到事件
這三個方法到底有什么關系?
我們先簡述一下他們之間的關系,之后再進行源碼的詳細分析
當一個事件傳遞給一個根ViewGroup之后,這時他的dispatchTouchEvent就會被調用,進行事件的分發,如果該ViewGroup的onInterceptTouchEvent返回true,表示他要攔截此事件,接著這個事件就會交給ViewGroup處理,即他的onTouchEvent就會被調用,如果他的onInterceptTouchEvent返回fasle就表示不攔截此事件,這時就會把此事件傳遞給他的子View,接著子View的dispatchTouchEvent就會被調用,如此反復直到事件最終被處理
源碼分析
當一個事件產生后,他的傳遞遵循如下順序Activity→Window→View,即事件總是縣傳遞給Activity,然后Activity傳遞給Window,最后Window傳遞給頂級View,頂級View接收到事件后,就會按照事件分發機制分發事件
Activity對事件的分發
當一個點擊操作發生時,事件最先傳遞給當前的Activity,由Activity的dispatchTouchEvent進行分發,我們看下Activity的dispatchTouchEvent的源碼
public boolean dispatchTouchEvent(MotionEvent ev) {if (ev.getAction() == MotionEvent.ACTION_DOWN) {onUserInteraction();}if (getWindow().superDispatchTouchEvent(ev)) {return true;}return onTouchEvent(ev);}
復制代碼
上面代碼表示,Activity會把事件交給Window處理,如果Window的分發返回true,表示事件就此結束,返回false,表示沒有人處理,那么Activity的onTouchEvent就會被調用
Window對事件的分發
那么Window是怎么分發事件的呢?我們看下Window的源碼,我們發現Window其實是一個抽象類,superDispatchTouchEvent也是一個抽象方法
public abstract boolean superDispatchTouchEvent(MotionEvent event);
復制代碼
那么Window的實現類是什么?其實是PhoneWindow,那我們看一下PhoneWindow是怎么處理事件的
@Overridepublic boolean superDispatchTouchEvent(MotionEvent event) {return mDecor.superDispatchTouchEvent(event);}
復制代碼
PhoneWindow直接把事件交給了DecorView,DecorView其實就是最頂層的View我們setContentView的View就是DecorView的一個子View,DecorView繼承自FrameLayout,這個時候事件已經分發到了ViewGroup上
ViewGroup事件的分發
現在我們看一下ViewGroup的dispatchTouchEvent方法的源碼
@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {...
//--------TAG=1-------------------這里是一開始---------------------------------------------------//如果是Action_down 就對其先前所有的狀態進行重置if (actionMasked == MotionEvent.ACTION_DOWN) {cancelAndClearTouchTargets(ev);resetTouchState();}
//--------TAG=2-----------------這里開始進行攔截驗證-----------------------------------------------//如果是ACTION_DOWN,或者mFirstTouchTarget != null,就進行攔截驗證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;}
//------------------------------------------------------------------------------------------------------------------------------....//----------TAG=3----------------這里看是遍歷子view---------------------------------------------------------------//如果不攔截,并且不是cancel事件,就進行遍歷子view分發事件if (!canceled && !intercepted) {...//當ACTION_DOWN和ACTION_POINTER_DOWN和ACTION_HOVER_MOVE時候才會遍歷子viewif (actionMasked == MotionEvent.ACTION_DOWN|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {//找到可以接受觸摸事件孩子,從前向后遍歷查找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);...//判斷觸摸點是否在此View的范圍中,是否在移動if (!canViewReceivePointerEvents(child)|| !isTransformedTouchPointInView(x, y, child, null)) {ev.setTargetAccessibilityFocus(false);continue;}...//分發事件,如果事件被子view消費,就跳出循環,不再繼續分發給其他viewif (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {...//addTouchTarget內部賦值mFirstTouchTarget=當前viewnewTouchTarget = addTouchTarget(child, idBitsToAssign);alreadyDispatchedToNewTouchTarget = true;break;}}
//-----------TAG=4-----------------這里已經遍歷完了子view--------------------------------------------// //遍歷完所有的子View后,還沒有處理事件,就自己處理if (mFirstTouchTarget == null) {// No touch targets so treat this as an ordinary view.handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);} else {//Action_Down之外的事件直接分發給目標viewTouchTarget 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;}...}}
//------------------------------------------------------------------------------------------------------------------------------// Update list of touch targets for pointer up or cancel, if needed.if (canceled|| actionMasked == MotionEvent.ACTION_UP|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {resetTouchState();} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {final int actionIndex = ev.getActionIndex();final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);removePointersFromTouchTargets(idBitsToRemove);}}if (!handled && mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);}return handled;}
復制代碼
首先我們分析一下攔截事件的源碼
//如果是ACTION_DOWN,或者mFirstTouchTarget != null,就進行攔截驗證final boolean intercepted;if (actionMasked == MotionEvent.ACTION_DOW || 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;}
復制代碼
這段代碼我們可以看到,有倆種情況會判斷是否要攔截當前事件,事件類型是Action_Down,或者mFirstTouchTarget != null,ACTION_DOWN我們可以理解,mFirstTouchTarget != null代表什么呢?
我們從后面的代碼可以看出,事件由ViewGroup的子元素處理成功時,mFirstTouchTarget被賦值并指向該子元素,也就是說當ViewGroup不攔截事件交由子元素處理時mFirstTouchTarget != null
一旦ViewGroup攔截事件mFirstTouchTarget != null就不成立,而當ACTION_MOVE ,ACTION_UP到來時,由于(actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)這個判斷為false,ViewGroup的onInterceptTouchEvent不在會被調用,并且同一序列的其他事件,會默認交給ViewGroup處理
這里還有一種特殊情況,FLAG_DISALLOW_INTERCEPT標志位,這個標志位是通過requestDisallowInterceptTouchEvent來設置的,一般用于子View中,一旦FLAG_DISALLOW_INTERCEPT標志為被設置后,ViewGroup將無法攔截,除了ACTION_DOWN之外的其他事件,為什么要除了ACTION_DOWN呢,因為每當ACTION_DOWN帶來都會重置FLAG_DISALLOW_INTERCEPT這個標記位,ACTION_DOWN事件總會調用自己的onInterceptTouchEvent詢問是否攔截
強調一點requestDisallowInterceptTouchEvent,這個方法并不是萬能的,執行他的前提是子View必須獲取事件,假如父View的Down事件的onInterceptTouchEvent就返回true,攔截事件,那么子View做任何操作也不可能獲取到事件
從上面分析我們可以得出結論
- 當ViewGroup決定攔截事件的時候,那么后續的點擊事件將默認交給他,不再調用onInterceptTouchEvent
- FLAG_DISALLOW_INTERCEPT作用是讓ViewGroup不再攔截事件,前提是ViewGroup不攔截Action_Down事件
- onInterceptTouchEvent不是每次都會調用的,如果我們要提前處理點擊事件需要在dispatchTouchEvent
- 當我們遇到滑動沖突的時候,可以考慮FLAG_DISALLOW_INTERCEPT來處理
我們看一下ViewGroup不攔截的事件的情況
先看一下源碼,這個是刪減后的源碼,看起來比較清楚
//如果不攔截,并且不是cancel事件,就進行遍歷子view分發事件if (!canceled && !intercepted) {...//當ACTION_DOWN和ACTION_POINTER_DOWN和ACTION_HOVER_MOVE時候才會遍歷子viewif (actionMasked == MotionEvent.ACTION_DOWN|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {//找到可以接受觸摸事件孩子,從前向后遍歷查找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);...//判斷觸摸點是否在此View的范圍中,是否在移動if (!canViewReceivePointerEvents(child)|| !isTransformedTouchPointInView(x, y, child, null)) {ev.setTargetAccessibilityFocus(false);continue;}...//分發事件,如果事件被子view消費,就跳出循環,不再繼續分發給其他viewif (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {...//addTouchTarget內部賦值mFirstTouchTarget=當前viewnewTouchTarget = addTouchTarget(child, idBitsToAssign);alreadyDispatchedToNewTouchTarget = true;break;}}
復制代碼
首先遍歷ViewGroup的所有子元素,然后判斷判斷子元素是否能接收到點擊事件,是否能接收到點擊事件主要由倆點來衡量
- 點擊的坐標是否落在了子元素的區域內
- 子元素是否在播放動畫
如果子元素滿足這倆個條件,那么事件將傳遞給他處理,分發事件其實dispatchTransformedTouchEvent是這個方法做的,我們看一下dispatchTransformedTouchEvent源碼
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {final boolean handled;//先記住這一段判斷cancel的源碼,很重要下面分析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;}....if (child == null) {handled = super.dispatchTouchEvent(event);} else {...handled = child.dispatchTouchEvent(event);}.....return handled;}
復制代碼
這里面主要代碼如果 if (cancel || oldAction == MotionEvent.ACTION_CANCEL) 為false,這個判斷的意思是,如果不是ACTION_CANCEL,外部傳入的cancel也為fasle,就進行下面的判斷,而下面的判斷主要是根據傳入的child是否為null來判斷的,如果child不為null,那么就調用child的dispatchTouchEvent方法,這個事件就交給子元素去處理,這就完成一輪的事件分發
如果child的dispatchTouchEvent返回為true,先不考慮事件怎么在子元素中分發,那么mFirstTouchTarget就被賦值,跳出for循環
//分發事件,如果事件被子view消費,就跳出循環,不再繼續分發給其他viewif (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {...//addTouchTarget內部賦值mFirstTouchTarget=當前viewnewTouchTarget = addTouchTarget(child, idBitsToAssign);alreadyDispatchedToNewTouchTarget = true;break;}
復制代碼
上面的代碼完成了,給mFirstTouchTarget賦值,并且跳出for循環,終止對子元素的遍歷,如果子元素的dispatchTouchEvent返回fasle,那么就會繼續遍歷子元素,把事件傳遞給下一個合適的子元素(如果還有合適的子元素的話)
mFirstTouchTarget賦值是在addTouchTarget方法內部完成的,mFirstTouchTarget是一個單鏈表結構
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);//注意這里這里很重要,target.next =null,然后 mFirstTouchTarget = target;也就是說這時候的 mFirstTouchTarget.next=nulltarget.next = mFirstTouchTarget;mFirstTouchTarget = target;return target;}
復制代碼
如果遍歷所有的子元素事件都沒有合適的處理,這里包含倆種情況,一種就是ViewGroup沒有子元素,第二種就是子元素的dispatchTouchEvent返回了fasle,這倆種情況下ViewGroup會自己處理事件
//遍歷完所有的子View后,還沒有處理事件,就自己處理if (mFirstTouchTarget == null) {handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);}
復制代碼
注意這里child參數傳入的是null,根據之前的分析就會調用 super.dispatchTouchEvent(event);由于ViewGroup也是繼承自View,這里就會轉到View的dispatchTouchEvent,即點擊事件交給View處理
注意敲黑板了啊
我看了很多博客,都沒有對這種情況進行分析,這個問題一度卡了我很久
現在考慮一種情況,如果父View的onInterceptTouchEvent的Down事件返回false不攔截,move up事件返回true攔截,這個效果就是子View只能收到Down事件而收不到Up和Move事件
那么我們現在分析一下這種情況,按照我們上方的分析,父View的Down事件不攔截,那么mFirstTouchTarget就會被賦值,第二次Move和Up事件要攔截,但是由于mFirstTouchTarget被賦值了,所以是走不到下面這步的
// //遍歷完所有的子View后,還沒有處理事件,就自己處理if (mFirstTouchTarget == null) {// No touch targets so treat this as an ordinary view.handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);}
復制代碼
那么父View是怎么攔截Move和Up事件的呢? 當地一個Move事件傳遞給父View后,此時mFirstTouchTarget不為null,所以走攔截這一步代碼
//如果是ACTION_DOWN,或者mFirstTouchTarget != null,就進行攔截驗證final boolean intercepted;if (actionMasked == MotionEvent.ACTION_DOW || 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;}
復制代碼
攔截返回true后,不走遍歷子Vew代碼,直接到最后的判斷代碼
// //遍歷完所有的子View后,還沒有處理事件,就自己處理if (mFirstTouchTarget == null) {// No touch targets so treat this as an ordinary view.handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);} else {//Action_Down之外的事件直接分發給目標viewTouchTarget 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;}}}
復制代碼
由于mFirstTouchTarget在Down的時候已經賦值不為null,會走下邊代碼
final boolean cancelChild = resetCancelNextUpFlag(target.child)|| intercepted;
復制代碼
由于攔截事件,cancelChild為true,也就是說下面這個分發dispatchTransformedTouchEvent的方法傳入的是true
if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {handled = true;}
復制代碼
在這個分發方法里,有判斷Cancel事件的代碼
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {final boolean handled;//先記住這一段判斷cancel的源碼,很重要下面分析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;}...return handled;}
復制代碼
由于傳入的cancel為true, 會重新定義事件為Cancel事件event.setAction(MotionEvent.ACTION_CANCEL);child不為null所以會調用child.dispatchTouchEvent(event);也就是說第一個Move事件,父View不會攔截,但會給子View發送一個Cancel事件
接下來會繼續走代碼
TouchTarget target = mFirstTouchTarget;final TouchTarget next = target.next;
...if (cancelChild) {...mFirstTouchTarget = next;...}
復制代碼
上面已經分析過cancelChild為true,進入方法給mFirstTouchTarget重新賦值mFirstTouchTarget.next,那么mFirstTouchTarget.next等于什么?看下面一段代碼
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);//注意這里這里很重要,target.next =null,然后 mFirstTouchTarget = target;也就是說這時候的 mFirstTouchTarget.next=nulltarget.next = mFirstTouchTarget;mFirstTouchTarget = target;return target;}
復制代碼
其實mFirstTouchTarget.next=null,那整合起來就是把mFirstTouchTarget重新賦值為null,從這里開始,第二個Move事件就會直接傳遞給父View完成了攔截
if (mFirstTouchTarget == null) {handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);}
復制代碼
總結
當父View不攔截Down事件,但要攔截Move和Up事件時,第一個Move事件會重新賦值為Cancel事件發送給子View,然后mFirstTouchTarget賦值為null,第二次開始的Move事件就會交給父View
View的事件分發源碼
View對事件的處理比較簡單,注意這里的View不包括ViewGroup,先看他的dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {...boolean result = false;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的時間傳遞比較簡單,因為View(不包括ViewGroup),是一個單獨的元素,無法向下傳遞事件,所以沒有onInterceptTouchEvent方法,從上面源碼可以看出
- 首先會判斷與沒有mOnTouchListener,如果有并且其中的onTouch方法返回true那么onTouchEvent放方法不會調用,可以看出mOnTouchListener的優先級高于onTouchEvent
下面看一下onTouchEvent方法的源碼
首先看一下,當View處于不可用狀態下,事件的處理過程
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;if ((viewFlags & ENABLED_MASK) == DISABLED) {if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {setPressed(false);}mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;// A disabled view that is clickable still consumes the touch// events, it just doesn't respond to them.return clickable;}
復制代碼
可以看出不可用的狀態下,View消耗點擊事件
再看一下對具體事件的處理
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {switch (action) {case MotionEvent.ACTION_UP:...if (mPerformClick == null) {mPerformClick = new PerformClick();}if (!post(mPerformClick)) {performClickInternal();}}}....case MotionEvent.ACTION_DOWN:...case MotionEvent.ACTION_CANCEL:...case MotionEvent.ACTION_MOVE:...break;}復制代碼
從上面代碼為可以看出
- 只要View的CLICKABLE和LONG_CLICKABLE一個為true,不管他是不是DISABLED狀態都消耗事件,只不過DISABLED不走下面的down,up事件
- 當Action_Up觸發時,會調用PerformClick方法,如果View設置了onClickListener,那么PerformClick將調用他的onClick方法
- View的LONG_CLICKABLE默認是false,但是CLICKABLE是否為fasle,跟具體View有關,可點擊的CLICKABLE為true,不可點擊的CLICKABLE為false
- setClickable和setLongClickable可以改變CLICKABLE,和LONG_CLICKABLE的值
- setClickLinsterer和setLongClickLinsterer會自動設置CLICKABLE和LONG_CLICKABLE為true
到這里事件分發就處理完了
參考:Android開發藝術探索
allenfeng.com/2017/02/22/…