android的事件分發(fā)在面試時(shí)算是高頻問題陌兑,工作中也能用到租谈,這里將事件分發(fā)、事件沖突展氓,和NestedScrolling中的事件傳遞整理哈穆趴。
Android事件分發(fā)
方法說明
dispatchTouchEvent:事件分發(fā),Activity遇汞, ViewGroup未妹, View都有該方法,Activity和ViewGroup分發(fā)給子View空入, View分發(fā)給自己
onInterceptTouchEvent:攔截事件络它,只有ViewGroup有該方法,用于事件歪赢,ViewGroup想要處理某個(gè)事件時(shí)化戳,可以隨時(shí)對子View, say no埋凯!我這個(gè)我要處理
onTouchEvent:事件消費(fèi)点楼,Activity扫尖,ViewGroup,View都有該方法
對于開發(fā)者來說掠廓,第一個(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
- 沒有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中并沒有重寫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)識小压,當(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;
}
}
}
}
}
// 沒有找到消費(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都會走這里去下發(fā)
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
//down事件是 alreadyDispatchedToNewTouchTarget 已經(jīng)為true蜻牢,所以down事件不會被消費(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)了事件笛谦。對于上層ViewGroup而言抱虐,View或者ViewGroup,對于他們都是一樣處理饥脑。調(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)了就會返回true
總結(jié)
- 事件從ViewGroup逐級往下分發(fā)谣沸,直到找到消費(fèi)的view或者viewgroup
- 子view一但消費(fèi)了down后續(xù)的move和up都會分發(fā)給它(一個(gè)前提,未被父view攔截)笋颤,即使onTouchEvent返回了flase
- 父view一但做了攔截乳附,不管子view是否還想消費(fèi)事件,都會被父view消費(fèi)掉
- 如果沒有子view消費(fèi),父view就會調(diào)用自己的onTouchEvent
關(guān)于第二點(diǎn)還要補(bǔ)充哈许溅,為什么子view一但在down事件中返回了true,后續(xù)的事件都會分發(fā)給它秉版,因?yàn)楦竀iew的mFirstTouchTarget 已經(jīng)不為空贤重,父View的父級View中的mFirstTouchTarget 也不為,一層層的下來清焕。事件每次都會分發(fā)給down時(shí)返回true的view并蝗。這就是為什么,有時(shí)候我們明明已經(jīng)移出控件外了秸妥,但是還是會接收到move和up事件滚停。如果move和up事件返回false,事件最終就會調(diào)用activity的onTouchEvent
事件沖突處理
從上面的事件分發(fā)可知粥惧,ViewGroup擁有子view的絕對分配權(quán)键畴,父view攔截事件,就沒得子view啥事了突雪。
在我們開發(fā)過程中可能遇到起惕,在一個(gè)垂直滾動(dòng)的scrollview中前提一個(gè)橫向的列表,如果橫向滾動(dòng)列表咏删,手指不會一直是一條直線惹想,導(dǎo)致scrollview上下滾動(dòng),這樣體驗(yàn)就不好督函。這個(gè)就是需要解決的事件沖突嘀粱,解決這種沖突有兩個(gè)方案。
Plan1:重寫父onTouchEvent辰狡,監(jiān)聽當(dāng)前頁面的列表锋叨,如果列表是當(dāng)前消費(fèi)事件,onTouchEvent就不消費(fèi)了
Plan2:在子View的down或者move事件中調(diào)用parent.requestDisallowInterceptTouchEvent()
為什么需要在子View的down和move中去調(diào)用搓译?在父View的dispatchTouchEvent中悲柱,down事件是會去重置禁止攔截的標(biāo)識,詳細(xì)查看ViewGroup.resetTouchState()