android的事件分發(fā)在面試時(shí)算是高頻問(wèn)題,工作中也能用到,這里將事件分發(fā)、事件沖突,和NestedScrolling中的事件傳遞整理哈。
Android事件分發(fā)

方法說(shuō)明
dispatchTouchEvent:事件分發(fā),Activity, ViewGroup, View都有該方法,Activity和ViewGroup分發(fā)給子View, View分發(fā)給自己
onInterceptTouchEvent:攔截事件,只有ViewGroup有該方法,用于事件,ViewGroup想要處理某個(gè)事件時(shí),可以隨時(shí)對(duì)子View, say no!我這個(gè)我要處理
onTouchEvent:事件消費(fèi),Activity,ViewGroup,View都有該方法
對(duì)于開發(fā)者來(lái)說(shuō),第一個(gè)接收到事件的地方就在dispatchTouchEvent中,如果想全局不允許點(diǎn)擊是,事件可以在這里直接返回,不進(jìn)行下一步的分發(fā)。上段源碼
Activity#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
- 調(diào)用Window的superDispatchTouchEvent
- 沒(méi)有view消費(fèi),我自己調(diào)用onTouchEvent,返回消費(fèi)結(jié)果
PhoneWindow#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
這里就熟悉了,調(diào)用了mDecor的superDispatchTouchEvent
DecorView#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView繼承了FrameLayout,F(xiàn)rameLayout又繼承了ViewGroup,F(xiàn)rameLayout中并沒(méi)有重寫dispatchTouchEvent, 所以就調(diào)用到了ViewGroup的dispatchTouchEvent
ViewGroup#dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...//此處省略數(shù)行
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 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);
//重寫設(shè)置狀態(tài)
resetTouchState();
}
// Check for interception.
final boolean intercepted;
//mFirstTouchTarget 不為空和Down事件,所以有子view消費(fèi)的情況下,此處一直為真
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//是否禁止攔截
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//父view是否攔截
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;
}
//很重要的標(biāo)識(shí),當(dāng)前是否已經(jīng)分配給target,不至于被down事件被消費(fèi)兩次
boolean alreadyDispatchedToNewTouchTarget = false;
//如果取消和攔截都不查找子view
if (!canceled && !intercepted) {
...此處省略數(shù)行
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...此處省略數(shù)行
//這里調(diào)dispatchTransformedTouchEvent, 最終調(diào)用了子View的onTouchEvent去確定該事件是否消費(fèi)
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...此處省略數(shù)行
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}
// 沒(méi)有找到消費(fèi)的子view,那去看看自己消費(fèi)不,最終調(diào)用到了ViewGroup的onTouchEvent
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}else {
//mFirstTouchTarget的后續(xù)事件,move/up都會(huì)走這里去下發(fā)
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
//down事件是 alreadyDispatchedToNewTouchTarget 已經(jīng)為true,所以down事件不會(huì)被消費(fèi)兩次
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
...此處省略數(shù)行
return handled;
}
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...此處省略數(shù)行
//父view調(diào)用是child==null,調(diào)用super.dispatchTouchEvent
// Perform any necessary transformations and dispatch.
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;
}
這里調(diào)用了子view的dispatchTouchEvent, 為了讓咋們的布局文件接收到分發(fā)事件,其實(shí)是頂層ViewGroup(DecorView)調(diào)用布局文件的dispatchTouchEvent,各個(gè)ViewGroup逐層分發(fā),直到有一個(gè)子View或者ViewGroup消費(fèi)了事件。對(duì)于上層ViewGroup而言,View或者ViewGroup,對(duì)于他們都是一樣處理。調(diào)用dispatchTouchEvent,ViewGroup調(diào)用dispatchTouchEvent就再次分發(fā),然后咋們看哈View的dispatchTouchEvent
View#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
...此處省略數(shù)行
if (!result && onTouchEvent(event)) {
result = true;
}
...此處省略數(shù)行
return result;
}
子view調(diào)用了onTouchEvent,如果消費(fèi)了就會(huì)返回true
總結(jié)
- 事件從ViewGroup逐級(jí)往下分發(fā),直到找到消費(fèi)的view或者viewgroup
- 子view一但消費(fèi)了down后續(xù)的move和up都會(huì)分發(fā)給它(一個(gè)前提,未被父view攔截),即使onTouchEvent返回了flase
- 父view一但做了攔截,不管子view是否還想消費(fèi)事件,都會(huì)被父view消費(fèi)掉
- 如果沒(méi)有子view消費(fèi),父view就會(huì)調(diào)用自己的onTouchEvent
關(guān)于第二點(diǎn)還要補(bǔ)充哈,為什么子view一但在down事件中返回了true,后續(xù)的事件都會(huì)分發(fā)給它,因?yàn)楦竀iew的mFirstTouchTarget 已經(jīng)不為空,父View的父級(jí)View中的mFirstTouchTarget 也不為,一層層的下來(lái)。事件每次都會(huì)分發(fā)給down時(shí)返回true的view。這就是為什么,有時(shí)候我們明明已經(jīng)移出控件外了,但是還是會(huì)接收到move和up事件。如果move和up事件返回false,事件最終就會(huì)調(diào)用activity的onTouchEvent
事件沖突處理
從上面的事件分發(fā)可知,ViewGroup擁有子view的絕對(duì)分配權(quán),父view攔截事件,就沒(méi)得子view啥事了。
在我們開發(fā)過(guò)程中可能遇到,在一個(gè)垂直滾動(dòng)的scrollview中前提一個(gè)橫向的列表,如果橫向滾動(dòng)列表,手指不會(huì)一直是一條直線,導(dǎo)致scrollview上下滾動(dòng),這樣體驗(yàn)就不好。這個(gè)就是需要解決的事件沖突,解決這種沖突有兩個(gè)方案。
Plan1:重寫父onTouchEvent,監(jiān)聽當(dāng)前頁(yè)面的列表,如果列表是當(dāng)前消費(fèi)事件,onTouchEvent就不消費(fèi)了
Plan2:在子View的down或者move事件中調(diào)用parent.requestDisallowInterceptTouchEvent()
為什么需要在子View的down和move中去調(diào)用?在父View的dispatchTouchEvent中,down事件是會(huì)去重置禁止攔截的標(biāo)識(shí),詳細(xì)查看ViewGroup.resetTouchState()