首頁技術(shù)文章正文

Android+物聯(lián)網(wǎng)培訓(xùn)實戰(zhàn)教程之系統(tǒng)觸摸事件三步曲

更新時間:2017-05-31 來源:黑馬程序員Android+物聯(lián)網(wǎng)培訓(xùn)學(xué)院 瀏覽量:

一、前言:

觸摸事件的處理對于android手機來說恐怕是最重要的一個機制了,當(dāng)你在使用手機時,絕大多事都是通過觸摸屏幕來控制手機的。所以把觸摸事件搞清楚對于我們理解android系統(tǒng),開發(fā)android應(yīng)用來說,都有著非常重要的意義。
對于一個初學(xué)者來說,搞清楚觸摸事件的處理機制不是一件簡單的事情,本文將觸摸事件的講解分為三步,由淺入深,循序漸進(jìn)的為讀者講解,希望這遍文章對讀者能有所幫助。

二、標(biāo)準(zhǔn)模型:事件的傳遞和消費

我們都知道android中的view能夠響應(yīng)觸摸事件,一般情況下是通過重寫該View的onTouchEvent(MotionEvent event)方法來實現(xiàn)的,如果該方法返回true,意思是說當(dāng)前對象需要消費觸摸事件,如果返回false,那就是說當(dāng)前這個view對象不需要消費觸摸事件。那么現(xiàn)在問題來了,看下圖當(dāng)中:
外框是一個普通的線性布局,布局當(dāng)中有一個ImageView圖片,紅色的點是我們觸摸的位置,那么這個觸摸事件是應(yīng)由誰來處理呢?我們先來回答一個問題:外面的布局和里面的圖片,誰先收到這個觸摸事件?答案是外面的布局,事件總是由最外層的布局,一層一層向里面?zhèn)鬟f的,最終傳遞給了這張圖片。
如果這張圖片需要響應(yīng)事件,即這個ImageView的onTouchEvent方法返回true,那么事件就由這個ImageView來處理;如果這個圖片不需要處理事件,那么事件就交由圖片外面的布局來處理,即,去判斷布局對象的onTouchEvent方法返回true,還是返回false。
 一句話的經(jīng)驗:事件的傳遞是由外向里一層層的傳遞的,而消費時,是由里向外一層層的判斷,最終找到某一個需要處理事件的對象。如下圖所示:
記憶小技巧:我們可以將頂級父view當(dāng)做爺爺,父view就是父親,子view就是兒子,而觸摸事件就是一個蘋果,爺爺拿到一個蘋果,給了父親,父親又給了兒子,而兒子正好需要這個蘋果,就把蘋果給吃掉了,即兒子這個對象的onTouchEvent方法返回true,如果兒子現(xiàn)在不想吃蘋果,對這個蘋果不感興趣,那么就把這個蘋果又還給了父親,由父親來判斷是否來消費這個蘋果,就是看父view中的onTouchEvent方法是返回true還是返回false,如此循環(huán),以次類推。
知識點說明:本文中為了便于理解,判斷view是否處理事件,就是看該view的onTouchEvent方法是返回true,還是返回false來判斷的。但我們都知道,一個view除了可以重寫onTouchEvent方法外,還可以通過設(shè)置一個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方法的返回結(jié)果,如果沒有touchListener 的話,也可以認(rèn)為是看 onTouchEvent 方法的返回結(jié)果。

三、進(jìn)階:事件的中斷

前面所說的是一個事件傳遞和消費的標(biāo)準(zhǔn)模型,但這個模型有些簡陋,不能適應(yīng)所有的情況,如下圖所示:
ListView的條目當(dāng)中有一個按鈕,點中這個按鈕,上下滑動。在此場景中,如果按前面的標(biāo)準(zhǔn)模型來講,這個事件應(yīng)由按鈕來處理,但此時顯然并不是用戶的本意,用戶并非要真的點擊按鈕,而是要滑動listView,事件應(yīng)該由ListView來處理,那這又是如何實現(xiàn)的呢?
我們先來考濾一個問題,上面我們已經(jīng)說過了,當(dāng)事件發(fā)生時,總是父view先收到的事件,然后通過計算將該事件傳遞給正確的子view,這是一般情況,那么,還有個特殊情況,就是父view拿到事件以后,他改變主意了,他并沒有傳遞給子view,而是中斷了事件的正常傳遞,由自己直接來處理了。對應(yīng)的代碼為:
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }
這個方法默認(rèn)情況下返回false,意思就是:并不中斷事件的傳遞,按標(biāo)準(zhǔn)模型進(jìn)行,但如果某個ViewGroup重寫該方法,并返回true,就意味著,當(dāng)事件傳遞到該ViewGroup時,中斷了事件的正常傳遞,由當(dāng)前這個ViewGroup直接來處理該事件。于是我們可以將Touch事件的流程圖改進(jìn)如下:
任何一個父view都有能力中斷事件的正常傳遞,如果所有的父view都沒有中斷事件的正常傳遞,那么和前面的標(biāo)準(zhǔn)模型是一樣的,如果某個父view收到事件后,將事件中斷了,那么,就由當(dāng)前這個父view直接來處理該事件。
還拿之前的爺孫仨分蘋果的比喻來說明中斷的問題:現(xiàn)在爺爺最先拿到,按正常的處理,將蘋果傳遞給了父親,而父親現(xiàn)在正好想吃蘋果呢,于是,吧唧一口,把蘋果給吃掉了,那這樣兒子就收不到這個蘋果了。如上圖所示:父view的 onInterceptTouchEvent方法返回true,那么觸摸事件直接交收父view的onTouchEvent來處理,而后的操作和標(biāo)準(zhǔn)模型就一樣了。

四、終級必殺:事件傳遞機制的代碼分析

知道了事件的傳遞、中斷、消費以后,普通的開發(fā)工作就能夠滿足了,如果你對技術(shù)的追求永無止境的話,那么我們再來進(jìn)行深一步的研究。在標(biāo)準(zhǔn)摸型中,我們在講解事件的傳遞和消費時,都是用文字,和圖表來說明的,其實我們都知道,這些機制肯定有對應(yīng)的,可執(zhí)行的代碼。這些代碼就在類ViewGroup中的dispatchTouchEvent方法,(我們以android2.3的源碼來講解)
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final int action = ev.getAction(); // 獲得觸摸的動作類型
        final float xf = ev.getX(); // 獲得觸摸點的X坐標(biāo)
        final float yf = ev.getY(); // 獲得觸摸點的Y坐標(biāo)
 
        final Rect frame = mTempRect; // 獲得一個臨時需要的矩形
         // 判斷標(biāo)記位,一般情況下為 true
        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
 
        if (action == MotionEvent.ACTION_DOWN) {// 如果是down 事件,判斷點中的目標(biāo)是誰
            if (mMotionTarget != null) { // 如果之前有目標(biāo),那么清空目標(biāo)
                mMotionTarget = null;
            }
            // 判斷 是否要中斷事件,
            if (disallowIntercept || !onInterceptTouchEvent(ev)) {
一開始,做一些準(zhǔn)備性的工作,獲得觸摸點的X,Y坐標(biāo)等。如果當(dāng)前是down事件,那么就判斷當(dāng)前點擊的目標(biāo)是誰,每一個父view都有一個自己的目標(biāo),這些目標(biāo)串起來,像鏈條一樣,直接指向最終消費事件的對象。在這里調(diào)用onInterceptTouchEvent,默認(rèn)返回的是false ,意思是不中斷,沒有中斷,那就應(yīng)該找一下,看目標(biāo)是哪個,如果中斷了,就不用找了,就由自己來處理事件了。
然后,我們看,是如何找的,繼續(xù)看:
// 判斷 是否要中斷事件,
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
    final int scrolledXInt = (int) scrolledXFloat; // X坐標(biāo)點
    final int scrolledYInt = (int) scrolledYFloat; // Y坐標(biāo)點
    final View[] children = mChildren; // 獲得當(dāng)前所有的子view
    final int count = mChildrenCount; // 當(dāng)前子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ū)域是否包含當(dāng)前觸摸點
通過這段代碼我們可以看出,父view查找子view是通過for循環(huán)獲得每一個子view的位置,然后,判斷這個位置是否包含了觸摸點的坐標(biāo),如果包含了,就是說,點中了這個子view,通過標(biāo)準(zhǔn)模型我們知道,下一步就該將這個事件傳遞給子view,收子view來處理:
    if (frame.contains(scrolledXInt, scrolledYInt)) { // 看這個區(qū)域是否包含當(dāng)前觸摸點
        final float xc = scrolledXFloat - child.mLeft; // 對X坐標(biāo)進(jìn)行換算
        final float yc = scrolledYFloat - child.mTop; // 對Y坐標(biāo)進(jìn)行換算
        ev.setLocation(xc, yc); // 將新坐標(biāo)設(shè)置給 MotionEvent 對象
        if (child.dispatchTouchEvent(ev)) { // 將這個事件,交由子view進(jìn)行處理
            mMotionTarget = child;
            return true;
        }
    }
如果點中了當(dāng)前子view,首先將event的坐標(biāo)進(jìn)行換算,以保證,我們在處理touch時用,event.getX()方法獲得的X坐標(biāo),是以前這個view的左上角為原點的坐標(biāo)。其中child.mLeft是子view在父view中左邊界的距離,child.mTop是子view在父view中上邊界的距離。
然后調(diào)用 if (child.dispatchTouchEvent(ev)) 語句,將事件傳遞給子view,此時,這個child可能是一個布局,也可能只是一個普通的view,如果一個布局,那么我們在上面所分析的代碼,會在這個child布局中,再一次被執(zhí)行,如此嵌套執(zhí)行。如果這個child不是布局,比如說是一個ImageView,或TextView,那么,會去執(zhí)行這個view的dispatchTouchEvent方法,判斷該view是否消費事件,該方法在標(biāo)準(zhǔn)模型中已經(jīng)有介紹,如果此時child.dispatchTouchEvent返回值是true,即消費事件,那么當(dāng)前這個ViewGroup就有了目標(biāo),就是當(dāng)前這個child,同樣,當(dāng)前ViewGroup的父View就也有目標(biāo),就是當(dāng)前這個ViewGroup,如果循環(huán),我們就知道了,要消費事件的目標(biāo)是誰。
也就是說:在down事件發(fā)生時,系統(tǒng)會確定點擊的目標(biāo)是誰,一但確定了目標(biāo),當(dāng)move事件發(fā)生時,系統(tǒng)會直接將事件交給目標(biāo)來執(zhí)行:
        // 將坐標(biāo)換算成點擊目標(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)模型中事件的傳遞和消費的代碼邏輯就分析完了,知道了這些原理以后,在日常的工作和學(xué)習(xí)當(dāng)中,就不會再有陌人摸象的感覺,對于事件的處理,就可以得心應(yīng)手,甚至改變默認(rèn)的處理機制,達(dá)到一些很神奇的效果。這也是android開源的魅力所在,讓我們可以盡情的去研究他的原理,從而靈活應(yīng)用,達(dá)到自己想要的效果。



本文版權(quán)歸黑馬程序員Android+物聯(lián)網(wǎng)培訓(xùn)學(xué)院所有,歡迎轉(zhuǎn)載,轉(zhuǎn)載請注明作者出處。謝謝!
作者:黑馬程序員Android+物聯(lián)網(wǎng)培訓(xùn)學(xué)院
首發(fā):http://android.itheima.com

 
分享到:
在線咨詢 我要報名
和我們在線交談!