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

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

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

一、前言:

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

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

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

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

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

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

知道了事件的傳遞、中斷、消費(fèi)以后,普通的開發(fā)工作就能夠滿足了,如果你對技術(shù)的追求永無止境的話,那么我們再來進(jìn)行深一步的研究。在標(biāo)準(zhǔn)摸型中,我們在講解事件的傳遞和消費(fèi)時(shí),都是用文字,和圖表來說明的,其實(shí)我們都知道,這些機(jī)制肯定有對應(yīng)的,可執(zhí)行的代碼。這些代碼就在類ViewGroup中的dispatchTouchEvent方法,(我們以android2.3的源碼來講解)
    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)是誰
            if (mMotionTarget != null) { // 如果之前有目標(biāo),那么清空目標(biāo)
                mMotionTarget = null;
            }
            // 判斷 是否要中斷事件,
            if (disallowIntercept || !onInterceptTouchEvent(ev)) {
一開始,做一些準(zhǔn)備性的工作,獲得觸摸點(diǎn)的X,Y坐標(biāo)等。如果當(dāng)前是down事件,那么就判斷當(dāng)前點(diǎn)擊的目標(biāo)是誰,每一個(gè)父view都有一個(gè)自己的目標(biāo),這些目標(biāo)串起來,像鏈條一樣,直接指向最終消費(fèi)事件的對象。在這里調(diào)用onInterceptTouchEvent,默認(rèn)返回的是false ,意思是不中斷,沒有中斷,那就應(yīng)該找一下,看目標(biāo)是哪個(gè),如果中斷了,就不用找了,就由自己來處理事件了。
然后,我們看,是如何找的,繼續(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ù)組的長度
    for (int i = count - 1; i >= 0; i--) { // 遍歷所有的子view
        final View child = children[i]; // 獲得其中一個(gè)子view
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE ) { // 這個(gè)view是否可見
            child.getHitRect(frame); // 獲得這個(gè)view的矩形區(qū)域
            if (frame.contains(scrolledXInt, scrolledYInt)) { // 看這個(gè)區(qū)域是否包含當(dāng)前觸摸點(diǎn)
通過這段代碼我們可以看出,父view查找子view是通過for循環(huán)獲得每一個(gè)子view的位置,然后,判斷這個(gè)位置是否包含了觸摸點(diǎn)的坐標(biāo),如果包含了,就是說,點(diǎn)中了這個(gè)子view,通過標(biāo)準(zhǔn)模型我們知道,下一步就該將這個(gè)事件傳遞給子view,收子view來處理:
    if (frame.contains(scrolledXInt, scrolledYInt)) { // 看這個(gè)區(qū)域是否包含當(dāng)前觸摸點(diǎn)
        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)) { // 將這個(gè)事件,交由子view進(jìn)行處理
            mMotionTarget = child;
            return true;
        }
    }
如果點(diǎn)中了當(dāng)前子view,首先將event的坐標(biāo)進(jìn)行換算,以保證,我們在處理touch時(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)) 語句,將事件傳遞給子view,此時(shí),這個(gè)child可能是一個(gè)布局,也可能只是一個(gè)普通的view,如果一個(gè)布局,那么我們在上面所分析的代碼,會(huì)在這個(gè)child布局中,再一次被執(zhí)行,如此嵌套執(zhí)行。如果這個(gè)child不是布局,比如說是一個(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)是誰。
也就是說:在down事件發(fā)生時(shí),系統(tǒng)會(huì)確定點(diǎn)擊的目標(biāo)是誰,一但確定了目標(biāo),當(dāng)move事件發(fā)生時(shí),系統(tǒng)會(huì)直接將事件交給目標(biāo)來執(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ì)再有陌人摸象的感覺,對于事件的處理,就可以得心應(yīng)手,甚至改變默認(rèn)的處理機(jī)制,達(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

 
分享到:
在線咨詢 我要報(bào)名
和我們在線交談!