前言
Android
源碼分析之View
系列之事件分發(fā)機制
正文
一. 概述
View
的觸摸事件分發(fā)是View
系列中的一個重難點, 主要需要掌握的是MotionEvent
的傳遞規(guī)則和處理規(guī)則, 這是自定義View
中沖突處理的理論來源~
觸摸事件分發(fā)的處理主要是對MotionEvent
的處理, MotionEvent
封裝了用戶的一系列行為, 如: ACTION_DOWN
(手指剛觸摸屏幕), ACTION_MOVE
(手指在屏幕上滑動), ACTION_UP
(手指抬起)等; 以及事件發(fā)生的坐標(通過MotionEvent.getX()
, MotionEvent.getY()
可以得到)等
在開始講解之前需要明確的一些概念是:
一個事件序列: 指的是一次完整的觸摸過程, 即從
ACTION_DOWN
(手指觸摸屏幕)開始, 到中間的一系列ACTION_MOVE
(手指滑動), 最后到ACTION_UP
為止(手指抬起); 總結(jié)起來就是down...move...move..up
觸摸事件的分發(fā)其實是一個從上到下不斷遞歸傳遞和攔截的過程; 一個大致的傳遞流程是:
Activity
-->Window
-->ViewGroup
-->View
, 當然如果向下傳遞但是MotionEvent
又沒有消耗的話, 又會逐層返回, 最終將沒有消耗的MotionEvent
交給Activity
處理
二. 事件分發(fā)之源
觸摸事件產(chǎn)生和分發(fā)的源頭是在Activity
中處理的, 即在Activity
的dispatchTouchEvent()
中; 如下; 處理思路也很簡單, 只是單純的向下分發(fā)而已, 如果事件沒有得到處理, 那么最終就交給Activity
的onTouchEvent()
處理; 另外, 這里還為用戶提供了一個監(jiān)聽和攔截事件的方法, 即onUserInteraction()
, 該方法在Activity
中是一個空實現(xiàn), 可以重寫該方法在事件向下傳遞之前進行特殊攔截和處理
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction(); // 自定義事件攔截
}
if (getWindow().superDispatchTouchEvent(ev)) { // 通過Window向下分發(fā)事件
return true;
}
return onTouchEvent(ev); // 如果事件最終沒有被處理, 那么交給Activity自己的onTouchEvent()來處理
}
Activity
中的Window
實際上是PhoneWindow
, 這里通過PhoneWindow.superDispatchTouchEvent()
傳遞實際上是只是簡單調(diào)用了mDecor.superDispatchTouchEvent(event)
, 而這里的mDecor
實際上是DecorView
, 是一個FrameLayout
(ViewGroup
), 在DecorView
的superDispatchTouchEvent()
方法中, 也只是簡單的將事件傳遞給ViewGroup
進行分發(fā)(即ViewGroup.dispatchTouchEvent()
); 到這里就將事件傳遞給ViewGroup
和View
處理了, 也是事件分發(fā)處理中最主要的一部分
三. ViewGroup分發(fā)事件
ViewGroup.dispatchTouchEvent()
中對事件的分發(fā)處理過程比較長, 實際上大致分成了三個部分來處理
3.1 事件攔截
首先, ViewGroup
會判斷是否進行事件攔截, 如下; 從后面將事件分發(fā)給子View
的條件可以看出, 如果ViewGroup
進行了事件攔截, 那么該事件序列將不再向下分發(fā); 這里還需要注意的一點是, ViewGroup
判斷是否進行事件攔截的條件一個是為ACTION_DOWN
時, 另一個是mFirstTouchTarget != null
時; 也就是說一個事件序列的在開始時, 即ACTION_DOWN
時一定會調(diào)用ViewGroup
的onInterceptTouchEvent
(當然, 還有一個影響因素是FLAG_DISALLOW_INTERCEPT
, 我們稍后講解); 至于mFirstTouchTarget
的賦值是在后面分發(fā)給子View
時, 如果有子View
處理了事件那么mFirstTouchTarget
將會被賦值;
上面是ViewGroup
進行事件攔截的基本思路, 簡單總結(jié)起來就是:
ACTION_DOWN
時, 如果ViewGroup
進行了事件攔截(onInterceptTouchEvent()
返回true
), 那么同一事件序列將不再向下分發(fā)(因為之后的ACTION_MOVE
和ACTION_UP
到來時, 由于之前ACTION_DOWN
時進行了事件攔截,mFirstTouchTarget
沒有機會賦值, 所以仍然為null
, 故直接走else
語句, 即intercepted = true
);ACTION_DOWN
時, 如果ViewGroup
不進行事件攔截, 并且在事件向下分發(fā)時, 有子View
處理了事件, 那么mFirstTouchTarget
將會被賦值, 即不為null
, 此時仍然會繼續(xù)調(diào)用ViewGroup
的onInterceptTouchEvent
判斷是否進行事件攔截, 需要注意的是此時仍然在同一事件序列中ACTION_DOWN
時, 如果ViewGroup
不進行事件攔截, 并且在事件向下分發(fā)時, 也沒有子View
進行事件處理, 那么mFirstTouchTarget
仍為null
, 即走else
, 交由ViewGroup
處理事件
注: 只有當ViewGroup
攔截了事件或者子View
不處理事件時, onInterceptTouchEvent
才只會調(diào)用一次
// ViewGroup是否進行事件攔截
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;
}
if (!canceled && !intercepted) { // 如果攔截事件, 將不再分發(fā)給子View
// 事件分發(fā)給子View
....
}
另外, 上面還講了, 在ACTION_DOWN
時, 一定會調(diào)用ViewGroup
的onInterceptTouchEvent
, 這里還有一個影響因素是標志位FLAG_DISALLOW_INTERCEPT
, 該標志位是通過requestDisallowInterceptTouchEvent()
設(shè)置的, 作用是在子View
中強制父ViewGroup
不進行事件攔截, 但是該標志位不能影響ACTION_DOWN
, 因為在一個事件序列開始之前會先進行狀態(tài)重置, 如下; 在resetTouchState()
中會將該標志位重置, 所以就不會影響ACTION_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);
resetTouchState(); // 狀態(tài)重置
}
3.2 事件分發(fā)
如果ViewGroup
不進行事件攔截的話, 會將事件分發(fā)給子View
處理; 事件分發(fā)的主要代碼如下; 邏輯也比較簡單, 就是遍歷所有的子View
, 然后通過dispatchTransformedTouchEvent()
進行將事件傳遞給子View
if (!canceled && !intercepted) {
...
for (int i = childrenCount - 1; i >= 0; i--) {
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign); // 設(shè)置mFirstTouchTarget的值
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
...
}
我們繼續(xù)來看dispatchTransformedTouchEvent()
的處理過程, 如下; 從上面的代碼中我們可以看出, 將事件分發(fā)給子View
的時候, 調(diào)用dispatchTransformedTouchEvent()
傳入的child
非空, 所以應(yīng)該調(diào)用的是child.dispatchTouchEvent(event)
, 這樣就將事件傳遞到子View
中去了; 這里關(guān)于子View
的dispatchTouchEvent()
處理在后文繼續(xù)講解
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event); // child非null
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event); // 調(diào)用child.dispatchTouchEvent(event)
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
...
}
上面我們說過, 如果子View
處理了事件的話, 將會去設(shè)置mFirstTouchTarget
的值, 該值的設(shè)置其實是在addTouchTarget()
中, 也就是說, 當dispatchTransformedTouchEvent()
返回true
, 即有子View
處理了事件的話, 就會去調(diào)用該函數(shù), 也就證明了我們前面所說的; 我們來看addTouchTarget()
, 如下; 可以看出這里實際上相當于一個單鏈表
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target; // 設(shè)置mFirstTouchTarget
return target;
}
3.3 ViewGroup處理事件
如果ViewGroup
攔截了事件或者子View
沒有進行事件處理, 那么ViewGroup
將進行事件處理, 如下; 可以看出, ViewGroup
進行事件處理也是調(diào)用dispatchTransformedTouchEvent()
, 只是傳入的child
為null
, 那么從上面的dispatchTransformedTouchEvent()
代碼中我們可以看出, 如果child
為null
調(diào)用的應(yīng)該就是super.dispatchTouchEvent(event)
進行事件處理
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
需要注意的是不管是super.dispatchTouchEvent(event)
還是child.dispatchTouchEvent(event)
, 調(diào)用的其實都是View.dispatchTouchEvent()
, 所以接下來我們要看的就是View
中對事件的處理
四. View事件處理
需要注意的是, View
中沒有onInterceptTouchEvent()
方法來進行事件攔截; 我們這里關(guān)注的, 主要是View
對事件的處理, 這里的View
包括ViewGroup
進行事件攔截之后對事件的處理以及子View
對事件的處理; 因為從前面我們已經(jīng)說了, 不管是調(diào)用的super.dispatchTouchEvent()
(ViewGroup
處理事件)還是child.dispatchTouchEvent()
(子View
處理事件), 其實都是調(diào)用的View.dispatchTouchEvent()
; 所以二者對事件的處理實際上是一樣的, 同時需要注意的是, 這一節(jié)不包括事件的分發(fā)了,
事件分發(fā)在上一節(jié)中已經(jīng)講解完啦~
觸摸事件的處理主要涉及到OnTouchListener
, onTouchEvent
和onClick
的處理優(yōu)先級
主要代碼如下; 可以看出先處理的是OnTouchListener
, 如果View
沒有設(shè)置OnTouchListener
(View.setOnTouchListener()
)的話, 再去處理onTouchEvent()
, 所以OnTouchListener
的優(yōu)先級比onTouchEvent
高; 同時還要注意的一點是, 如果設(shè)置了OnTouchListener
的話, View
的onTouchEvent
將不再調(diào)用
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) { // OnTouchListener
result = true;
}
if (!result && onTouchEvent(event)) { // onTouchEvent
result = true;
}
...
}
這里還有一個onClick()
其實是在onTouchEvent()
中處理的; 如下; onClick
是在performClickInternal()
中觸發(fā)的, 可以看出, 要觸發(fā)onClick
需要的條件是: View
是可以點擊的(clickable
), 這里的可點擊包括了CLICKABLE
和LONG_CLICKABLE
, 注意View
的enable
屬性不影響onTouchEvent
的返回值, 只要它可點擊, 那么onTouchEvent()
就會處理該點擊事件
public boolean onTouchEvent(MotionEvent event) {
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
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)) {
performClickInternal();
}
}
}
...
}
}
而在performClickInternal()
中, 則是去調(diào)用了performClick()
進行處理, 在performClick()
會判斷, 如果設(shè)置了OnClickListener
, 則會去調(diào)用OnClickListener
, 代碼比較簡單, 就不貼啦~
五. 總結(jié)
到這里, View
的事件分發(fā)和處理流程就分析結(jié)束啦~; 我們最開始講事件分發(fā)之源時講Activity
對事件的傳遞的時候, 如果getWindow().superDispatchTouchEvent()
返回false
的話, 就最終將事件交給Activity
的onTouchEvent()
處理, 這種情況其實對應(yīng)的是ViewGroup
和View
都不進行事件處理, 那么就逐層回傳咯~
最后將上述流程總結(jié)為下圖: