系列文章傳送門 (持續更新中..) :bash
自定義控件(一) Activity的構成(PhoneWindow、DecorView)源碼分析
自定義控件(四) 源碼分析 layout 和 draw 流程動畫
話很少說, 先上圖ui
圖片取自 - 圖解 Android 事件分發機制public boolean dispatchTouchEvent(MotionEvent event) { }this
public boolean onInterceptTouchEvent(MotionEvent ev) { }spa
public boolean onTouchEvent(MotionEvent event) { }代理
這三個方法的關係能夠用下面的僞代碼來直觀的表示:rest
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result=false;
if(onInterceptTouchEvent(ev)){
result=super.onTouchEvent(ev);
}else{
result=child.dispatchTouchEvent(ev);
}
return result;
複製代碼
經過上面的僞代碼, 咱們能夠直觀的明白事件分發的大致流程。當一個點擊事件產生時,根 View 接收到事件並調用 dispatchTouchEvent() 來對事件進行分發, 在方法內部先調用 onInterceptTouchEvent() ,若是返回 true 就表示要攔截這個事件,那麼接下來這個事件就會交給這個 ViewGroup 經過調用本身的 onTouchEvent() 來處理。若是這個 ViewGroup 的 onInterceptTouchEvent 返回 false,表示它本身不攔截當前事件,事件就會分發給它的子view,即調用子view 的 dispatchTouchEvent(), 進行下一輪分發, 如此反覆直到事件最終被處理。code
下面,讓我大體看一下源碼中的分發流程。
#Activity
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
複製代碼
Activity 接收到事件後, 首先交給 Activity 所屬的 Window 分發, 若是返回 true,整個事件的循環就結束了,返回 false 則表示事件沒人處理,全部view 的 onTouchEvent 都返回了 false, 則調用 Activity 本身的 onTouchEvent 來處理事件。從上一篇文章裏瞭解到這個 Window 實現子類就是 PhoneWindow。
#PhoneWindow
public boolean superDispatchKeyShortcutEvent(KeyEvent event) {
return mDecor.superDispatchKeyShortcutEvent(event);
}
複製代碼
PhoneWindow 接着把事件分發給 DecorView, 也證明了以前說分的發過程 Activity -> PhoneWindow -> DecorView
在這裏, 事件分發到了頂級View之後, 會調用 ViewGroup 的 dispatchTouchEvent() 方法, 從這裏開始, 後面就是View 之間的事件分發了.
#DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
複製代碼
咱們繼續看 ViewGroup 中的 dispatchTouchEvent()
方法, 方法有點長, 大致的代碼解釋我都標明在代碼裏面了, 方便你們理解:
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
// 在 resetTouchState ()內部會重置標記位 FLAG_DISALLOW_INTERCEPT,
// 所以 requestDisallow...() 不能影響 ViewGroup 對 ACTION_DOWN 的攔截
resetTouchState();
}
final boolean intercepted;
/**
* 注意 if 的判斷條件:
* 1. 若是是 ACTION_DOWN , 則 if 判斷的條件爲true
* 2. 若是 ViewGroup 不攔截 ACTION_DOWN 而且交給子view 處理了事件 , 則會在後面的方法中給
* mFirstTouchTarget 賦值,即 mFirstTouchTarget != null。此時 if 的判斷條件爲 true
* 3. 若是 ViewGroup 攔截了事件,則 mFirstTouchTarget = null, if 的判斷條件爲 false
* 後面的 move、up都直接進 else{} 裏面, 就不會再調用本身的onInterceptTouchEvent()
* 方法,該事件序列的其它事件都會交給本身處理
*/
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// 此處的標誌位 FLAG_DISALLOW_INTERCEPT 由子類的requestDisallowInterceptTouchEvent
// 決定可是在 ACTION_DOWN 來臨時,會在 resetTouchState() 中將該標記位重置, 因此子類不能影
// 響父類對 ACTION_DOWN 的處理
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev); // 判斷是否攔截事件: ViewGroup 默認是不攔截的
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
...
// 若是事件沒有被攔截, 則會進入這裏面
if (!canceled && !intercepted) {
...
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 可否接受到點擊事件, 若是能夠, 則把事件交給該 子view 處理
// 1. 子view 是否在執行動畫且是 VISIBLE 狀態;
// 2.點擊事件的座標是否在 子view 的區域內
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
...
// 方法內部把事件分發給子view 的 dispatchTouchEvent() 方法處理
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){
...
// 若是子元素開始處理事件,即它的 dispatchTouchEvent() 返回 true,會走到這裏,
// 而且在 addTouchTarget() 方法內部會給 mFirstTouchTarget 賦值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
// 若是該事件沒有被處理, 會走到這裏面
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS); // 注意這裏參數 child 傳入的是 null , 內部則會調用 View 的 dispatchTouchEvent()方法, 而後調用 onTouchEvent 本身處理事件
} else {}
...
return handled;
}
複製代碼
上面 dispatchTouchEvent() 中調用的幾個方法:
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; // 重置標記位 FLAG_DISALLOW_INTERCEPT
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false; // ViewGroup 默認返回 false
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
// 等於null時, 會調用 View 的dispatchTouchEvent() 方法, 由於View沒有子元素, 因此會直接交給本身處理
handled = super.dispatchTouchEvent(event);
} else {
// 傳遞的參數 child 不等於 null, 則會調用子元素的 dispatchTouchEvent(),從而完成了一輪事件的分發
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
}
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target; // 給 mFirstTouchTarget 賦值
return target;
}
複製代碼
- 首先判斷是否要調用本身的
onInterceptTouchEvent()
方法有兩個條件: 事件爲ACTION_DOWN
和mFirstTouchTarget != null
, 而mFirstTouchTarget
是在當前的 ViewGroup 不攔截事件, 而且子元素開始處理事件時會被賦值並指向子元素。因此一旦當前的 ViewGroup 攔截了事件,則mFirstTouchTarget != null
就不成立, 然後續的 move、up 事件,因爲 if 的判斷條件爲 false,致使都不會再調用 ViewGroup 的onInterceptTouchEvent
, 而且同一事件序列的其它事件都會默認交給它處理FLAG_DISALLOW_INTERCEPT
這標記是由子類的requestDisallowInterceptTouchEvent
來決定的,可是在ACTION_DOWN
發生時,會在resetTouchState()
中重置這個標誌位,。因此當子view 設置了FLAG_DISALLOW_INTERCEPT
,它的 ViewGroup 將沒法攔截除了ACTION_DOWN
之外的事件。所以,當面對ACTION_DOWN
事件來臨時,ViewGroup 老是會調用本身的onInterceptTouchEvent
方法來決定是否攔截事件。- 當 ViewGroup 決定攔截事件後,後序的點擊事件默認會交給它本身處理, 而不會重複再調用
onInterceptTouchEvent
。因此,onInterceptTouchEvent
不是老是被調用的,當咱們要提早處理點擊事件時,要使用 dispatchTouchEvent- 若是子元素的
dispatchTouchEvent
返回 true, 那麼就會跳出 ViewGroup 遍歷子集的循環,並在addTouchTarget()
給mFirstTouchTarget
賦值- 在遍歷全部子元素後事件沒有被合適的處理, 有兩種狀況: 1. ViewGroup 沒有子元素; 2. 或者 子view 處理了點擊事件可是在
dispatchTouchEvent(
) 方法裏面返回 false, 通常是onTouchEvent()
返回了false
先看它的 dispatchTouchEvent :
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
// 首先判斷有沒有設置 onTouchListener, 若是 onTouch() 返回 true, 則不會再走 onTouchEvent()
// 說明 mOnTouchListener.onTouch() 的優先級要比 onTouchEvent() 高
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 對點擊事件的處理過程, 首先判斷有沒有設置 OnTouchListener ,若是 OnTouchListener 中的 onTouch 中返回了 true, 那麼 onTouchEvent 不會被調用, 可見 OnTouchListener 的優先級高於 onTouchEvent , 這樣作的好處是方便在外界處理點擊事件
繼續看 onTouchEvent :
public boolean onTouchEvent(MotionEvent event) {
// 從這裏能夠看出來, 即便當view 處於 DISABLED 不可用的狀態時, 它依然能夠消耗點擊事件
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them. return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE); } // 若是 View 設置有代理, 那麼會執行 mTouchDelegate.onTouchEvent(event), 工做機制相似 onTouchListener if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } // 此處能夠看出來只要 clickable 和 long_clickable 有一個爲 true , 就能夠消費這個事件 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) { // take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
// 這裏內部會調用 onClick() (若是設置了 mOnClickListener)
performClick();
}
}
}
return true;
}
return false;
}
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
// 這裏能夠看到若是設置了 mOnClickListener, 則會調用 onClick()
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;
}
複製代碼
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
複製代碼