一、前言:
觸摸事件的處理對(duì)于android手機(jī)來(lái)說(shuō)恐怕是最重要的一個(gè)機(jī)制了,當(dāng)你在使用手機(jī)時(shí),絕大多事都是通過(guò)觸摸屏幕來(lái)控制手機(jī)的。所以把觸摸事件搞清楚對(duì)于我們理解android系統(tǒng),開(kāi)發(fā)android應(yīng)用來(lái)說(shuō),都有著非常重要的意義。
對(duì)于一個(gè)初學(xué)者來(lái)說(shuō),搞清楚觸摸事件的處理機(jī)制不是一件簡(jiǎn)單的事情,本文將觸摸事件的講解分為三步,由淺入深,循序漸進(jìn)的為讀者講解,希望這遍文章對(duì)讀者能有所幫助。
二、標(biāo)準(zhǔn)模型:事件的傳遞和消費(fèi)
我們都知道android中的view能夠響應(yīng)觸摸事件,一般情況下是通過(guò)重寫(xiě)該View的onTouchEvent(MotionEvent event)方法來(lái)實(shí)現(xiàn)的,如果該方法返回true,意思是說(shuō)當(dāng)前對(duì)象需要消費(fèi)觸摸事件,如果返回false,那就是說(shuō)當(dāng)前這個(gè)view對(duì)象不需要消費(fèi)觸摸事件。那么現(xiàn)在問(wèn)題來(lái)了,看下圖當(dāng)中:
外框是一個(gè)普通的線性布局,布局當(dāng)中有一個(gè)ImageView圖片,紅色的點(diǎn)是我們觸摸的位置,那么這個(gè)觸摸事件是應(yīng)由誰(shuí)來(lái)處理呢?我們先來(lái)回答一個(gè)問(wèn)題:外面的布局和里面的圖片,誰(shuí)先收到這個(gè)觸摸事件?答案是外面的布局,事件總是由最外層的布局,一層一層向里面?zhèn)鬟f的,最終傳遞給了這張圖片。
如果這張圖片需要響應(yīng)事件,即這個(gè)ImageView的onTouchEvent方法返回true,那么事件就由這個(gè)ImageView來(lái)處理;如果這個(gè)圖片不需要處理事件,那么事件就交由圖片外面的布局來(lái)處理,即,去判斷布局對(duì)象的onTouchEvent方法返回true,還是返回false。
一句話的經(jīng)驗(yàn):事件的傳遞是由外向里一層層的傳遞的,而消費(fèi)時(shí),是由里向外一層層的判斷,最終找到某一個(gè)需要處理事件的對(duì)象。如下圖所示:
記憶小技巧:我們可以將頂級(jí)父view當(dāng)做爺爺,父view就是父親,子view就是兒子,而觸摸事件就是一個(gè)蘋(píng)果,爺爺拿到一個(gè)蘋(píng)果,給了父親,父親又給了兒子,而兒子正好需要這個(gè)蘋(píng)果,就把蘋(píng)果給吃掉了,即兒子這個(gè)對(duì)象的onTouchEvent方法返回true,如果兒子現(xiàn)在不想吃蘋(píng)果,對(duì)這個(gè)蘋(píng)果不感興趣,那么就把這個(gè)蘋(píng)果又還給了父親,由父親來(lái)判斷是否來(lái)消費(fèi)這個(gè)蘋(píng)果,就是看父view中的onTouchEvent方法是返回true還是返回false,如此循環(huán),以次類推。
知識(shí)點(diǎn)說(shuō)明:本文中為了便于理解,判斷view是否處理事件,就是看該view的onTouchEvent方法是返回true,還是返回false來(lái)判斷的。但我們都知道,一個(gè)view除了可以重寫(xiě)onTouchEvent方法外,還可以通過(guò)設(shè)置一個(gè)setOnTouchListener 來(lái)處理touch事件,那如果二個(gè)動(dòng)作都做了,情況會(huì)是如何呢?
看類View中的如下代碼:
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null
&& mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
} |
這里可以很明顯的看出,如果一個(gè)view有touchListener對(duì)象,同時(shí)該對(duì)象的onTouch方法返回為true的時(shí)候,onTouchEvent方法根本就沒(méi)有機(jī)會(huì)執(zhí)行。
一個(gè)view是否消費(fèi)了事件,其實(shí)看的是dispatchTouchEvent方法的返回結(jié)果,如果沒(méi)有touchListener 的話,也可以認(rèn)為是看 onTouchEvent 方法的返回結(jié)果。
三、進(jìn)階:事件的中斷
前面所說(shuō)的是一個(gè)事件傳遞和消費(fèi)的標(biāo)準(zhǔn)模型,但這個(gè)模型有些簡(jiǎn)陋,不能適應(yīng)所有的情況,如下圖所示:
ListView的條目當(dāng)中有一個(gè)按鈕,點(diǎn)中這個(gè)按鈕,上下滑動(dòng)。在此場(chǎng)景中,如果按前面的標(biāo)準(zhǔn)模型來(lái)講,這個(gè)事件應(yīng)由按鈕來(lái)處理,但此時(shí)顯然并不是用戶的本意,用戶并非要真的點(diǎn)擊按鈕,而是要滑動(dòng)listView,事件應(yīng)該由ListView來(lái)處理,那這又是如何實(shí)現(xiàn)的呢?
我們先來(lái)考濾一個(gè)問(wèn)題,上面我們已經(jīng)說(shuō)過(guò)了,當(dāng)事件發(fā)生時(shí),總是父view先收到的事件,然后通過(guò)計(jì)算將該事件傳遞給正確的子view,這是一般情況,那么,還有個(gè)特殊情況,就是父view拿到事件以后,他改變主意了,他并沒(méi)有傳遞給子view,而是中斷了事件的正常傳遞,由自己直接來(lái)處理了。對(duì)應(yīng)的代碼為:
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
} |
這個(gè)方法默認(rèn)情況下返回false,意思就是:并不中斷事件的傳遞,按標(biāo)準(zhǔn)模型進(jìn)行,但如果某個(gè)ViewGroup重寫(xiě)該方法,并返回true,就意味著,當(dāng)事件傳遞到該ViewGroup時(shí),中斷了事件的正常傳遞,由當(dāng)前這個(gè)ViewGroup直接來(lái)處理該事件。于是我們可以將Touch事件的流程圖改進(jìn)如下:
任何一個(gè)父view都有能力中斷事件的正常傳遞,如果所有的父view都沒(méi)有中斷事件的正常傳遞,那么和前面的標(biāo)準(zhǔn)模型是一樣的,如果某個(gè)父view收到事件后,將事件中斷了,那么,就由當(dāng)前這個(gè)父view直接來(lái)處理該事件。
還拿之前的爺孫仨分蘋(píng)果的比喻來(lái)說(shuō)明中斷的問(wèn)題:現(xiàn)在爺爺最先拿到,按正常的處理,將蘋(píng)果傳遞給了父親,而父親現(xiàn)在正好想吃蘋(píng)果呢,于是,吧唧一口,把蘋(píng)果給吃掉了,那這樣兒子就收不到這個(gè)蘋(píng)果了。如上圖所示:父view的 onInterceptTouchEvent方法返回true,那么觸摸事件直接交收父view的onTouchEvent來(lái)處理,而后的操作和標(biāo)準(zhǔn)模型就一樣了。
四、終級(jí)必殺:事件傳遞機(jī)制的代碼分析
知道了事件的傳遞、中斷、消費(fèi)以后,普通的開(kāi)發(fā)工作就能夠滿足了,如果你對(duì)技術(shù)的追求永無(wú)止境的話,那么我們?cè)賮?lái)進(jìn)行深一步的研究。在標(biāo)準(zhǔn)摸型中,我們?cè)谥v解事件的傳遞和消費(fèi)時(shí),都是用文字,和圖表來(lái)說(shuō)明的,其實(shí)我們都知道,這些機(jī)制肯定有對(duì)應(yīng)的,可執(zhí)行的代碼。這些代碼就在類ViewGroup中的dispatchTouchEvent方法,(我們以android2.3的源碼來(lái)講解)
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction(); // 獲得觸摸的動(dòng)作類型
final float xf = ev.getX(); // 獲得觸摸點(diǎn)的X坐標(biāo)
final float yf = ev.getY(); // 獲得觸摸點(diǎn)的Y坐標(biāo)
final Rect frame = mTempRect; // 獲得一個(gè)臨時(shí)需要的矩形
// 判斷標(biāo)記位,一般情況下為 true
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {// 如果是down 事件,判斷點(diǎn)中的目標(biāo)是誰(shuí)
if (mMotionTarget != null) { // 如果之前有目標(biāo),那么清空目標(biāo)
mMotionTarget = null;
}
// 判斷 是否要中斷事件,
if (disallowIntercept || !onInterceptTouchEvent(ev)) { |
一開(kāi)始,做一些準(zhǔn)備性的工作,獲得觸摸點(diǎn)的X,Y坐標(biāo)等。如果當(dāng)前是down事件,那么就判斷當(dāng)前點(diǎn)擊的目標(biāo)是誰(shuí),每一個(gè)父view都有一個(gè)自己的目標(biāo),這些目標(biāo)串起來(lái),像鏈條一樣,直接指向最終消費(fèi)事件的對(duì)象。在這里調(diào)用onInterceptTouchEvent,默認(rèn)返回的是false ,意思是不中斷,沒(méi)有中斷,那就應(yīng)該找一下,看目標(biāo)是哪個(gè),如果中斷了,就不用找了,就由自己來(lái)處理事件了。
然后,我們看,是如何找的,繼續(xù)看:
// 判斷 是否要中斷事件, if (disallowIntercept || !onInterceptTouchEvent(ev)) {
final int scrolledXInt = (int) scrolledXFloat; // X坐標(biāo)點(diǎn)
final int scrolledYInt = (int) scrolledYFloat; // Y坐標(biāo)點(diǎn)
final View[] children = mChildren; // 獲得當(dāng)前所有的子view
final int count = mChildrenCount; // 當(dāng)前子view的數(shù)量,也就是這個(gè)數(shù)組的長(zhǎng)度
for (int i = count - 1; i >= 0; i--) { // 遍歷所有的子view
final View child = children[i]; // 獲得其中一個(gè)子view
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE ) { // 這個(gè)view是否可見(jiàn)
child.getHitRect(frame); // 獲得這個(gè)view的矩形區(qū)域
if (frame.contains(scrolledXInt, scrolledYInt)) { // 看這個(gè)區(qū)域是否包含當(dāng)前觸摸點(diǎn) |
通過(guò)這段代碼我們可以看出,父view查找子view是通過(guò)for循環(huán)獲得每一個(gè)子view的位置,然后,判斷這個(gè)位置是否包含了觸摸點(diǎn)的坐標(biāo),如果包含了,就是說(shuō),點(diǎn)中了這個(gè)子view,通過(guò)標(biāo)準(zhǔn)模型我們知道,下一步就該將這個(gè)事件傳遞給子view,收子view來(lái)處理:
if (frame.contains(scrolledXInt, scrolledYInt)) { // 看這個(gè)區(qū)域是否包含當(dāng)前觸摸點(diǎn)
final float xc = scrolledXFloat - child.mLeft; // 對(duì)X坐標(biāo)進(jìn)行換算
final float yc = scrolledYFloat - child.mTop; // 對(duì)Y坐標(biāo)進(jìn)行換算
ev.setLocation(xc, yc); // 將新坐標(biāo)設(shè)置給 MotionEvent 對(duì)象
if (child.dispatchTouchEvent(ev)) { // 將這個(gè)事件,交由子view進(jìn)行處理
mMotionTarget = child;
return true;
}
} |
如果點(diǎn)中了當(dāng)前子view,首先將event的坐標(biāo)進(jìn)行換算,以保證,我們?cè)谔幚韙ouch時(shí)用,event.getX()方法獲得的X坐標(biāo),是以前這個(gè)view的左上角為原點(diǎn)的坐標(biāo)。其中child.mLeft是子view在父view中左邊界的距離,child.mTop是子view在父view中上邊界的距離。
然后調(diào)用
if (child.dispatchTouchEvent(ev)) 語(yǔ)句,將事件傳遞給子view,此時(shí),這個(gè)child可能是一個(gè)布局,也可能只是一個(gè)普通的view,如果一個(gè)布局,那么我們?cè)谏厦嫠治龅拇a,會(huì)在這個(gè)child布局中,再一次被執(zhí)行,如此嵌套執(zhí)行。如果這個(gè)child不是布局,比如說(shuō)是一個(gè)ImageView,或TextView,那么,會(huì)去執(zhí)行這個(gè)view的dispatchTouchEvent方法,判斷該view是否消費(fèi)事件,該方法在標(biāo)準(zhǔn)模型中已經(jīng)有介紹,如果此時(shí)child.dispatchTouchEvent返回值是true,即消費(fèi)事件,那么當(dāng)前這個(gè)ViewGroup就有了目標(biāo),就是當(dāng)前這個(gè)child,同樣,當(dāng)前ViewGroup的父View就也有目標(biāo),就是當(dāng)前這個(gè)ViewGroup,如果循環(huán),我們就知道了,要消費(fèi)事件的目標(biāo)是誰(shuí)。
也就是說(shuō):在down事件發(fā)生時(shí),系統(tǒng)會(huì)確定點(diǎn)擊的目標(biāo)是誰(shuí),一但確定了目標(biāo),當(dāng)move事件發(fā)生時(shí),系統(tǒng)會(huì)直接將事件交給目標(biāo)來(lái)執(zhí)行:
// 將坐標(biāo)換算成點(diǎn)擊目標(biāo)的坐標(biāo)
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
return target.dispatchTouchEvent(ev); |
至此,標(biāo)準(zhǔn)模型中事件的傳遞和消費(fèi)的代碼邏輯就分析完了,知道了這些原理以后,在日常的工作和學(xué)習(xí)當(dāng)中,就不會(huì)再有陌人摸象的感覺(jué),對(duì)于事件的處理,就可以得心應(yīng)手,甚至改變默認(rèn)的處理機(jī)制,達(dá)到一些很神奇的效果。這也是android開(kāi)源的魅力所在,讓我們可以盡情的去研究他的原理,從而靈活應(yīng)用,達(dá)到自己想要的效果。
本文版權(quán)歸黑馬程序員Android+物聯(lián)網(wǎng)培訓(xùn)學(xué)院所有,歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明作者出處。謝謝!作者:黑馬程序員Android+物聯(lián)網(wǎng)培訓(xùn)學(xué)院首發(fā):http://android.itheima.com