Android
事件傳遞流程在網上可以找到很多資料,FrameWork
層輸入事件和消費事件,可以參考:
- [Touch事件派發過程詳解] 1
這篇blog闡述了底層是如何處理屏幕輸,並往上傳遞的。Touch
事件傳遞到Activity
的DecorView
時,往下走就是ViewGroup
和子View
之間的事件傳遞,可以參考郭神的這兩篇博客
郭神的兩篇博客清楚明白地說明了View
之間事件傳遞的大方向,但是具體的一些晦暗的細節闡述較少,本文主要是總結這兩篇博客的同時,側重於兩點:
- 事件分發過程中一些細節到底如何實現的?
- 子
view
到底如何和父View
搶事件,父View
又是如何攔截事件不發送給子View
,以及如果我們需要處理這種混亂的關系才能讓兩者和諧相處?。
MotionEvent抽象
要明白View
的事件傳遞,很有必要先說一下Touch
事件是如何在Android
系統中抽象的,這主要使用的就是MotionEvent
。這個類經歷了幾次重大的修改,一次是在2.x版本支持多點觸摸,一次是4.x將大部分代碼甩給native
層處理。
一次簡單的事件
我們先舉個栗子來說明一次完整的事件,用戶觸屏 滑動 到手機離開屏幕,這認為是一次完整動作序列(movement traces
)。一個動作序列中包含很多動作Action
,比如在用戶按下時,會封裝一個MotionEvent
,分發給視圖樹,我們可以通過motionevent.getAction
拿到這個動作是ACTION_DOWN
。同樣,在手指抬起時,我們可以接收到Action
類型是Action_UP
的MotionEvent
。對於滑動(MOVE
)這個操作,Android
為了從效率出發,會將多個MOVE
動作打包到一個MotionEvent
中。通過getX getY
可以獲取當前的坐標,如果要訪問打包的緩存數據,可以通過getHistorical**()
函數來獲取。
加入多點觸摸
對於單點的操作來看,MotionEvent
顯得比較簡單,但是考慮引入多點觸摸呢?我們定義一個接觸點為(Pointer
)。我們從onTouch
接受到一個MotionEvent
,怎么拿到多個觸碰點的信息?為了解開筆者剛開始學習這部分知識時的困惑,我們首先樹立起一種概念:一個MotionEvent
只允許有一個Action
(動作),而且這個Action
會包含觸發這次Action
的觸碰點信息,對於MOVE
操作來說,一定是當前所有觸碰點都在動。只有ACTION_POINTER_DOWN
這類事件事件會在Action
里面指定是哪一個POINTER
按下。
在MotionEvent
的底層實現中,是通過一個16位來存儲Action
和Pointer
信息(PointerIndex
)。低8位表示Action
,理論上可以表示255種動作類型;高8位表示觸發這個Action
的PointerIndex
,理論上Android
最多可以支持255點同時觸摸,但是在上層代碼使用的時候,默認多點最多存在32個,不然事件在分發的時候會有問題。
MotionEvent
中多個手指的操作API
大部分都是通過pointerindex
來進行的,如:獲取不同Pointer
的觸碰位置,getX(int pointerIndex)
;獲取PointerId
等等。大部分情況下,pointerid == pointeridex
。
ACTION_DOWN
OR ACTION_POINTER_DOWN
:
這兩個按下操作的區別是ACTION_DOWN
是一個系列動作的開始,而ACTION_POINTER_DOWN
是在一個系列動作中間有另外一個觸碰點觸碰到屏幕。
這部分詳細的描述,請參考:
android觸控,先了解MotionEvent
到這里,鋪墊終於結束了,我們開始直奔主題。
View的事件傳遞
Android
的Touch
事件傳遞到Activity
頂層的DecorView
(一個FrameLayout
)之后,會通過ViewGroup
一層層往視圖樹的上面傳遞,最終將事件傳遞給實際接收的View
。下面給出一些重要的方法。如果你對這個流程比較熟悉的話,可以跳過這里,直接進入第二部分。
dispatchTouchEvent
事件傳遞到一個ViewGroup
上面時,會調用dispatchTouchEvent
。代碼有刪減
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Attention 1 :在按下時候清除一些狀態
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
//注意這個方法
resetTouchState();
}
// Attention 2:檢查是否需要攔截
final boolean intercepted;
//如果剛剛按下 或者 已經有子View來處理
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 {
// 不是一個動作序列的開始 同時也沒有子View來處理,直接攔截
intercepted = true;
}
//事件沒有取消 同時沒有被當前ViewGroup攔截,去找是否有子View接盤
if (!canceled && !intercepted) {
//如果這是一系列動作的開始 或者有一個新的Pointer按下 我們需要去找能夠處理這個Pointer的子View
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
//上面說的觸碰點32的限制就是這里導致
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
//對當前ViewGroup的所有子View進行排序,在上層的放在開始
final ArrayList<View> preorderedList = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
// canViewReceivePointerEvents visible的View都可以接受事件
// isTransformedTouchPointInView 計算是否落在點擊區域上
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//能夠處理這個Pointer的View是否已經處理之前的Pointer,那么把
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;
} }
//Attention 3 : 直接發給子View
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);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}
// 前面已經找到了接收事件的子View,如果為NULL,表示沒有子View來接手,當前ViewGroup需要來處理
if (mFirstTouchTarget == null) {
// ViewGroup處理
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
if(alreadyDispatchedToNewTouchTarget) {
//ignore some code
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
}
return handled;
}
上面代碼中的Attention
在后面部分將會涉及,重點注意。
這里需要指出一點的是,一系列動作中的不同Pointer
可以分配給不同的View
去響應。ViewGroup會維護一個PointerId
和處理View
的列表TouchTarget
,一個TouchTarget
代表一個可以處理Pointer
的子View
,當然一個View
可以處理多個Pointer
,比如兩根手指都在某一個子View
區域。TouchTarget
內部使用一個int
來存儲它能處理的PointerId
,一個int
32位,這也就是上層為啥最多只能允許同時最多32點觸碰。
看一下Attention 3
處的代碼,我們經常說view
的dispatchTouchEvent
如果返回false,那么它就不能系列動作后面的動作,這是為啥呢?因為Attention 3
處如果返回false
,那么它不會被記錄到TouchTarget
中,ViewGroup認為你沒有能力處理這個事件。
這里可以看到,ViewGroup
真正處理事件是在dispatchTransformedTouchEvent
里面,跟進去看看:
dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
//沒有子類處理,那么交給viewgroup處理
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}
可以看到這里不管怎么樣,都會調用View
的dispatchTouchEvent
,這是真正處理這一次點擊事件的地方。
dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
if (onFilterTouchEventForSecurity(event)) {
//先走View的onTouch事件,如果onTouch返回True
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
設置的onTouch
事件處在一個較高的優先級,如果onTouch
執行返回true
,那么就不會去走view
的onTouchEvent
,而我們一些點擊事件都是在onTouchEvent
中處理的,這也是為什么onTouch
中返回true,view
的點擊相關事件不會被處理。
小小總結一下這個流程
ViewGroup
在接受到上級傳下來的事件時,如果是一系列Touch
事件的開始(ACTION_DOWN
),ViewGroup
會先看看自己需不需要攔截這個事件(onInterceptTouchEvent
,ViewGroup
的默認實現直接返回false
表示不攔截),接着ViewGroup
遍歷自己所有的View
。找到當前點擊的那個View
,馬上調用目標View
的dispatchTouchEvent
。如果目標View
的dispatchTouchEvent
返回false,那么認為目標View
只是在那個位置而已,它並不想接受這個事件,只想安安靜靜的做一個View
(我靜靜地看着你們裝*)。此時,ViewGroup
還會去走一下自己dispatchTouchEvent,Done!
子View和父View的撕*大戰
終於來到本文的重要環節,子View和父布局(ViewGroup
)是如何撕逼的。我們經常遇到這樣的問題:在ListView
中放一個ViewPager
不能滑動的問題,其實這里就會涉及到子View和布局之間的協商,事件處理到底你上還是我上。
首先需要明確一點的是,一個事件肯定是由ViewGroup
傳遞給自己的子View
的,所以ViewGroup
具有絕對的權威來禁止事件往下傳,這就是onInterceptTouchEvent
方法。可以看上面ViewGroup中的dispatchTouchEvent
的Attention 1
和 Attention 2
。
先看Attetion2
:
進行判斷有有兩個條件:1,如果是一次新的事件 or 在一次事件中但是已經有子View來處理這個事件,那么父類需要去看看是否攔截這次事件。否則,直接攔截(此時處於一系列動作的中間,而且沒有子view來接盤,那么ViewGroup就直接攔下來)。
決定是否攔截有兩個步驟,
disallowIntercept
是否駁回攔截,默認false
。注意這個值是子View
和撕*的關鍵,因為ViewGroup
開放了給這個標記賦值的接口requestDisallowInterceptTouchEvent()
,而且這個方法直接往上遞歸,這個ViewGroup
的各級父容器都會設置駁回攔截。onInterceptTouchEvent
雖然ViewGroup
中默認返回false,但是在很多有滑動功能的ViewGroup
里面(如scrollview ListView
等)會處理各種情況,決定是否攔截這個事件,所以就會出現之前說的ListView
中的Viewpager
不能滑動的問題,原因是事件被父View攔截了。
在Attetion1
的位置如果是一次新的ACTION_DOWN
,那么會把之前事件傳遞設置的各種狀態清除。
對ViewGroup來說需要做什么
對於一個需要攔截事件的ViewGroup
,它通常都有一些特殊的操作,比如ScrollView
,比如ViewPager
,它重寫onInterceptTouchEvent
是非常關鍵的,這也是能和子View
和諧相處的關鍵。舉個例子,我自己定義了一個ViewGroup
:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
return true;
}
return super.onInterceptTouchEvent(ev);
}
這樣會發生什么?
所有位於MyViewGroup中的子View收不到任何的事件,原因可以看一下Attention2
的代碼,判斷是否攔截是在系列動作按下時會進行判斷,如果此時攔截,那么直接不會去查找相應處理的子View,所以touchtarget
為空,那么接下來的動作都直接被ViewGroup
笑納。
所以哪怕再強勢的ViewGroup
,一般都是在Down
的時候給子類機會去掉用requestDisallowInterceptTouchEvent
,如設置駁回攔截,那么在ViewGroup分發事件的時候,會跳過onInterceptTouchEvent
的執行。
子View需要做什么
對於子View來說,在合適的時機調用requestDisallowInterceptTouchEvent
即可。當然啥時候合適?對於一個View
來說,那就是在dispatchTouchEvent
或者onTouchEvent
來調用。
對於ViewGroup
來說,通常我們會在onInterceptTouchEvent
進行判斷。比如我們經常會遇到在ListView
里面套了ViewPager
導致ViewPager
不能滑動的問題,通常的處理方式:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (absListView != null) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = event.getX();
mDownY = event.getY();
//ACTION_DOWN的時候,趕緊把事件hold住
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if(Math.abs(event.getX() - mDownX)>Math.abs(event.getY()-mDownY)) {
getParent().requestDisallowInterceptTouchEvent(true);
}else {
//發現不是自己處理,還給父類
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
//其實這里是多余的
getParent().requestDisallowInterceptTouchEvent(false);
}
}
return super.onInterceptTouchEvent(event);
}
總結
本來打算寫一個短篇的,結果一個不小心,弄成了長篇大論。
最后需要注意一點的是,所有我們上述討論的內容都是在一層層遞歸中進行,而且requestDisallowInterceptTouchEvent
這個函數也是遞歸調用的。
我們可以認為ViewGroup
是一個具有絕對話語權但是從不專政的霸道總裁,它自己可以攔截處理某些事件,比如Viewpager
的橫滑,但是它也可以給子View足夠的空間去要求這個事件給自己處理。作為一名開發者,一方面在自己定義ViewGroup
時需要考慮能夠給子View足夠空間中斷自己的攔截;一方面自己定義View時,我們需要在合適的時候跟父View索要事件。ViewPager(新版)
作為容器來說,它需要攔截橫滑事件,同時,自己具備了和父View
爭搶事件的能力,所以不管把ViewPager
放到什么布局中,它都能正確處理。看看它的onInterceptTouchEvent
怎么寫的吧,完美的體現了這一思想。