通過問題來學(xué)習(xí)一個東西是很好的方法痊银。學(xué)習(xí)Android中View的事件體系,我也通過給自己提問題施绎,在解決問題的同時也就知道了其中原理溯革。
0
首先來幾個問題起步:
- 什么是事件?
- 什么是事件分發(fā)機制谷醉?
在我們通過屏幕與手機交互的時候致稀,每一次點擊、長按俱尼、移動等都是一個個事件抖单。按照面向?qū)ο蟮乃枷耄@些一個個事件都被封裝成了MotionEvent号显。
分發(fā)機制就是某一個事件從屏幕傳遞給app視圖中的各個View臭猜,然后由其中的某個View來使用這一事件或者忽略這一事件躺酒,這整個過程的控制就是分發(fā)機制了押蚤。
要注意的是,事件分發(fā)機制中羹应,事件是按一個事件序列的形式分發(fā)給View的揽碘。這一序列由 ACTION_DOWN 開始,經(jīng)過一系列 ACTION_MOVE 等事件园匹,最后以 ACTION_UP 事件結(jié)束雳刺。這一個序列中的所有事件,要么被忽略裸违,要么就只能有一個事件能使用掖桦。要是同一個序列,比如從按下到移動這一系列的動作供汛,不同的View都能接受的話枪汪,那整個界面就會非秤磕拢混亂,而且邏輯很復(fù)雜雀久。
接下來我提出這三個問題:
- 某一個事件從屏幕一直傳遞到View上這一過程的大致流程是怎樣的宿稀?
- 前面說了事件分發(fā)的其實是事件序列。那么同一個序列里那么多事件赖捌,是怎樣的機制只交給一個View的祝沸?
- 我們平時在應(yīng)用開發(fā)時,在外部給View設(shè)置的的OnClick OnLongClick 的監(jiān)聽越庇,是在哪里被View處理的罩锐?
問題一:事件傳遞的流程是怎樣的?
Android中的View是樹狀結(jié)構(gòu)悦荒,如下圖所示:
每一個Activity內(nèi)部都包含一個Window用來管理要顯示的視圖唯欣。而Window是一個抽象類,其具體實現(xiàn)是 PhoneWindow類搬味。DecovrView作為PhoneWindow的一個內(nèi)部類境氢,實際管理著具體視圖的顯示。他是FrameLayout的子類碰纬,盛放著我們的標(biāo)題欄和根視圖萍聊。我們自己寫的一些列View和ViewGroup都是由他來管理的。因此事件分發(fā)的時候悦析,頂層的這些“大View”們實際上是不會對事件有任何操作的寿桨,他們只是把事件不斷的向下遞交,直到我們可以使用這些事件强戴。
所以亭螟,事件自頂向下的傳遞過程應(yīng)該是這樣的:
Activity(不處理)-> 根View -> 一層一層ViewGroup(如果有的話) -> 子View
如果傳遞到最后我們的子View們沒有處理這一事件怎么辦呢?這時候就會原路返回骑歹,最終傳遞給Activity预烙。只有當(dāng)Activity也沒有處理這一事件時,這一事件才會被丟棄道媚。
Activity(不處理則丟棄) <- 根View <- 一層一層ViewGroup(如果有的話) <- 子View
具體在傳遞事件的時候扁掸,是由以下三個方法來控制的:
- dispatchTouchEvent : 分發(fā)事件
- onInterceptTouchEvent : 攔截事件
- onTouchEvent : 消費事件
這三個方法有一個共同點,就是他們具體是否執(zhí)行了自己的功能(分發(fā)最域、攔截谴分、消費)完全由自己的返回值來確定,返回true就表示自己完成了自己的功能(分發(fā)镀脂、攔截牺蹄、消費)。不同之處除了功能外薄翅,還有使用的場景沙兰。dispatchTouchEvent()和onTouchEvent()這兩個方法虑省,無論是Activity ViewGroup 還是View,都會被用到。而onInterceptTouchEvent()方法因為只是為了攔截事件僧凰,那么Activity和View一個在最頂層探颈,一個在最底層,也就沒必要使用了训措。因此在View 和 Activity中是沒有onInterceptTouchEvent()方法的伪节。
我這里自定義幾個ViewGroup和View,分別重寫他們的這些方法绩鸣,在重寫的時候打上log怀大。在不添加任何監(jiān)聽(即沒有View消費事件)的條件下看一下運行結(jié)果:
點擊外部ViewGroup:
點擊子View:
可以看到,事件分發(fā)首先由ViewGroup的dispatchTouchEvent()方法開始呀闻,先調(diào)用自己的onInterceptTouchEvent()方法判斷是否攔截化借,返回false表示自己沒有攔截,那么接下來直接把事件傳給子View捡多。子View調(diào)用自己的dispatchTouchEvent()方法進行分發(fā)蓖康,因為View沒有onInterceptTouchEvent()方法,所以不存在攔截操作垒手,因此直接將事件交給自己的onTouchEvent()方法消費蒜焊。因為我的子View沒有使用這個事件,因此onTouchEvent()方法直接返回了false表示自己沒有消費科贬,那么這個事件此時就算是傳到底了泳梆。因為自己沒有消費,因此自己就沒有分發(fā)出去榜掌,那么子View的dispatchTouchEvent()方法返回false优妙,把這個事件交還給上一層的ViewGroup。ViewGroup發(fā)現(xiàn)這個事件沒有子View消費憎账,那么就自己動手吧套硼!將事件傳給自己的onTouchEvent()方法消費∈蟾纾可是ViewGroup也沒有消費熟菲,那么onTouchEvent()方法只能是再返回false了看政。同理朴恳,ViewGroup自己沒有消費事件,因此他的dispatchTouchEvent()方法也返回了false允蚣。這段文字說得可能有點亂于颖,那么就貼一張圖來演示一下:(圖中紅色箭頭表示事件自頂向下分發(fā)的過程,黃色則表示自底向上返回的過程)
接下來嚷兔,我在子View上添加OnClick監(jiān)聽森渐,再看一下點擊子View時的運行結(jié)果:
乍一看做入,呀,怎么重復(fù)打印了兩遍log?其實并不是哪里寫錯了同衣。前面我說了竟块,事件分發(fā)分發(fā)的是一個事件序列,我添加了點擊事件耐齐,那么我就要消費點擊事件浪秘。而點擊事件其實是要分成兩個事件的,即ACTION_DOWN + ACTION_UP ,只有這樣才算是一次點擊事件埠况。因此打印了“兩遍”log其實是先打印了ACTION_DOWN的分發(fā)流程耸携,再打印了一遍ACTION_UP的分發(fā)流程,因此會看到最后一行打印了click事件辕翰。即夺衍,click事件是在ACTION_UP事件發(fā)生后才發(fā)生的。
然后看看各個方法的返回值喜命。果然由于我的子View明確表示要消費這個事件序列沟沙,因此從ACTION_DOWN開始的所有事件就都交給他消費了。所以子View的onTouchEvent的返回值為true壁榕,表示自己需要消費這個事件尝胆,然后他的dispatchTouchEvent也返回了true,表示這一事件被自己分發(fā)了护桦。既然自己的子View消費了事件含衔,ViewGroup就認為這一事件是被自己分發(fā)了,因此他的dispatchTouchEvent也就返回了true二庵。還是來一張圖更清楚一點:
最后贪染,我在上一步的基礎(chǔ)上,給ViewGroup的onInterceptTouchEvent()方法返回值強行改為true催享,表示事件傳到這一層的時候就被攔截了杭隙,看一下log:
果然,雖然我要在子View消費事件因妙,但是事件在傳到子View之前就被ViewGroup攔截了痰憎,那么事件就只會由ViewGroup來消費了,所以ViewGroup就把事件傳給了自己的onTouchEvent()來消費攀涵。再來一張圖:
綜上铣耘,事件分發(fā)的大致流程就是這樣。
問題二:如何保證統(tǒng)一序列的事件都交給一個View來處理
先上結(jié)論:在傳遞過程中以故,只要有一個View主動去消費了第一個事件(ACTION_DOWN)蜗细,那么ViewGroup會將這個View保存起來,之后同一事件序列的其他事件都直接交給這個View來處理。具體怎么操作炉媒,需要看一下源碼:
//這是ViewGroup dispatchTouchEvent()的源碼:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//省略前面一部分無關(guān)代碼
//handled是返回的結(jié)果踪区,表示是否被分發(fā),默認當(dāng)然是
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 判斷一下是不是ACTION_DOWN吊骤,如果是的話缎岗,代表一個新的事件序列來臨了
if (actionMasked == MotionEvent.ACTION_DOWN) {
//要注意一下這兩個方法,在這里會做一下相當(dāng)于是“清零”的操作
//在這里包含了諸如mFirstTouchTarget=null這樣的初始化操作
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// intercepted是用來記錄是否被攔截的結(jié)果
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 {
// 沒有mFirstTouchTarget白粉,同時事件為非ACTION_DOWN密强,那么就算要在這里攔截了
intercepted = true;
}
//忽略部分攔截相關(guān)的代碼
//這兩個對象記一下,后面會碰到
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// 這里就開始對事件類型區(qū)分了蜗元,如果是ACTION_DOWN或渤,那么就算是一個新的事件序列開始
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 準(zhǔn)備一下,接下來開始遍歷自己的子View們
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
// 獲取到點擊的坐標(biāo)奕扣,用來從子View中篩選出點擊到的VIEW
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// 按從后向前的順序開始遍歷子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們過濾掉
//一個一個continue就表示在發(fā)現(xiàn)View不合適的時候直接進入下一次循環(huán)
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//終于找到了合適的子View,注意這里將子View封裝為一個target
//要是返回的結(jié)果不為空就跳出循環(huán)
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;
}
//就算返回結(jié)果為空也沒關(guān)系薪鹦,在這里繼續(xù)遞歸的調(diào)用子View的dispatchTransformedTouchEvent()
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
if (preorderedList != null) preorderedList.clear();
}
//沒有找到要接受事件的View
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
//接下來就是對于非ACTION_DOWN事件的分發(fā)了,這里有兩種情況
if (mFirstTouchTarget == null) {
// 1.壓根就沒有找到要接受事件的view惯豆,或者被攔截了池磁,調(diào)用了自身的dispatchTransformedTouchEvent()且穿了一個null的View進去,這樣有什么用呢楷兽?需要后面分析dispatchTransformedTouchEvent()
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//2.有View接受ACTION_DOWN事件地熄,那么這個View也將接受其余的事件
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
//alreadyDispatchedToNewTouchTarget這個變量在前面View接受ACTION_DOWN事件時設(shè)為了true
//同時這個mFirstTouchTarget也就是那個View封裝好的target
//那么這個返回值handled就為true
handled = true;
} else {
//對于非ACTION_DOWN事件,依然是遞歸調(diào)用dispatchTransformedTouchEvent
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;
}
}
// 處理ACTION_UP和ACTION_CANCEL
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
接下來看看dispatchTransformedTouchEvent()的源碼:
//前面在分析dispatchTouchEvent()的時候發(fā)現(xiàn)有多處調(diào)用了這個dispatchTransformedTouchEvent(),而且有的地方傳來的第三個參數(shù)是null
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
//處理ACTION_CANCEL
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;
}
//忽略部分代碼……
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
//如果傳來的參數(shù)child為空時芯杀,調(diào)用自身dispatchTouchEvent()
handled = super.dispatchTouchEvent(event);
} else {
//不為空端考,那么就調(diào)用他的dispatchTouchEvent()
handled = child.dispatchTouchEvent(event);
}
return handled;
}
} else {
//...
}
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);
}
// Done.
transformedEvent.recycle();
return handled;
}
上面是對dispatchTouchEvent()和dispatchTransformedTouchEvent()的分析,看起來有點亂揭厚,這里梳理一下:
- 首先明確一點却特,事件分發(fā)是從ViewGroup的dispatchTouchEvent()開始的
- ViewGroup在遇到一個新的事件序列,即事件ACTION_DOWN時筛圆,開始遍歷自己的所有子View,找到需要接收到事件的View
- 無論是否找到裂明,都會調(diào)用dispatchTransformedTouchEvent()方法,區(qū)別在于如果找到了,那么在這個方法中傳入的是那個View太援,否則就是null
- dispatchTransformedTouchEvent()方法中第三個參數(shù)child為空時闽晦,會調(diào)用父類的dispatchTouchEvent()方法,否則會調(diào)用那個child的dispatchTouchEvent()方法提岔∠沈龋總而言之,都會去調(diào)用View類的dispatchTouchEvent()方法唧垦。
- dispatchTransformedTouchEvent()方法是進行具體的事件分發(fā)捅儒,除了OnClick()等事件外液样,onTouchEvent()方法就是在這里調(diào)用的
- 只要找到了要接受事件的View,就會將他封裝為一個target,保存起來振亮,后續(xù)的其他事件都由他來接受
問題三:OnClick OnLongClick等對外的監(jiān)聽是在哪里處理的巧还?
首先想一想一個很簡單的邏輯,OnClick事件是先ACTION_DOWN之后再ACTION_UP,所以必定要在onTouchEvent()處理坊秸。同理麸祷,OnLongClick是在保持ACTION_DOWN一段時間后發(fā)生,因此也要在onTouchEvent()中處理褒搔〗纂梗看看源碼,發(fā)現(xiàn)果然是在這里:
//以下源碼均為忽略了不想關(guān)部分星瘾,只保留了重點
public boolean onTouchEvent(MotionEvent event) {
//...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 處理click
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
}
break;
case MotionEvent.ACTION_DOWN:
// a short period in case this is a scroll.
if (isInScrollingContainer) {
//...
} else {
// 處理longclick
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
//...
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
//...
break;
}
return true;
}
return false;
}
根據(jù)前面的分析走孽,在View的dispatchTouchEvent()方法中,會對
public boolean dispatchTouchEvent(MotionEvent event) {
//...
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
//...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//只要獲取到的ListenerInfo不為空琳状,就說明我們設(shè)置了監(jiān)聽磕瓷,那么就會認為我們想讓這個View處理所有事件
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {//所以會在這里執(zhí)行onTouch()
result = true;
}
//而如果沒有處理,那么再調(diào)用onTouchEvent(),直到onTouchEvent()也返回false才會認為該View不消費事件
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
可以看到念逞,在View的dispatchTouchEvent()方法中困食,會通過查看是否由設(shè)置監(jiān)聽器等方法來判斷是否要消費事件。onTouchEvent()方法永遠會調(diào)用翎承,click和longclick都在這里面硕盹。而無論內(nèi)部如何處理,只要返回了true叨咖,就會認為消費了這一事件瘩例。
分析就到這了,作為一個小菜雞甸各,分析過程難免有些錯誤和疏漏仰剿,歡迎在評論區(qū)告訴我