1. 源碼分析目標
上一篇文章中對 View 事件分發(fā)的規(guī)律進行了總結(jié),總結(jié)了 View 事件流的分發(fā)規(guī)律以及不同攔截情況下的走向速梗。其中有些總結(jié)我們可能只知道結(jié)論,但是并不知道為什么是那樣的結(jié)論腔寡,比如舷蟀,在父 View 中的 onInterceptTouchEvent 中攔截 move 時翠储,首次 move 事件過來時危号,子 View 的 dispatchTouchEvent 中收到了一個動作 ACTION_CANCEL什猖,而父 View 首次并沒有收到第一次 move 事件票彪,而 Activity 中收到了第一次的 move 事件?不通過源碼我們很難理解為什么是這樣不狮。
下面整理一下問題點降铸,閱讀源碼時我們主要來解決幾個疑問:
- 事件分發(fā)的 U 型結(jié)構(gòu)是怎么產(chǎn)生的?
- 子 View 什么情況下會收到 Cancel 事件摇零?
- 父 View 攔截 Move 事件后推掸,為什么把第一個 Move 事件交給 Activity 了?
- 為什么 Down 事件至關(guān)重要?
- 什么是內(nèi)部攔截法谅畅,什么是外部攔截法登渣?
- 為什么會有 onInterceptTouchEvent 和 requestDisallowInterceptTouchEvent?
- 為什么會有 TouchTarget 鏈毡泻,什么情況下會有多個 TouchTarget胜茧?
2. Activity 之前的事件分發(fā)
在底層是通過管道機制來完成事件的監(jiān)聽和下發(fā),首先當產(chǎn)生事件時牙捉,driver 向特定描述符寫入事件后竹揍,會觸發(fā)喚醒 epoll 工作,此時 eventHub 通過 read 方法從描述符中讀取原始事件邪铲,然后通過簡單封裝成 rawEvent 并傳遞給 InputReader芬位。InputReader 中循環(huán)線程會獲取 eventHub 中的事件,然后將事件傳遞到 InputDispater 并最終傳遞到上層带到。上層中有 InputEventReceiver 來接收事件昧碉,它的實現(xiàn)類 WindowInputEventReceiver 負責(zé)接收,WindowInputEventReceiver 是在 ViewRootImpl 中的內(nèi)部類揽惹,ViewRootImpl 我們就不陌生了被饿,雖然不是 View,但是 View 中很多操作源頭開始于此搪搏,如繪制狭握,事件分發(fā)等。
在 ViewRootImpl 中主要將事件按照隊列形式來處理疯溺,依次從隊列中出去事件论颅,每個事件又分為 InputStage 處理,可以理解為階段處理囱嫩。InputStage 主要是用來將事件的處理分成若干個階段(stage)進行恃疯,事件依次經(jīng)過每一個stage,如果該事件沒有被處理(標識為FLAG_FINISHED)墨闲,則該stage就會調(diào)用 onProcess 方法處理今妄,然后調(diào)用 forward 執(zhí)行下一個 stage 的處理;如果該事件被標識為處理則直接調(diào)用 forward鸳碧,執(zhí)行下一個 stage 的處理盾鳞,直到?jīng)]有下一個 stage(也就是最后一個SyntheticInputStage)。這里一共有 7 種 stage瞻离,各個 stage 間串聯(lián)起來雁仲,形成一個鏈表。
其中對于我們關(guān)心的事件屬于 MotionEvent琐脏,相應(yīng)的 InputStage 實現(xiàn)類是 ViewPostImeInputStage,對于手指觸碰的 TouchEvent,在 onProcess 方法中最終調(diào)用 processPointerEvent 處理日裙,processPointerEvent 中又調(diào)用了 mView.dispatchPointerEvent(event)吹艇,mView 是 DecorView,這樣就越來越接近我們的視野范圍昂拂。
final class ViewPostImeInputStage extends InputStage {
...
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
mAttachInfo.mUnbufferedDispatchRequested = false;
mAttachInfo.mHandlingPointerEvent = true;
boolean handled = mView.dispatchPointerEvent(event);
...
return handled ? FINISH_HANDLED : FORWARD;
}
}
DecorView 中沒有重寫 dispatchPointerEvent 方法受神,而是走了父類 View 的 dispatchPointerEvent 方法,根據(jù)事件類型判斷格侯,如果是 TouchEvent鼻听,則回到
DecorView 中的 dispatchTouchEvent。
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}
DecorView 中是調(diào)用 Window.Callback 來處理的联四,在 Activity 啟動過程中會創(chuàng)建 PhoneWindow撑碴,并將自己賦給 PhoneWindow,因為 Activity 實現(xiàn)了 Window.Callback 接口朝墩,所以就會轉(zhuǎn)到 Activity 中的 dispatchTouchEvent 處理醉拓。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
接著調(diào)用 PhoneWindow 的 superDispatchTouchEvent,又回到 mDecor.superDispatchTouchEvent(event)收苏,輾轉(zhuǎn)反側(cè)還交給 mDecor 來處理,接著就來到我們熟悉的 ViewGroup 的 dispatchTouchEvent 中亿卤。
這里簡單思考一下:
(1)ViewRootImpl 中收到事件后為什么不直接通過 DecorView 直接分發(fā)事件,而是繞『彎路』交給 Activity 處理鹿霸,然后又轉(zhuǎn)到 DecorView排吴?
(2)Activity 中為什么不直接獲取 DecorView,而是通過 Window 獲扰呈蟆钻哩?
首選看下第一個問題,由于 Activity 中也會需要處理事件葛闷,如果在 DecorView 中處理憋槐,那么在 DecorView 中就需要回調(diào) Activity 或者持有 Activity,這樣顯然不是很合理淑趾,耦合嚴重阳仔,此外,如果從 DecorView 開始處理扣泊,那么起點就是 Activity 了近范。Activity 是直接面向開發(fā)者的,起始點應(yīng)該設(shè)在 Activity 中延蟹,Activity 是一個外殼评矩,Activity 依賴 Window, Window 再對 View 管理,也就是 PhoneWindow 持有 DecorView阱飘。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
再看下第二個問題:第一個問題中已經(jīng)提到了斥杜,Activity 是一個外殼虱颗,Activity 直接依賴 Window,Window 是更高層的抽象蔗喂,View 僅僅是 Window 管理的其中部分忘渔,可以比喻為,Activity 不會親自干這種細活缰儿,細活應(yīng)該交給 Window 管理畦粮。同時也達到解耦的目的。
2. Activity 之中的事件分發(fā)
一般來說乖阵,我們平時開發(fā)更多關(guān)注的是 View 的事件分發(fā)宣赔,很少在 Activity 中處理一些事件。而 Activity 中的事件分發(fā)也相對簡單瞪浸,沒有過多的邏輯儒将。在 Activity 中是事件分發(fā)的起點,內(nèi)部 View 沒有攔截的話默终,最后交給 Activity 的 onTouchEvent 處理椅棺。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
接著是 View 的事件分發(fā),事件從底層過來之后齐蔽,按照從下到上的順序分發(fā)两疚,可以理解為在 Z 方向上下屏幕下到上完成事件的傳遞。一個完整的事件流由一個 Down 事件含滴、若干個 Move 事件和一個 Up 事件組成诱渤。接下來就看下底層 View 是如果向上分發(fā)事件的。先看下 ViewGroup 的 dispatchTouchEvent 方法谈况。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
// 安全策略校驗通過
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 1. down 事件時清除狀態(tài)勺美,清除事件傳遞鏈表
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 2. 事件攔截檢查
final boolean intercepted;
// 在初始 Down 事件時或者已經(jīng)有子 View 攔截事件時,即 mFirstTouchTarget碑韵,下次有事件過來時赡茸,如 Move 和 Up 事件,作為 ViewGroup 需要看下是否攔截祝闻,即看 onInterceptTouchEvent 是否攔截
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 在不攔截 Move 和 Up 事件時占卧,所以當前 ViewGroup 攔截事件,意味著事件不再向下傳遞
intercepted = true;
}
// 檢查是否取消事件
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
3. 在 Down 事件時联喘,ViewGroup 沒有攔截的情況下华蜒,尋找攔截事件的子 View
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
...
// 在 Down 事件時尋找要攔截的子 View,ACTION_POINTER_DOWN 代表是多指觸碰豁遭,后面會分析
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
...
// 從前到后遍歷子View叭喜,找到需要攔截事件的子 view
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...
// 檢查子View是否能夠接收事件,并判斷事件是否在 view 范圍內(nèi)
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// 判斷子 View 是否已經(jīng)添加到攔截事件的鏈表上蓖谢,即以 mFirstTouchTarget 為頭的鏈表
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);
// 向子 View 分發(fā)事件捂蕴,看是否需要攔截
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// 子 View 要攔截譬涡,即找到了攔截的子View,結(jié)束循環(huán)启绰,因為每個事件只有一個View處理
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
}
}
// 4 沒有子 view 攔截時昂儒,交給自己處理,即走自己的 onTouchEvent
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
// 5 有子 view 攔截時委可,調(diào)用子 View 的 dispatchTouchEvent 方法,向上分發(fā)
else {
// 有子 view 攔截腊嗡,遍歷鏈表着倾,將事件傳遞到子 View,這里有兩種情況需要處理
// (1) 如果 ViewGroup 攔截事件,那么向子 View 下發(fā)取消事件燕少,并跳轉(zhuǎn)鏈表的下一個節(jié)點
// (2) 如果當前的 TouchTarget 是 newTouchTarget卡者,代表已經(jīng)分發(fā)事件了,即在上面已經(jīng)對子 View 調(diào)用過 dispatchTransformedTouchEvent
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
}
return handled;
}
ViewGroup 的 dispatchTouchEvent 方法相對較長客们,這里再分步驟詳細說明一下(父View 用 ViewGroup 表示),事件下發(fā)時崇决,從 ViewGroup 到子 View,也就是說Down底挫、Move恒傻、Up 事件流是ViewGroup流向子View的,那么在這個過程中ViewGroup可以通過 onInterceptTouchEvent 攔截事件建邓,攔截后就不再向子 View 傳遞盈厘,當然子 View 也可以設(shè)置,不讓 ViewGroup 攔截官边,通過設(shè)置 requestDisallowInterceptTouchEvent 不讓 ViewGroup 攔截沸手。
(1) down 事件時清除狀態(tài),清除事件傳遞鏈表注簿,也就是說 Down 事件時契吉,重新記錄事件的攔截情況,因為 Down 是事件流的開始
(2)actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null诡渴,在初始 Down 事件時或者已經(jīng)有子 View 攔截事件時捐晶,即 mFirstTouchTarget 不為空,下次有事件過來時玩徊,如 Move 和 Up 事件租悄,作為 ViewGroup 需要看下是否攔截,即看 onInterceptTouchEvent 是否攔截恩袱。
- 對于 Down 事件時泣棋,如果 ViewGroup 攔截了,可以看 intercepted 使用的位置畔塔,會導(dǎo)致不會有子 View 收到事件潭辈,mFirstTouchTarget 為空鸯屿,意味著事件只會在 dispatchTouchEvent 中處理,并返回上一層的 dispatchTouchEvent把敢,ViewGroup 的 onTouchEvent 也不會被調(diào)用寄摆。
- 如果不是 Down 事件,那么就會是 Move 或者 Up 事件修赞,此時判斷 mFirstTouchTarget 需要不為空才會有可能將事件向上分發(fā)婶恼,才會判斷 onInterceptTouchEvent,為什么呢柏副?上面分析了勾邦,如果 Down 事件時 ViewGroup 攔截了,mFirstTouchTarget 為空的割择,意味著 ViewGroup 在 Down 事件時不能攔截事件眷篇,子 View 需要攔截事件,這樣后續(xù)子 View 才有可能收到 Move 和 Up 事件荔泳,這塊也說明了 Down 事件對子 View 至關(guān)重要蕉饼,決定是否可以收到后續(xù)事件的前提
(3)在 Down 事件時,ViewGroup 沒有攔截的情況下玛歌,尋找攔截事件的子 View昧港。這一步雖然代碼不少,但是邏輯相對簡單沾鳄,就是遍歷子 View慨飘,向子 View 分發(fā)事件,找到要攔截的那個子View译荞,dispatchTransformedTouchEvent 方法會調(diào)用子 View 的 dispatchTouchEvent 方法瓤的,如果子 View 也是一個 ViewGroup,那么就會進行遞歸調(diào)用吞歼,若果是一個 View,會走 View 的 dispatchTouchEvent 方法圈膏,要么 dispatchTouchEvent 方法返回 true,要么內(nèi)部 onTouchEvent 返回 true,或者 OnTouchListener 返回true篙骡,總之在 Down 時需要攔截到事件稽坤。
(4)在步驟 3 中如果沒有找到攔截事件的子 View,mFirstTouchTarget 為空糯俗,此時事件就交給 ViewGroup 自己來處理尿褪,即通過調(diào)用 dispatchTransformedTouchEvent 方法,里面會調(diào)用 View 的 dispatchTouchEvent 方法得湘。
(5)如果找到需要攔截事件的子 View杖玲,那么 mFirstTouchTarget 就不為空,此時如果 ViewGroup 不攔截事件淘正,會將事件繼續(xù)向子 View 分發(fā)摆马。因為事件流總是由父 View 流向子 View臼闻,所以需要看父 View 是否攔截。分發(fā)時需要判斷 Dwon 事件情況囤采,因為這塊的代碼沒有使用事件類型判斷述呐,如判斷事件是否是 Move 或者 Up,而是使用mFirstTouchTarget不為空判斷的蕉毯,需要排除 Down 事件的情況乓搬,即 alreadyDispatchedToNewTouchTarget && target == newTouchTarget 成立時,因為前面已經(jīng)調(diào)用過子 view 的 dispatchTouchEvent 方法恕刘,避免重復(fù)調(diào)用缤谎。
在 ViewGroup 的 dispatchTouchEvent 中多處調(diào)用了 dispatchTransformedTouchEvent 方法,那就看下 dispatchTransformedTouchEvent 中的主要處理邏輯褐着。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// 1 分發(fā)取消事件
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;
}
// 對事件處理,判斷是否是多指觸摸的事件還是同一根手指事件
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {
return false;
}
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
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;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
// 2 事件自己處理托呕,調(diào)用 View dispatchTouchEvent 方法含蓉,自己作為 view 處理事件
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
}
// 3 向子 view 分發(fā)事件,調(diào)用子 View 的 dispatchTouchEvent 方法
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);
}
// Done.
transformedEvent.recycle();
return handled;
}
主要有3個重要的處理:
(1)取消事件處理项郊,可以是自身的取消事件馅扣,也可能是子 View 的取消事件處理,那么都是什么情況才是取消事件呢着降?在 dispatchTouchEvent 中找到調(diào)用 dispatchTransformedTouchEvent 方法中 cancel 參數(shù)是 true 的情況差油,
- 對于子 View 情況,即 child 參數(shù)不為空任洞,同時 cancel 參數(shù)為 true蓄喇,也就是在上面 dispatchTouchEvent 分析的第 5 步中,mFirstTouchTarget 不為空交掏,也就是在 down 事件時有子 View 攔截妆偏,在 Move 或者 Up 時 ViewGroup 攔截了事件,此時 Move 和 Up 不再分發(fā)給子 View盅弛,會給子 View 傳遞一個 Cancel 事件钱骂,一方面可以讓子 View 相關(guān)狀態(tài)復(fù)位,另一方面告知子 View 的事件流結(jié)束
- 對于 ViewGroup 自身情況挪鹏,dispatchTransformedTouchEvent 方法傳遞參數(shù) child 為 null见秽,cancel 為 true,也就是 ViewGroup 自己的子 View 沒有攔截事件讨盒,自己作為下層父 View 的子 View解取,攔截了 Down 事件,在 Move 或者 Up 事件被自己的父 View 攔截時催植,cancel 為 true
(2)沒有子 View 攔截事件時肮蛹,即 mFirstTouchTarget 為 null勺择,事件自己處理,走 View 中的 dispatchTouchEvent 方法
(3)有子 View 攔截事件時伦忠,事件向上層子 View 分發(fā)
從這塊代碼分析省核,可以得出,子 View 收到 Cancel 事件昆码,首先是子 View 能夠攔截到 Down 事件气忠,在后續(xù)的事件被父 View 攔截時會收到一個 Cancel事件。
接著再看下 View 中到底是如何處理事件的
public boolean dispatchTouchEvent(MotionEvent event) {
...
final int actionMasked = event.getActionMasked();
// down 事件時停止?jié)L動
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
// 先調(diào)用 mOnTouchListener 處理赋咽,優(yōu)先級比 onTouchEvent 高
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// onTouchEvent 高
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
// UP 旧噪、CANCEL 結(jié)束事件處理,停止滑動
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
View 的 dispatchTouchEvent 事件相對簡單脓匿,主要是對事件的響應(yīng)處理淘钟,即調(diào)用 mOnTouchListener 或者 onTouchEvent 來對事件直接響應(yīng)的可以看到 mOnTouchListener 優(yōu)先級比 onTouchEvent 高,如果 mOnTouchListener 中返回 true陪毡,那么 onTouchEvent 中就不再收到事件處理米母,而 onTouchEvent 還有 mOnClickListener 和 mOnLongClickListener 處理,可以認為優(yōu)先級比 onTouchEvent 低毡琉。
到這里事件流的一個整理流向分析基本已經(jīng)完成铁瞒,那么我們看下對于 Down 事件,是如何完成經(jīng)典的 U 型走向的?
- 其實所謂的 U 型桅滋,實際上就是分發(fā)函數(shù)遞歸向下層層調(diào)用慧耍,即 父 View1 dispatchTouchEvent --> 父 View2 dispatchTouchEvent ---> 子 View dispatchTouchEvent,能一直向下調(diào)用的前提是丐谋,父 View 中的 onInterceptTouchEvent 沒有攔截芍碧,均是走 dispatchTouchEvent 默認的 super 方法,簡單來說就是走源碼的 dispatchTouchEvent 實現(xiàn),不是我們自己重寫方法的實現(xiàn)
- 那 U 型結(jié)構(gòu)的另外一邊呢,即回溯過程血巍?為了保證完整的 U 型結(jié)構(gòu),對于子 View 仍舊走 dispatchTouchEvent 的默認實現(xiàn)践美,最終調(diào)用默認的 onTouchEvent,這樣對于上層子 View dispatchTouchEvent 返回值是 false 的找岖,對于父 View 中 mFirstTouchTarget 是 null陨倡,這樣父 View 調(diào)用 dispatchTransformedTouchEvent 傳遞的 child 也為 null,所以也會走view dispatchTouchEvent 方法许布,然后走 onTouchEvent 方法兴革,然后重復(fù)這個過程返回值層層向上回溯,最終會回到 Activity 的 dispatchTouchEvent 方法中,最后走 Activiy 的 onTouchEvent 方法杂曲。
對于一個 Down 事件庶艾,如果我們不做任何操作,Down 事件就會完成這樣一個 U 型結(jié)構(gòu)的流向擎勘。
在文章開頭的一個問題:ViewGroup 攔截 Move 事件后咱揍,為什么把第一個 Move 事件交給 Activity 了?這個問題是有背景的棚饵,是在上一篇文章中的例子煤裙,ViewGroup 是 Activity 中布局的根布局,ViewGroup 的子 View 攔截 Down 事件噪漾,在 Move 事件過來時硼砰,被 ViewGroup 攔截,此時第一個 Move 事件傳到了 Activity 中
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 攔截 Move 事件欣硼,intercepted = true
...
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 攔截 Move 事件后向子 View 發(fā)動一個 Cancel 事件
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
...
}
predecessor = target;
target = next;
}
return handled;
}
}
我們跟蹤一下事件流题翰,ViewGroup 攔截事件后,向子 View 傳遞一個 Cancel 事件诈胜,即調(diào)用 dispatchTransformedTouchEvent 方法遍愿,然后調(diào)用子 View 的 dispatchTouchEvent 方法,子 View 中 cancel 事件一般返回 false耘斩,那么 handled 值也為 false,所以回到 Activity 中的 dispatchTouchEvent 方法時桅咆,會走 onTouchEvent(ev) 方法所以第一個的 Move 事件就流到了 Activity 中括授。對于后面的一系列 Move 事件,只會在 ViewGroup 中被消費岩饼,從上面的代碼中看出荚虚,首次攔截后 mFirstTouchTarget 會被清除掉,所以后面會走 mFirstTouchTarget == null 分支籍茧,即 ViewGroup 自己消費 Move 事件版述,handled 為 true,返回 Activity 中時寞冯,也會直接返回 true渴析,不再走 onTouchEvent(ev)。
下面看下 TouchTarget吮龄,為什么會有 TouchTarget 鏈俭茧,什么情況下會有多個 TouchTarget?
TouchTarget 是用來表示 View 對多個捕獲事件的描述漓帚,簡單來說母债,主要是能夠處理多指觸摸的情況,多只觸摸情況下,多個手指可能觸摸一個 View毡们,也可能是多個 View迅皇,多指觸摸一個 View 時,通過 TouchTarget 記錄該 View 的多個 pointerIdBits衙熔,多指觸摸多個 View 時登颓,通過多個 TouchTarget 組成的鏈表來記錄。鏈表的創(chuàng)建主要是在 Down 事件時通過 addTouchTarget(child, idBitsToAssign) 添加到鏈表上青责,鏈表頭是 mFirstTouchTarget挺据。對于 Down 事件,第一個 Down 事件是 ACTION_DOWN脖隶,后面其他手指的 Down 事件是 ACTION_POINTER_DOWN扁耐,不同 Down 事件都有 index 和 id,對于 ACTION_DOWN 的index始終是 0产阱。所以多指觸摸情況下會有 ACTION_DOWN 和 多個 ACTION_POINTER_DOWN 事件婉称,多個 ACTION_POINTER_DOWN 事件如果是作用在 ViewGroup 中不同子 View 上就會有多個 TouchTarget 形成一個鏈表結(jié)構(gòu),而在 Move 事件時是沒有 ACTION_POINTER_MOVE 的构蹬,只有 ACTION_MOVE王暗。
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
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
}
什么是內(nèi)部攔截法,什么是外部攔截法庄敛?內(nèi)部和外部是以子 View 來說的俗壹,如果在子 View 內(nèi)部處理滑動沖突,就是內(nèi)部攔截法藻烤,如果在 ViewGroup 中處理事件攔截绷雏,就是外部攔截法,這里舉一個例子怖亭,父 View 需要攔截左右方向的 Move 事件涎显,子 View 需要攔截垂直方向的 Move 事件。
外部攔截法:
父 View
private float startX;
private float startY;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
float delX = Math.abs(ev.getX() - startX);
float delY = Math.abs(ev.getY() - startY);
if (delY < delX) {
return true;
}
break;
default:
break;
}
return super.onInterceptTouchEvent(ev);
}
子 View
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
return true;
case MotionEvent.ACTION_MOVE:
// do some thing
return true;
default:
break;
}
return super.onTouchEvent(ev);
}
內(nèi)部攔截法
父 View
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
return true;
default:
break;
}
return super.onInterceptTouchEvent(ev);
}
子 View
private float startX;
private float startY;
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent(). requestDisallowInterceptTouchEvent(true);
return true;
case MotionEvent.ACTION_MOVE:
float delX = Math.abs(ev.getX() - startX);
float delY = Math.abs(ev.getY() - startY);
if (delY < delX) {
getParent(). requestDisallowInterceptTouchEvent(false);
return true;
}
break;
default:
break;
}
return super.onTouchEvent(ev);
}
以上就是內(nèi)部攔截和外部攔截法的簡單實例兴猩,外部攔截法期吓,就是外部ViewGroup通過 onInterceptTouchEvent 來決定是否攔截事件,攔截后倾芝,事件不再向子 View 分發(fā)讨勤。內(nèi)部攔截法是子 View 通過 getParent(). requestDisallowInterceptTouchEvent(false、true);決定是否讓 ViewGroup 攔截蛀醉,簡單來說就是看將判斷邏輯放在哪悬襟。
那么從設(shè)計角度看為什么有 onInterceptTouchEvent 和 requestDisallowInterceptTouchEvent?首先看 onInterceptTouchEvent 方法拯刁,有這樣一個場景脊岳,ViewGroup 是能夠上下滑動的,內(nèi)部子 View 是可以點擊的,假設(shè)沒有 onInterceptTouchEvent 方法割捅,那么手指在 子 View 上垂直方向移動時奶躯,應(yīng)該能夠上下滑動,才符合操作習(xí)慣亿驾,但是事件默認都是先給到子 View嘹黔,Move 達到子 View 時再傳給 父 View 也不是不行,但是較麻煩莫瞬,最直接的方式還是在 ViewGroup 向子 view 分發(fā)時直接將 Move 攔截儡蔓,這樣邏輯更加清晰。
requestDisallowInterceptTouchEvent 雖然看上去可有可無疼邀,但是有些場景還是需要有它來處理喂江,比如 ViewPager 中多個 Page,每個 Page 中有 ScrollView 可以上下滑動旁振,開始階段一直上下滑動获询,手指不抬起來,變成水平滑動拐袜,此時應(yīng)該不觸發(fā) ViewPager 切換更為合理吉嚣,但是沒有 requestDisallowInterceptTouchEvent 處理一下,ViewPager 會在水平滑動時攔截 Move 事件蹬铺,導(dǎo)致 ScrollView 對事件處理中途被改變了尝哆。所以這就是源碼中設(shè)置 onInterceptTouchEvent 和 requestDisallowInterceptTouchEvent 這兩個鉤子方法的意義。
3. View 中 onTouchEvent 分析
上面分析了 View 中 dispatchTouchEvent 方法甜攀,內(nèi)部優(yōu)先級是 mOnTouchListener > onTouchEvent, mOnTouchListener 沒什么可說的较解,是由用戶自定義重寫的接口,接下來看下 onTouchEvent 方法赴邻。
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
// 是否可點擊或者長按
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
// 是否view設(shè)置了DISABLED狀態(tài),如果是不再走后面的邏輯啡捶,但是如果 clickable姥敛,仍舊可以消費事件,只是不響應(yīng)點擊事件
if ((viewFlags & ENABLED_MASK) == DISABLED
&& (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
// 如果設(shè)置了 View 的事件代理瞎暑,則由代理完成 onTouchEvent彤敛,常用來擴大 View 的點擊熱區(qū)
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// 如果可點擊,根據(jù)事件來處理邏輯
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
// 如果不可點擊則移除 tap 檢測和長按檢測
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
// 是否是按下狀態(tài)
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// 獲取焦點
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
// 設(shè)置按下狀態(tài)了赌,一般是背景色半透明墨榄,讓用戶感知到是按下狀態(tài)
if (prepressed) {
setPressed(true, x, y);
}
// 在 UP 事件狀態(tài),如果沒有執(zhí)行長按情況下勿她,才有可能執(zhí)行點擊事件
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 移除長按事件的延遲處理
removeLongPressCallback();
// 注意這里是 !focusTaken袄秩,不是沒有焦點才執(zhí)行點擊事件,而是本身已經(jīng)有了焦點才執(zhí)行動作,也就是不需要重新獲取焦點之剧,如在有鍵盤情況下郭卫,點擊 // View 是不響應(yīng)事件的,而是先關(guān)閉鍵盤背稼,這種就屬于先要獲取焦點贰军。
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
// 延時取消按下狀態(tài)
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
// 不可點擊時,檢查是否可以長按蟹肘,因為有可能是 (mViewFlags & TOOLTIP) == TOOLTIP) 狀態(tài)
if (!clickable) {
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
break;
}
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// 使用 CheckForTap 做一個延遲檢測词疼,避免處于可滑動的父 View 中時,和滑動事件沖突
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// 觸發(fā)長按檢測帘腹,長按長按時間 400ms 響應(yīng)長按事件
setPressed(true, x, y);
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
break;
case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
...
// 移出 View 時贰盗,取消 tap 檢測和長按檢測,并取下按下狀態(tài)
if (!pointInView(x, y, touchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
...
break;
}
return true;
}
return false;
}
onTouchEvent 方法代碼雖然很多,但主要就是處理點擊和長按事件,在不同事件下對這兩種事件進行響應(yīng)處理竹椒。對于單擊事件相對明確童太,單擊事件是在 Up 時才響應(yīng)的,長按是有個時長胸完,超過 400ms 時才響應(yīng)书释,否則會取消響應(yīng),以及一些按壓狀態(tài)處理赊窥。
(1)在 Up 事件時爆惧,如果不可點擊則移除 tap 檢測和長按檢測,其中 tap 檢測是為了防止長按事件和屬于滑動容器中時和滑動事件互斥锨能,tap 檢測延遲的時長是 100ms扯再,超過 100ms 則說明是長按時間,再延遲 400-100 = 300ms 后執(zhí)行長按事件址遇。
(2)down 事件時主要是觸發(fā)長按事件的延遲檢測熄阻,其中也有上面提到的 tap 檢測,也就是說長按是由時間來決定的倔约,點擊事件是由 UP 事件決定的秃殉。
(3)move 事件時檢查觸摸位置是否還在需要響應(yīng)事件的 View 范圍內(nèi),不在的話浸剩,就需要取消 tap 檢測和長按檢測
(4)取消事件不用多說钾军,同樣會取消 tap 檢測和長按檢測,以及按壓狀態(tài)等
4 總結(jié)
以上就是對 View 分發(fā)事件源碼的一個解析绢要,主要針對開篇的幾個問題吏恭,在分析過程中也做出了回答。本質(zhì)上 View 的事件在一個時刻只由一個 View 來處理重罪,所以就會有攔截過程樱哼,事件由底層向上分發(fā)哀九,中間設(shè)置了一些鉤子函數(shù),onInterceptTouchEvent 和 onTouchEvent,給開發(fā)者定制的機會唇礁。當然還有更加復(fù)雜的場景勾栗,比如嵌套滑動的場景,使用 NestedScrollingChild 和 NestedScrollingParent以及后面更新的 2 和 3 代接口盏筐,主要來更好的解決嵌套滑動場景围俘,簡單來說就是使得 Move 事件能夠在一個事件流過程中,能夠被不同的 View 消費琢融,消費多少滑動距離界牡,都能夠做到精細控制,后面再針對嵌套滑動情形進行分析漾抬。