說在前面的話
事件分發(fā)是一個重點(diǎn)也是難點(diǎn),所以,本篇幅有點(diǎn)長,如果耐心看完本篇,相信讀者會有收獲的。同時,讀者也可以自己寫例子測試,畢竟,紙上得來終覺淺,絕知此事要躬行。但是,對于水平高的讀者,其實(shí)最好的方式是看源代碼,因?yàn)橐磺性蚨伎梢詮脑搭^找到答案。
關(guān)于事件分發(fā)
關(guān)于事件分發(fā),其實(shí)主要就是理解三個函數(shù),這三個函數(shù)分別是dispatchTouchEvent(MotionEvent ev),onInterceptTouchEvent(MotionEvent ev)以及onTouchEvent(MotionEvent ev),這里直接上一張圖:

這張圖將分發(fā)事件中的重要的三個函數(shù)之間的關(guān)系表達(dá)的比較清晰,這里再簡單的解釋一下上面的偽代碼:一個點(diǎn)擊事件產(chǎn)生后,會從外到內(nèi)傳遞,傳遞到根ViewGroup后,會調(diào)用根ViewGroup的dispatchTouchEvent方法,然后如果自己的onInterceptTouchEvent方法返回true,表示攔截事件,那么這個事件就會自己處理,也就是調(diào)用自己的onTouchEvent方法,如果返回false,就表示自己不攔截這個事件,事件就會傳遞給子元素,也就是調(diào)用子元素的dispatchTouchEvent方法,如此傳遞下去,直到事件被處理。
知道了關(guān)系以后,我們還需要了解這三個函數(shù),了解一個函數(shù),其實(shí)無非就是理解它的作用,傳入的參數(shù)以及返回值。
先說dispatchTouchEvent(MotionEvent ev):
這個函數(shù)的作用是分發(fā)事件,不管是ViewGroup還是View都有這個方法,它的返回值受本身的onInterceptTouchEvent(MotionEvent ev)和child.dispatchTouchEvent(MotionEvent ev)共同影響(從上面那張圖就可以看出來),返回的各個值的意義如下:
return true :表示該View內(nèi)部消化掉了所有事件。
return false :事件在本層不再繼續(xù)進(jìn)行分發(fā),這個false也就是本身的dispatchTouchEvent(MotionEvent ev)返回值,而這個返回值會回溯給上層控件的dispatchTouchEvent(MotionEvent ev),表示自己沒有接受這個事件,不管上層控件是view還是viewGroup,都是交由上層控件的onTouchEvent(MotionEvent ev)方法進(jìn)行消費(fèi)(如果本層控件已經(jīng)是Activity,那么事件將被系統(tǒng)消費(fèi)或處理)。
如果事件分發(fā)返回系統(tǒng)默認(rèn)的 super.dispatchTouchEvent(ev),事件將分發(fā)給本層的事件攔截onInterceptTouchEvent(MotionEvent ev)方法進(jìn)行處理,而不是super的onInterceptTouchEvent。因?yàn)?code>return super.dispatchTouchEvent(ev)會去運(yùn)行父viewGroup的dispatchTouchEvent(ev),然后運(yùn)行onInterceptTouchEvent,那么這個onInterceptTouchEvent是誰的呢?根據(jù)方法是基于對象的,所以就會運(yùn)行child的onInterceptTouchEvent(MotionEvent ev)也就是本層的事件攔截器,而不是super的onInterceptTouchEvent。詳情可以參考規(guī)則中的第十條。
然后是onInterceptTouchEvent(MotionEvent ev):
這個函數(shù)的作用是攔截事件,只有ViewGroup有這個方法,返回的各個值的意義如下:
return true :表示將事件進(jìn)行攔截,并將攔截到的事件交由本層控件 的 onTouchEvent 進(jìn)行處理;
return false :則表示不對事件進(jìn)行攔截,事件得以成功分發(fā)到子View。并由子View的dispatchTouchEvent進(jìn)行處理?!?br>
如果返回super.onInterceptTouchEvent(ev),默認(rèn)false,即表示不攔截該事件,這樣事件才能以分發(fā)下去。
最后是onTouchEvent(MotionEvent ev):
這個函數(shù)的作用是處理觸摸事件,ViewGroup和View都有這個方法,返回值的意義如下:
如果return true,表示onTouchEvent處理完事件后消費(fèi)了此次事件。此時事件終結(jié);
如果return fasle,則表示不響應(yīng)事件,如果是ACTION_DOWN事件,那么該事件將會不斷向上層View的onTouchEvent方法傳遞,直到某個View的onTouchEvent方法返回true,如果到了最頂層View還是返回false,那么事件就會交給Activity處理。且在同一個事件系列中,當(dāng)前View無法再次接收到該事件序列,如果不是ACTION_DOWN事件,那么不會返回給父view的onTouchEvent處理,而是給Activity處理,并且該view可以繼續(xù)接收該事件序列;
如果return super.onTouchEvent(event);,默認(rèn)是true,即表示處理事件。那這個和return true有什么區(qū)別呢?從代碼就可以看出來,return super.onTouchEvent(event)會執(zhí)行super.onTouchEvent(event)這個方法。比如,當(dāng)你繼承EditText后,重寫onTouchEvent(MotionEvent event)方法,如果你將return super.onTouchEvent(event);換成return true,就會發(fā)現(xiàn)當(dāng)你按返回取消輸入框,再次點(diǎn)擊自定義EditText時就會無法彈出輸入框,解決辦法可以是將return true修改成return super.onTouchEvent(event),或者是在之前調(diào)用一次super.onTouchEvent(event)方法,彈出輸入框是在action為ACTION_UP的時候彈出的。
重要的知識點(diǎn)(大家拿本子記一下,高考必考啊)
1.一個viewGroup一旦決定攔截事件(這里分兩種情況,一個是攔截了ACTION_DOWN事件,還有一個是沒有子View滿足分發(fā)事件的條件或者子view在ACTION_DOWN時返回了false),那么后面的事件序列都會交給它處理,并且不會再調(diào)用onInterceptTouchEvent(ev)方法, 當(dāng)ACTION_DOWN事件成功傳入子view的時候, 那么父ViewGroup在別的事件分發(fā)的時候,比如ACTION_MOVE,每次都會調(diào)用onInterceptTouchEvent來判斷是否攔截當(dāng)前事件。 也就是說,父ViewGroup的onInterceptTouchEvent不會再次調(diào)用的時機(jī)只是自己來處理這個事件,也就是自己的onTouchEvent被調(diào)用,只有這個時候才不會再次調(diào)用onInterceptTouchEvent,當(dāng)事件傳入子view來處理事件的時候,父ViewGroup都會每次都調(diào)用onInterceptTouchEvent來決定是否攔截當(dāng)前事件。
2.dispatchTouchEvent無論返回true還是false,事件都不再進(jìn)行分發(fā),只有當(dāng)其返回super.dispatchTouchEvent(ev),才表明其具有向下層分發(fā)的愿望,但是是否能夠分發(fā)成功,則需要經(jīng)過事件攔截onInterceptTouchEvent的審核。事件是否向上傳遞處理是由onTouchEvent的返回值決定的。
3.正常情況下,一個事件序列只能被一個view攔截且消耗,因?yàn)?,一旦決定攔截事件,那么這個事件只能被這個view消耗,并且它的onInterceptTouchEvent(ev)方法也不會再次調(diào)用(這里的攔截和規(guī)則一中的攔截是一樣的。這里的再次調(diào)用是指當(dāng)確定攔截事件后,除了在ACTION_DOWN時調(diào)用onInterceptTouchEvent(ev),后面都不調(diào)用,其實(shí)跟規(guī)則一中說的一樣),如果你想這個事件序列被多個view攔截消耗,那么你可以在攔截事件的那個view中的onTouchEvent()方法中調(diào)用你想讓其攔截事件的那個view的onTouchEvent()方法來實(shí)現(xiàn)。
4.view一旦用onTouchEvent()開始處理事件,如果沒有處理ATION_DOWN事件,那么同一個事件序列中的事件也不會交給他處理,會回溯給他的父控件,如果你處理了ACTION_DOWN但是沒有處理ACTION_MOVE或者ACTION_UP,那么這個事件還是被你消耗,不會調(diào)用父控件的onTouchEvent方法,最后會是Activity處理,后面的事件還是繼續(xù)交給你處理。其實(shí),這就類似現(xiàn)實(shí),如果別人第一次叫你做事,你沒做好,那么后面就都不會放心叫你做了,如果你第一次做好了,后面沒做好,別人還是會給你做的,所以,第一次很重要。
5.view的onTouchEvent默認(rèn)都是消費(fèi)事件的(返回true),除非是不可點(diǎn)擊的,也就是longClickable和clickable都為false,只有這個屬性會影響view的onTouchEvent的返回值,別的屬性不會,比如,Enabled屬性,就算是Enabled屬性為false,也就是disable狀態(tài),view的onTouchEvent默認(rèn)返回的還是true。
6.事件傳遞是由外向內(nèi)的,即事件總是傳遞給父元素,再由父元素分發(fā)給子控件,通過requestDisallowInterceptTouchEvent();方法可以在子元素中干預(yù)父元素的事件分發(fā)過程,但是不能干預(yù)ACTION_DOWN事件,因?yàn)楫?dāng)時ACTION_DOWN事件的時候,父元素會重置FLAG_DISALLOW_INTERCEPT標(biāo)志位。
7.使用內(nèi)部攔截法的時候,為了弄清楚順序,我就直接調(diào)試,結(jié)果,運(yùn)行到父元素的dispatchTouchEvent后,不會去調(diào)用父元素的onInterceptTouchEvent方法,直接就到了子元素的dispatchTouchEvent,依然會運(yùn)行到子view的onTouchEvent,等到ACTION_UP的時候才會又跑到父元素中的dispatchTouchEvent和onInterceptTouchEvent去判斷是否攔截ACTION_UP事件。我倒騰了一天,才發(fā)現(xiàn)是需要移動,也就是讓move多次調(diào)用才行,因?yàn)槭录怯赏庀騼?nèi)的,當(dāng)?shù)谝淮蜛CTION_MOVE事件到的時候,先運(yùn)行父ViewGroup的dispatchTouchEvent方法,此時FLAG_DISALLOW_INTERCEPT依然是設(shè)置成true,所以,不會運(yùn)行父ViewGroup的onInterceptTouchEvent方法,直接就會運(yùn)行子view的dispatchTouchEvent方法,然后FLAG_DISALLOW_INTERCEPT被設(shè)置成false,于是當(dāng)?shù)诙蔚腁CTION_MOVE到來的的時候,才會去運(yùn)行父viewGroup的onInterceptTouchEvent方法,然后子view收到ACTION_CANCEL事件,等到第三個ACTION_MOVE的時候父viewGroup才開始攔截事件。但是因?yàn)槲抑笆钦{(diào)試,所以都只有一次move事件,結(jié)果就不一樣了。也是醉了。并且使用內(nèi)部攔截法的時候,ACTION_UP事件也會被父view攔截,不會傳遞到子view中,也就意味著子view的onClick事件不會響應(yīng),這一點(diǎn)要記住。
8.內(nèi)部攔截法和外部攔截法的區(qū)別:內(nèi)部攔截法需要到該事件的第三個的時候才有用,也就是該事件的第一個依然被子view得到,外部攔截法則是到第二個就有用了,子view不會得到該事件的任何一個,比如,攔截ACTION_MOVE的時候,使用內(nèi)部攔截法在攔截第三個ACTION_MOVE的時候才攔截了,因?yàn)榈谝粋€ACTION_MOVE會被子view得到,而使用外部攔截法則是第二個ACTION_MOVE的時候就攔截了,因?yàn)樽觱iew不會得到ACTION_MOVE中的任何一個。詳情可以見9,10。所以,使用外部攔截法要好點(diǎn)。
9.當(dāng)viewGroup沒有攔截ACTION_DOWN而攔截了ACTION_MOVE或者ACTION_UP的時候,那么,第一個被攔截的動作不會在viewGroup中的onTouchEvent中觸發(fā),也不會在子view的onTouchEvent中觸發(fā),而是子view會受到ACTION_CANCEL事件。該事件序列后面的事件都會被攔截,并且下一個同類型的事件傳來時,不會再調(diào)用viewGroup的onInterceptTouchEvent方法,直接就調(diào)用viewGroup的onTouchEvent方法,這里解釋一下,什么是第一個被攔截的動作,比如,多個move的時候,第一個move就不會被父view或者子view執(zhí)行,感覺是這個事件變成了ACTION_CANCEL事件傳遞到了子view。也就是說,一旦在這種情況下,ACTION_UP事件永遠(yuǎn)不會被子view接收。也就意味著,不管是使用外部攔截法還是內(nèi)部攔截法,只要攔截了,那么子view就收不到ACTION_UP事件。還有就是ViewGroup就不要攔截ACTION_UP了,因?yàn)檫@樣大家都得不到ACTION_UP事件,何必呢?
10.為了講述方便,當(dāng)從一個ViewGroup分發(fā)事件到子ViewGroup時,在子ViewGroup的dispatchTouchEvent方法中調(diào)用父類的dispatchTouchEvent,發(fā)現(xiàn)不會繼續(xù)調(diào)用父類的onInterceprTouchEvent,而是直接調(diào)用子ViewGroup的onInterceptTouchEvent,為什么在這里調(diào)用父類的dispatchTouchEvent不會跟著調(diào)用父類的onInterceptTouchEvent?我調(diào)試和看源碼發(fā)現(xiàn)當(dāng)運(yùn)行到findChildWithAccessibilityFocus()方法時,view會變成接受到事件的view,然后就不知道了。水平還是看不懂源代碼。其實(shí)這是因?yàn)槲覍ava的理解有錯誤,基于方法都是基于對象的,所以在子viewGroup中調(diào)用父類的dispatchTouchEvent,也就是super.dispatchTouchEvent()時,這時會運(yùn)行到父viewGroup的dispatchTouchEvent里,會調(diào)用onInterceptTouchEvent方法,這時的onInterceptTouchEvent其實(shí)就已經(jīng)是子viewGroup的onInterceptTouchEvent方法,而不是父ViewGroup的dispatchTouchEvent方法,因?yàn)榉椒ㄊ腔趯ο蟮摹?/strong>
11.onClick發(fā)生的前提就是可點(diǎn)擊,并且收到了ACTION_DOWN和ACTION_UP事件。這里解釋一下,這里的收到了用詞不是那么準(zhǔn)確,應(yīng)該是能接收到事件,并且return super.onTouchEvent()了,記住是return super.onTouchEvent(),如果是return true都不行,因?yàn)閞eturn true沒有執(zhí)行view的onTouchEvent方法,而點(diǎn)擊事件是在ACTION_UP中設(shè)置的。即在ACTION_UP的時候源碼中調(diào)用了performClick()方法。這里貼一部分源代碼
TextView的源代碼:
final boolean superResult = super.onTouchEvent(event);
View的源代碼:
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}