事件分發是Android中的一個重點也是一個難點,在自定義控件中很是常用。前後看了好多書和部落格,感覺寫的東西順序都稍微有些不對,讓剛接觸的人看起來不是很好懂。在這裡也是将我從不清楚到熟悉的過程寫下來,希望對大家有所幫助,對自己也起到總結的作用。
下面介紹幾個方法,隻要先有個印象就好,以後會慢慢解釋:
首先是
dispatchTouchEvent(MotionEvent ev)
分發事件,傳回true表示事件被目前的View或其子View消耗,傳回false表示事件沒被消耗。
onInterceptTouchEvent(MotionEvent ev)
是否攔截某個事件,傳回true表示攔截,傳回false表示不攔截
onTouchEvent(MotionEvent ev)
處理點選事件,傳回true表示事件被目前的ViewGroup/View消耗掉,傳回false表示沒消耗
任玉剛大神書中的一段代碼對這個邏輯闡述的非常清楚,下面是這段僞代碼:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
先簡單的了解上面的代碼,在源碼的分析中将會對代碼進行進一步分析,而後給出結論。
源代碼分析:
當一個點選事件發生時,最先接收到事件的是Activity的dispatchTouchEvent方法,下面是相應源碼:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
如果動作是ACTION_DOWN的話,會回調onUserIteraction方法,這個和我們關系不大。
我們先暫且認為getWindow().superDispatchTouchEvent(ev)是調用子View分發事件的方法,當子View沒有消耗掉這個事件(也就是子View的dispatchTouchEvent傳回false)時,Activity就會調用自身的onTouchEvent方法來處理這個事件。
進入getWindow().superDispatchTouchEvent(ev)發現這是一個抽象的方法,我們知道Window的實作類是PhoneWindow,檢視PhoneWindow的相應方法可以看到其調用了DecorView的superDispatchTouchEvent,在以前的學習中我們知道在setContentView中我們設定的View就是DecorView的子View,是以點選事件一定能傳遞到子View,而一般的子View就是ViewGroup。根據本文開始的分析,進入時應該調用的是dispatchTouchEvent方法,我們現在來看一下ViewGroup中dispatchTouchEvent方法的源碼:
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
在方法開始的時候有上述的代碼片段,注釋上寫的很明确,要清除之前的所有狀态,具體要清除的是什麼狀态呢?先接着看代碼:
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;
}
出現了一個mFirstTouchTarget變量,它代表的是什麼意思呢?在後文中會有對mFirstTouchTarget的說明,但是這裡為了解釋清楚,不妨先透露一下,如果目前View将事件分發到子View,那麼mFirstTouchTarget将被指派。
還有一個符号位FLAG_DISALLOW_INTERCEPT,看名字也應該可以猜出來,它的作用是不要攔截目前的事件,那和這個符号位進行與操作的mGroupFlags又是誰設定的?不難猜出,應該是子View設定的為了讓目前View不去攔截事件。
現在重新審視我們剛才寫過的内容,在進入dispatchTouchEvent方法時清除狀态所清除的内容也就很好了解了:
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
很清楚,将mGroupFlags的狀态清除掉了,同時将mFirstTouchTarget置為null。
現在我們再來看給intercepted指派的代碼塊,不難得出下面這幾個結論:
1.每次ACTION_DOWN發生的時候,都會回調onInterceptEvent方法(因為每次ACTION_DOWN發生的時候狀态都會清除)。
2.一旦目前View将ACTION_DOWN事件攔截後,mFirstTouchTarget仍然為空,那麼在一個事件序列(一個ACTION_DOWN,中間任意多個ACTION_MOVE,一個ACTION_UP)内不會再調用onInterceptTouch方法進行判斷,intercepted變量始終為true,也就是所事件一直被目前View所攔截。
3.若ACTION_DOWN不攔截,那麼在一個事件序列内不管攔截了什麼動作,在下一個事件到來時都要回調onInterceptTouchEvent方法。
4.子View可以通過設定mGroupFLags值的方式來控制目前View攔截事件,但是ACTION_DOWN事件不被控制。
if (!canceled && !intercepted) {
final int childrenCount = mChildrenCount;
if (childrenCount != 0) {
// Find a child that can receive the event.
// Scan children from front to back.
final View[] children = mChildren;
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
final boolean customOrder = isChildrenDrawingOrderEnabled();
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder ?
getChildDrawingOrder(childrenCount, i) : i;
final View child = children[childIndex];
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
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;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
mLastTouchDownIndex = childIndex;
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
這裡删除了一些不必要的代碼。我們仔細觀察一下最後一個if語句塊,可以發現,當 dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法傳回true的時候for循環即可退出,下面是這個方法代碼中有用的片段:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
可以看出目前的child不為空,那麼其調用了child的dispatchTouchEvent方法。如果目前所有的子View的dispatchTouchEvent都傳回的是false,或者目前沒有子View,那麼for循環就會跳出,執行如下的代碼:
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
這次傳入dispatchTransformedTouchEvent方法的參數是null,由上面的代碼可以看出将調用父View的dispatchTouchEvent方法。這裡的傳回值是handled,也就是子View的dispatchTouchEvent傳回值或者目前View的處理,也說明了開篇時僞代碼的正确性。
前面提到了mFirstTouchTarget,那麼這個參數是在哪指派的呢?仔細看dispatchTransformedTouchEvent傳回true時進入的if代碼塊,調用了一個函數是addTouchTarget(child, idBitsToAssign),其中的代碼如下:
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
在其中完成了對mFirstTouchTarget的指派。
至此我們完成了整個ViewGroup的分析過程,如果仔細觀察不難得出結論,目前View如果将事件分發給子View來進行處理,但是子View的dispatchTouchEvent方法傳回false的話,那麼目前View将會處理這個事件。同時不難用遞歸的思想來想這個問題,如果目前的View的dispatchTouchEvent也傳回false呢,那麼目前View的上一級View就會進行處理,而最後不難發現事件将會有Activity中的dispatchTouchEvent方法進行消耗,也就是調用了Activity中的onTouchEvent,如果忘記了可以去看最開始貼出的Activity源碼。
分析過了整個ViewGroup的分發過程,現在到了整個事件分發的最底端,也就是View的分發過程,同樣的,我們先來看一下dispatchTouchEvent中的代碼:
<span style="font-size:18px;">public boolean dispatchTouchEvent(MotionEvent event) {
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
return true;
}
if (onTouchEvent(event)) {
return true;
}
}
return false;
}</span>
整個方法很簡單,if的判斷條件過濾了所有View不響應的狀态,我們看if代碼塊内的内容。值得注意的是,當為View設定了OnTouchListener後,并且重寫的onTouch方法傳回true的時候,整個方法直接傳回了。換句話說就是onTouchEvent方法并不會執行,這點要注意。另外,注意到整個dispatchTouchEvent方法的傳回值也就是onTouch或者onTouchEvent的傳回值,代表了目前的事件傳遞到了View是否被消耗了,如果并沒有被消耗,根據上文中我們得出的結論,那麼上一級View的dispatchTouchEvent就會被調用,這個關系可以依次向上級傳遞。
上面的方法中調用了onTouchEvent,我們現在來看一看這個方法的預設實作:
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == 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));
}
剛進入方法時來了一段這樣的片段,不難了解出它的意思。在目前的View狀态為DISABLE時,隻要它是可點選的,或者是可以長點選的話,onTouchEvent就會傳回true并且将目前的事件消費掉,但是并不響應這個事件。
繼續向後來閱讀:
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
看到了這樣一個代碼塊,如果目前的View設定有TouchDelegate(代理,一般用于增加點選的範圍,有興趣的可以自行了解一下)的話,那麼事件是否被消耗掉取決于代理onTouchEvent的傳回值。
而後面的代碼比較多也和事件分發無關,如果感興趣可以去讀一讀。值得注意的是,在目前的View的Clickable或LongClickable至少有一個為真時,onTouchEvent就會傳回true,也就是說View會将這個事件消費掉,同時要注意上文中有一行代碼是ViewGroup調用super的dispatchTouchEvent方法,那麼最終也是會執行到這裡的。