基礎
在分析事件分發之前,我們先來了解三個相關的重要知識點:
- 事件分發的對象是什麼?
- 誰在分發?
- 依賴於什麼分發?
我們先把這三個問題弄清楚了再來看具體的原理:
-
事件分發的對象就是我們的觸摸屏幕產生的
Touch
事件。最常見的事件類型爲:down、up、move和cancel。-
MotionEvent.ACTION_DOWN
:觸摸到屏幕所立即產生的事件類型 -
MotionEvent.ACTION_UP
:離開屏幕所產生的事件類型 -
MotionEvent.ACTION_MOVE
:在屏幕上移動所產生的事件類型 -
MotionEvent.ACTION_CANCEL
:事件被取消,非人爲的取消,比如:關機、鎖屏
-
-
事件分發是在
Activity
、ViewGroup
和View
三者之間傳遞的過程,事件先傳遞到Activity
,接着傳遞到ViewGroup
,最後傳給了View
。如下圖 ① -> ② -> ③:
-
事件分發主要就是通過三個方法在上述三者之間傳遞:
-
dispatchTouchEvent(ev: MotionEvent?): Boolean
:分發、派發事件的方法 -
onInterceptTouchEvent(ev: MotionEvent?): Boolean
:攔截事件的方法 -
onTouchEvent(event: MotionEvent?): Boolean
:消費事件的方法
通過下方的表格來認識一下三個方法和三者之間的擁有關係:
dispatchTouchEvent() onInterceptTouchEvent() onTouchEvent() Activity 可以分發 無攔截方法 可消費 ViewGroup 可以分發 有攔截方法 可消費 View 可以分發 無攔截方法 可消費 理解了上面三個問題之後,我們就可以放心大膽的去通過源碼去了解事件分發的原理了。
-
入口 Activity
首先看看 Activity
的 dispatchTouchEvent()
方法:
/**
* 事件分發的入口,可以override
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
// 如果事件類型爲down,那麼先執行一遍onUserInteraction()
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// getWindow()是獲取Window對象,但是Window是抽象類,唯一實現類爲PhoneWindow
// 可直接查看PhoneWindow.superDispatchTouchEvent(ev)方法
// 順藤摸瓜下去就可以看到實際上調用的是ViewGroup.dispatchTouchEvent(ev)方法
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
/**
* 用於和用戶交互
* 此方法爲空方法,可override
*/
public void onUserInteraction() {
}
/**
* 當前Activity下的任何一個View都沒有處理點擊事件,就會執行此方法
* 此方法用於處理Window邊界外的點擊事件
*/
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
Activity
的事件分發代碼還是比較簡潔的,我們大致理一下流程:
- 接收到
down
事件,執行onUserInteraction()
; - 如果
ViewGroup.dispatchTouchEvent(ev)
返回爲true
,那麼就直接返回true
,結束; - 如果
ViewGroup.dispatchTouchEvent(ev)
返回爲false
,調用自身的onTouchEvent(ev)
方法,結束。
結合下方的流程圖更容易理解:
中間人ViewGroup
理解了 Activity
對事件的處理之後,我們趁熱打鐵來分析一下 ViewGroup
對事件的分發、攔截和消費,因爲它涉及到了三個方法,可想而知難度會提升一點。
來看看 ViewGroup
是如何分發、攔截和消費事件的:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 標誌是否分發了此點擊事件
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// 判斷是否攔截此點擊事件
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// disallowIntercept = 是否禁用事件攔截的功能(默認是false),可通過調用requestDisallowInterceptTouchEvent()修改
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// 可以攔截
if (!disallowIntercept) {
// 調用攔截事件方法,默認返回false
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;
}
// 如果沒有被取消而且沒有被自身攔截,那就向下(子View)分發
if (!cancel && !intercepted) {
// 循環每個子View,調用子View的dispatchTouchEvent
for (int i = childrenCount - 1; i >= 0; i--) {
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// 實際就是調用子View的dispatchTouchEvent(ev)
if(dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){
// 如果子View分發成功,那麼將child賦值到mFirstTouchTarget對象的next指針中
// mFirstTouchTarget是TouchTarget對象的引用,TouchTarget類似鏈表結構
addTouchTarget(child,idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
}
}
}
// 如果mFirstTouchTarget爲空,說明沒有執行上面的addTouchTarget(child,idBitsToAssign)方法
// 那就代表事件要麼被取消了,要麼被攔截了,這時候dispatchTransformedTouchEvent()第三個參數傳的是null,會在下面介紹
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}else{
// alreadyDispatchedToNewTouchTarget在上面循環的時候,如果分發成功,它就爲true,handled就爲true
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
}
}
return handled;
}
}
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) {
// 如果子View爲空,那麼就調用super.dispatchTouchEvent(ev) == View.dispatchTouchEvent(ev)
// ViewGroup是View的子類,所以在super中的事件將被ViewGroup代替
handled = super.dispatchTouchEvent(event);
} else {
// 子View不爲空,調用View.dispatchTouchEvent(ev)
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
}
/**
* 可覆寫,返回true表示攔截,false表示不攔截
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
上面的註釋一定要仔細看,看完如果還有點不理解,沒關係,我們接着理一下 ViewGroup
的整體思路:
- 在
dispatchTouchEvent()
方法中,首先執行onInterceptTouchEvent(ev)
方法,看是否被攔截; - 如果被攔截,那麼就不分發事件到子
View
的dispatchTouchEvent(ev)
,而是分發到super.dispatchTouchEvent()
,由於ViewGroup
的父類就是View
,所以還是會執行View.dispatchTouchEvent(ev)
方法,這裏先不急着弄懂這步,只要知道流程就行,將會在下節介紹; - 如果沒有被攔截,直接循環子
View
,調用子View
的dispathcTouchEvent(ev)
方法。
到這裏大致的流程就清晰了,再結合下方的流程圖加深下理解:
最後接收人View
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
ListenerInfo li = mListenerInfo;
// 監聽信息不爲空
// OnTouchListener不爲空,也就是setOnTouchListener()
// 當前view的enable爲true
// OnTouchListener接口中的boolean onTouch(View v, MotionEvent event)方法需要覆寫
// 缺一不可
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 如果上面不滿足,那麼看onTouchEvent方法是否返回true,如果返回true,那麼result也爲true,否則爲false
// 其實這個if()也就等於:return onTouchEvent(event),仔細體會下
if (!result && onTouchEvent(event)) {
result = true;
}
return result;
}
public boolean onTouchEvent(MotionEvent event) {
// 只要設置了單擊和長按事件,clickable就爲true
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
// 調用點擊事件
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
return true;
}
return false;
}
View
的事件分發相當於 ViewGroup
就簡單多了,流程在源碼中也體現的很清楚,我們來理一下整個流程:
- 執行到
dispatchTouchEvent(ev)
的時候,先去調用onTouch(view,ev)
方法; - 如果
onTouch()
返回false
,纔去調用下面的onTouchEvent(ev)
方法; - 在
onTouchEvent(ev)
方法內部,可以看見執行了performClickInternal()
方法,這個方法就是我們常見的onClick()
以上三點都是需要滿足種種條件纔可執行的,條件都在源碼註釋中詳細解釋了,大家一定要把這些條件看清楚了。
到這裏我們需要解決在 ViewGroup
中留下的一個問題,那麼就是當 ViewGroup
沒有子 View
的時候,調用了一個 super.dispatchTouchEvent(ev)
方法,其實就是把 ViewGroup
的 onTouch()
、onTouchEvent()
和 onClick()
三個方法按照條件來執行,千萬不要以爲調用父類的方法還是執行子 View
的事件分發!
View
的事件分發主要流程參考下方的流程圖:
事件分發的流程說難吧其實理清了也不難,說不難吧還是挺繞的,最後總結下幾點:
事件分發的順序是:
Activity
->ViewGroup
->View
;Activity
和View
都沒有攔截事件,Activity
如果存在攔截事件,那麼整個頁面都響應不了點擊事件了,View
因爲是最後一層,攔不攔截都沒必要了;ViewGroup
在無子View
接收分發事件或子View
分發事件返回false
的時候,會調用自身的onTouch
、onTouchEvent()
和onClick()
方法。
大家一定要好好體會Android事件分發機制,無論是在日常開發中還是面試中,都是必不可缺的知識點!源碼分析的文章還在不斷的更新,如果本文章你發現有不正確或者不足之處,歡迎你在下方留言或者掃描下方的二維碼留言也可!