了解Activity的構(gòu)成
一個(gè)Activity包含了一個(gè)Window對象,這個(gè)對象是由PhoneWindow來實(shí)現(xiàn)的。PhoneWindow將DecorView作為整個(gè)應(yīng)用窗口的根View,而這個(gè)DecorView又將屏幕劃分為兩個(gè)區(qū)域:一個(gè)是TitleView,另一個(gè)是ContentView,而我們平時(shí)所寫的就是展示在ContentView中的。
觸摸事件的類型
觸摸事件對應(yīng)的是MotionEvent類,事件的類型主要有如下三種:
ACTION_DOWN
ACTION_MOVE(移動的距離超過一定的閾值會被判定為ACTION_MOVE操作)
ACTION_UP
View事件分發(fā)本質(zhì)就是對MotionEvent事件分發(fā)的過程。即當(dāng)一個(gè)MotionEvent發(fā)生后,系統(tǒng)將這個(gè)點(diǎn)擊事件傳遞到一個(gè)具體的View上。
事件分發(fā)流程
事件分發(fā)過程由三個(gè)方法共同完成:
dispatchTouchEvent:方法返回值為true表示事件被當(dāng)前視圖消費(fèi)掉;返回為super.dispatchTouchEvent表示繼續(xù)分發(fā)該事件,返回為false表示交給父類的onTouchEvent處理。
onInterceptTouchEvent:方法返回值為true表示攔截這個(gè)事件并交由自身的onTouchEvent方法進(jìn)行消費(fèi);返回false表示不攔截,需要繼續(xù)傳遞給子視圖。如果return super.onInterceptTouchEvent(ev), 事件攔截分兩種情況:
1.如果該View存在子View且點(diǎn)擊到了該子View, 則不攔截, 繼續(xù)分發(fā)
給子View 處理, 此時(shí)相當(dāng)于return false。
2.如果該View沒有子View或者有子View但是沒有點(diǎn)擊中子View(此時(shí)ViewGroup
相當(dāng)于普通View), 則交由該View的onTouchEvent響應(yīng),此時(shí)相當(dāng)于return true。
注意:一般的LinearLayout、 RelativeLayout、FrameLayout等ViewGroup默認(rèn)不攔截, 而
ScrollView、ListView等ViewGroup則可能攔截,得看具體情況。
onTouchEvent:方法返回值為true表示當(dāng)前視圖可以處理對應(yīng)的事件;返回值為false表示當(dāng)前視圖不處理這個(gè)事件,它會被傳遞給父視圖的onTouchEvent方法進(jìn)行處理。如果return super.onTouchEvent(ev),事件處理分為兩種情況:
1.如果該View是clickable或者longclickable的,則會返回true, 表示消費(fèi)
了該事件, 與返回true一樣;
2.如果該View不是clickable或者longclickable的,則會返回false, 表示不
消費(fèi)該事件,將會向上傳遞,與返回false一樣。
注意:在Android系統(tǒng)中,擁有事件傳遞處理能力的類有以下三種:
Activity:擁有分發(fā)和消費(fèi)兩個(gè)方法。
ViewGroup:擁有分發(fā)、攔截和消費(fèi)三個(gè)方法。
View:擁有分發(fā)、消費(fèi)兩個(gè)方法。
三個(gè)方法的關(guān)系用偽代碼表示如下:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}
else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
通過上面的偽代碼,我們可以大致了解點(diǎn)擊事件的傳遞規(guī)則:對應(yīng)一個(gè)根ViewGroup來說,點(diǎn)擊事件產(chǎn)生后,首先會傳遞給它,這是它的dispatchTouchEvent就會被調(diào)用,如果這個(gè)ViewGroup的onInterceptTouchEvent方法返回true就表示它要攔截當(dāng)前事件,接著事件就會交給這個(gè)ViewGroup處理,這時(shí)如果它的mOnTouchListener被設(shè)置,則onTouch會被調(diào)用,否則onTouchEvent會被調(diào)用。在onTouchEvent中,如果設(shè)置了mOnCLickListener,則onClick會被調(diào)用。只要View的CLICKABLE和LONG_CLICKABLE有一個(gè)為true,onTouchEvent()就會返回true消耗這個(gè)事件。如果這個(gè)ViewGroup的onInterceptTouchEvent方法返回false就表示它不攔截當(dāng)前事件,這時(shí)當(dāng)前事件就會繼續(xù)傳遞給它的子元素,接著子元素的dispatchTouchEvent方法就會被調(diào)用,如此反復(fù)直到事件被最終處理。
一些重要的結(jié)論:
1、事件傳遞優(yōu)先級:onTouchListener.onTouch > onTouchEvent > onClickListener.onClick。
2、正常情況下,一個(gè)時(shí)間序列只能被一個(gè)View攔截且消耗。因?yàn)橐坏┮粋€(gè)元素?cái)r截了此事件,那么同一個(gè)事件序列內(nèi)的所有事件都會直接交給它處理(即不會再調(diào)用這個(gè)View的攔截方法去詢問它是否要攔截了,而是把剩余的ACTION_MOVE、ACTION_DOWN等事件直接交給它來處理)。特例:通過將重寫View的onTouchEvent返回false可強(qiáng)行將事件轉(zhuǎn)交給其他View處理。
3、如果View不消耗除ACTION_DOWN以外的其他事件,那么這個(gè)點(diǎn)擊事件會消失,此時(shí)父元素的onTouchEvent并不會被調(diào)用,并且當(dāng)前View可以持續(xù)收到后續(xù)的事件,最終這些消失的點(diǎn)擊事件會傳遞給Activity處理。
4、ViewGroup默認(rèn)不攔截任何事件(返回false)。
5、View的onTouchEvent默認(rèn)都會消耗事件(返回true),除非它是不可點(diǎn)擊的(clickable和longClickable同時(shí)為false)。View的longClickable屬性默認(rèn)都為false,clickable屬性要分情況,比如Button的clickable屬性默認(rèn)為true,而TextView的clickable默認(rèn)為false。
6、View的enable屬性不影響onTouchEvent的默認(rèn)返回值。
7、通過requestDisallowInterceptTouchEvent方法可以在子元素中干預(yù)父元素的事件分發(fā)過程,但是ACTION_DOWN事件除外。
如何解決View的事件沖突?舉個(gè)開發(fā)中遇到的例子?
常見開發(fā)中事件沖突的有ScrollView與RecyclerView的滑動沖突、RecyclerView內(nèi)嵌同時(shí)滑動同一方向。
滑動沖突的處理規(guī)則:
對于由于外部滑動和內(nèi)部滑動方向不一致導(dǎo)致的滑動沖突,可以根據(jù)滑動的方向判斷誰來攔截事件。
對于由于外部滑動方向和內(nèi)部滑動方向一致導(dǎo)致的滑動沖突,可以根據(jù)業(yè)務(wù)需求,規(guī)定何時(shí)讓外部View攔截事件,何時(shí)由內(nèi)部View攔截事件。
對于上面兩種情況的嵌套,相對復(fù)雜,可同樣根據(jù)需求在業(yè)務(wù)上找到突破點(diǎn)。
滑動沖突的實(shí)現(xiàn)方法:
外部攔截法:指點(diǎn)擊事件都先經(jīng)過父容器的攔截處理,如果父容器需要此事件就攔截,否則就不攔截。具體方法:需要重寫父容器的onInterceptTouchEvent方法,在內(nèi)部做出相應(yīng)的攔截。
內(nèi)部攔截法:指父容器不攔截任何事件,而將所有的事件都傳遞給子容器,如果子容器需要此事件就直接消耗,否則就交由父容器進(jìn)行處理。具體方法:需要配合requestDisallowInterceptTouchEvent方法。