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