1. 事件分發(fā)最重要的三個方法
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
- dispatchTouchEvent(MotionEvent ev)
用來進行事件分發(fā),如果事件能到達當前View,那么此方法一定會被調(diào)用,而且是先調(diào)用。返回值表示是否消耗當前事件。 - onInterceptTouchEvent(MotionEvent ev)
在dispatchTouchEvent方法內(nèi)部調(diào)用,用來判斷是否攔截某個事件,如果當前View攔截了某個事件,那么在同一個事件序列當中,此方法不會再被調(diào)用。返回值表示是否攔截當前事件。 - onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中調(diào)用,用來處理點擊事件,返回結(jié)果表示是否消耗當前事件。
三者關系用偽代碼說明:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
2. 一些結(jié)論
- 同一個事件序列是指從手指接觸屏幕的那一刻開始,到手指離開屏幕的那一刻結(jié)束,在這個過程中所產(chǎn)生的一系列事件。這個事件以down事件開始,中間含有數(shù)量不定的move事件,最終以up事件結(jié)束。
- 正常情況下,一個事件序列只能被一個View攔截且消耗。因為一旦一個元素攔截了某事件(down事件),那么同一事件序列內(nèi)的所有事件都會交給它處理。但是可以通過其他特殊手段,比如一個View將本該自己處理的事件通過onTouchEvent強行傳遞給其他View處理。
- 某個View一旦決定攔截(onInterceptTouchEvent),那么一個事件序列都只能由它來處理,并且它的oninterceptTouchEvent不會再被調(diào)用。
- 某個View一旦開始處理(onTouchEvent)事件,如果不消耗ACTION_DOWN事件,那么同一事件序列的其他事件都不會再交給它來處理,并且事件將重新交由它的父元素處理,即父元素的onTouchEvent方法會被調(diào)用。
- 如果View不消耗ACTION_DOWN以外的其他事件,那么這個點擊事件會消失,此時父元素的onTouchEvent并不會被調(diào)用,并且當前View可以持續(xù)收到后續(xù)事件,最終這些消失的點擊事件會傳遞給Activity處理。
- ViewGroup默認不攔截任何事件,源碼中ViewGroup的onInterceptTouchEvent方法默認返回false。
- View沒有onInterceptTouchEvent方法,一旦有點擊事件傳遞給它,那么它的onTouchEvent方法就會被調(diào)用。
- View的onTouchEvent默認都會消耗事件(返回true),除非它是不可點擊的(clickable和longClickable同時為false)。View的longClickable屬性默認都為false,clickable屬性要分情況,比如button的clickable屬性默認為true,而TextView的clickable屬性默認為false。
- View的enable屬性不影響onTouchEvent的默認返回值。哪怕一個View是disable狀態(tài),只要它的clickable或longClickable有一個為true,那么它的onTouchEvent返回的就是true。
- onClick會發(fā)生的前提是當前View是可點擊的,并且它收到了down和up事件。
- 事件傳遞過程是由外向內(nèi)的,即事件總是先傳遞給父元素,然后再由父元素分發(fā)給子元素,子元素可以通過requestDisallowInterceptTouchEvent方法可以在子元素中干預父元素的事件分發(fā)過程,但是ACTION_DOWN事件除外。
3. Activity對點擊事件的分發(fā)過程
點擊事件用MotionEvent來表示,當一個點擊操作發(fā)生時,事件最先傳遞給當前Activity,又Activity的dispatchTouchEvent來進行分發(fā),具體工作由Activity內(nèi)部的Windwo來完成。Window會將事件傳遞給decor view,decor view一般就是當前界面的底層容器(即setContentView所設置的View的父容器),通過Activity.getWindow().getDecorView()獲得。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
window是個抽象類,window的唯一實現(xiàn)是PhoneWindow,看PhoneWindow的分發(fā)事件方法
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
DecorView extends FrameLayout implements RootViewSurfaceTaker{}
我們在activity中可以獲取DecorView
getWindow().getDecorView()
我們通過setContentView方法設置的Veiw是DecorView的子View?,F(xiàn)在事件已經(jīng)到ViewGroup了,繼續(xù)看ViewGroup的分發(fā)。
4. 頂級View對點擊事件的分發(fā)過程
事件到達頂級View后,肯定會進入dispatchTouchEvent方法中,該方法中首先判斷是否攔截,攔截則當前Veiw自己處理,處理方式要先看是否有onTouchListener,有則執(zhí)行onTouchListener并根據(jù)其返回值看是否執(zhí)行OnTouchEvent。不攔截則找到當前點擊位置的子View繼續(xù)分發(fā)。View中dispatchTouchEvent方法的源碼:
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
繼續(xù)看ViewGroup的dispatchTouchEvent方法的攔截處理
// Check for interception.
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;
}
ViewGroup在兩個條件下會判斷是否攔截當前事件,ACTION_DOWN事件時或者mFirstTouchTarget不為空時。mFirstTouchTarget會在事件由ViewGroup子元素成功處理時,被賦予子元素的值。
也就是事件被子元素處理了,mFirstTouchTarget有值,沒被子元素處理,也就是被當前ViewGroup攔截了,則mFirstTouchTarget就沒有值,就不滿足條件了。
假如down已經(jīng)被當前viewGroup攔截,當move和up事件到來時,mFirstTouchTarget是空,所以會直接執(zhí)行intercepted=true,也就是直接攔截move和up事件都交給當前View處理。否則intercepted的值是onInterceptTouchEvent方法的返回值。
判斷了上面兩個條件,下面還有一個判斷:
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;
}
這里有一個標志位FLAG_DISALLOW_INTERCEPT,在子view中可以通過下面方法設置:
getParent().requestDisallowInterceptTouchEvent(true);
一旦設置后,ViewGroup將無法攔截除了ACTION_DOWN以外的其他點擊事件。
為什么除了ACTION_DOWN呢?因為在執(zhí)行上面的代碼前,還有一些代碼
// Handle an initial 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();
}
如果是ACTION_DOWN,會重置標記位。由此我們知道兩點:
- 第一點,onInterceptTouchEvent不是每次事件都會被調(diào)用,如果我們想提前處理所有的點擊事件,要選擇dispatchTouchEvent方法,只有這個方法能確保每次都調(diào)用。
- 另外一點,F(xiàn)LAG_DISALLOW_INTERCEPT標記位能幫我們解決滑動沖突。
攔截或者不攔截由intercepted決定,上面的條件判斷最后都會給intercepted賦值。然后看攔截和不攔截的代碼如下:
if (!canceled && !intercepted) {
// 不攔截
}
if (mFirstTouchTarget == null) {
// 攔截注意這里的dispatchTransformedTouchEvent方法.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
然后繼續(xù)看ViewGroup在不攔截時的詳細代碼。
for (int i = childrenCount - 1; i >= 0; i--) {
...
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
...
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
...
}
遍歷所有元素,判斷子元素是否能夠接受到點擊事件。下面兩個方法就是判斷標準,第一個表示是否可見以及是否有動畫。第二個表示點擊事件是否落在子元素的區(qū)域內(nèi)。
private static boolean canViewReceivePointerEvents(View child) {
return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null;
}
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempPoint();
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, child);
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
如果有滿足的元素則執(zhí)行dispatchTransformedTouchEvent方法
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) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
}
因為傳入的child不為null,所以調(diào)用子View的dispatchTouchEvent繼續(xù)分發(fā)。
如果子元素的分發(fā)返回了true,則上面的代碼繼續(xù)執(zhí)行addTouchTarget方法
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
發(fā)現(xiàn)如果子View消耗掉了點擊事件,則給mFirstTouchTarget賦值。它有了值,后續(xù)的move和up還會判斷是否要攔截。它沒有值,則直接由當前View處理。
如果遍歷所有的子View都沒有消耗事件。則調(diào)用
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
注意第三個參數(shù)child為null,則依據(jù)上面的源碼知道會調(diào)用super的dispatchTouchEvent方法。super是View,下面看View的dispatchTouchEvent方法
5. View的事件處理
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
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;
}
}
首先是onTouchListener的判斷,然后執(zhí)行的onTouchEvent方法,在onTouchEvent的ACTION_UP時,會判斷并調(diào)用click方法。
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
這里注意ViewGroup和View的dispatchTouchEvent方法是不同的,ViewGroup中的分發(fā)有攔截判斷;View中的分發(fā)只有onTouchListener的判斷,接著就調(diào)用了onTouchEvent方法。而ViewGroup是沒有重寫onTouchEvent方法的,在事件攔截后,ViewGroup會調(diào)super的dispatchTouchEvent,也就是View的dispatchTouchEvent,在里面調(diào)onTouchEvent方法,當然我們可以自己重寫onTouchEvent方法。
讀完這一章,覺得作者自己很清楚,但寫出來還是覺得混亂,連個流程圖都沒有。全是文字堆積,讓人看的昏昏欲睡。這里推薦兩篇郭霖的文章,相比之下還是比較清楚的,如果兩個結(jié)合來學習大有益處。
Android事件分發(fā)機制完全解析,帶你從源碼的角度徹底理解(上)
Android事件分發(fā)機制完全解析,帶你從源碼的角度徹底理解(下)
附加一篇簡書上的另一片文章,他總結(jié)的比我好:
Android View 事件分發(fā)機制源碼詳解(ViewGroup篇)