Android學(xué)習(xí)筆記12 事件分發(fā)機制完全解析

事件分發(fā)機制枪蘑,是Android提供的一套完善的對觸摸事件進(jìn)行處理的機制,熟悉整個事件分發(fā)流程很有必要岖免,因為它也是Android中常見的滑動沖突問題解決的理論基礎(chǔ)岳颇。這幾天閱讀了《Android開發(fā)藝術(shù)探索》等書籍,總結(jié)如下颅湘。

一话侧、引入
二、事件分發(fā)機制
   1.概述
   2.詳細(xì)
三闯参、源碼解析
   1.ViewGroup事件分發(fā)
   2.View事件分發(fā)
四瞻鹏、滑動沖突解決
五、總結(jié)

一鹿寨、引入

在介紹Android事件分發(fā)機制之前新博,我們先看生活中的一個例子。公司里有三個角色脚草,老板赫悄,項目經(jīng)理,程序員馏慨。有一天老板接到一個任務(wù)埂淮,他將任務(wù)分配給項目經(jīng)理完成,項目經(jīng)理又把任務(wù)分給程序員写隶。程序員完成任務(wù)后倔撞,告訴項目經(jīng)理任務(wù)完成了,項目經(jīng)理再向老板報告任務(wù)完成了樟澜。從老板接到任務(wù)误窖,到老板最終去交付任務(wù),這是個完整的過程秩贰。

在這個過程中霹俺,可能會有其它情況。假如在一開始老板接到任務(wù)時毒费,決定自己完成丙唧,不需要把任務(wù)往下分配,那么老板就自己做觅玻,項目經(jīng)理和程序員就沒事想际。同樣培漏,如果項目經(jīng)理決定自己去做,那么就沒有程序員的事胡本。上面的這個例子其實就是任務(wù)在老板牌柄、項目經(jīng)理和程序員這三個角色間的傳遞過程,Android中屏幕上的觸摸事件就相當(dāng)于這個任務(wù)侧甫,事件分發(fā)就類似于這個傳遞過程珊佣。

二、事件分發(fā)機制

我們知道披粟,Android的界面可能是由多個視圖層層嵌套構(gòu)成咒锻,一個ViewGroup視圖組合中可以包含其它的ViewGroup以及View,當(dāng)一個觸摸事件發(fā)生時守屉,系統(tǒng)需要把這個事件傳遞給一個具體的View惑艇,由它來完成處理。從事件發(fā)生拇泛,到傳遞給具體的View去完成滨巴,這個傳遞的過程就是View的事件分發(fā)。

概述

在事件分發(fā)機制中碰镜,涉及到的幾個關(guān)鍵部分分別是:TouchEvent(觸摸事件)兢卵、ViewGroup(視圖組合)、View(視圖)绪颖。下面先對這幾個部分做個介紹秽荤。

  • TouchEvent(觸摸事件)

觸摸事件就是觸摸屏幕產(chǎn)生的動作事件,比如常見的手指按下柠横,移動窃款,抬起等等,Android為我們提供了一個專門的MotionEvent類牍氛,它包含了發(fā)生的動作事件以及相關(guān)坐標(biāo)信息晨继,利用MotionEvent,我們可以處理很多與動作相關(guān)的工作搬俊。

  • View

我們經(jīng)常提到View事件分發(fā)機制紊扬,其實這里指的是View以及ViewGroup,我們知道View是Android中所有控件的基類唉擂,而ViewGroup翻譯為視圖組合餐屎,它是繼承自View的,可以包含子控件玩祟。我們在接下來的討論中腹缩,會把ViewGroup和View分開討論。

詳解

上面介紹了一些事件分發(fā)的基本概念,下面對分發(fā)流程有個總體的把握藏鹊。Android中事件分發(fā)機制主要涉及到三個重要方法润讥,如下:

  • dispatchTouchEvent ( MotionEvent event ) 事件分發(fā)
  • onInterceptTouchEvent 決定是否攔截事件
  • onTouchEvent 處理事件

上面三個方法之間的關(guān)系大概如下,當(dāng)事件傳遞到某個View時盘寡,先執(zhí)行dispatchTouchEvent方法進(jìn)行事件分發(fā)楚殿,在這個方法內(nèi)會調(diào)用方法onInterceptTouchEvent方法來決定是否攔截,如果返回true表示攔截宴抚,則調(diào)用onTouchEvent進(jìn)行事件處理勒魔,否則繼續(xù)往下傳遞甫煞,執(zhí)行子View的dispatchTouchEvent方法菇曲。

需要注意一點,View沒有onInterceptTouchEvent方法抚吠,一旦有事件傳遞給它常潮,那么它的onTouchEvent方法就會被調(diào)用。ViewGroup默認(rèn)不攔截任何事件楷力,因為從源碼中可以看到ViewGroup的onInterceptTouchEvent方法默認(rèn)返回false.

我們知道喊式,四大組件中,Activity通常提供界面用于交互萧朝,我們會通過setContentView來設(shè)置界面布局岔留,一般如果我們不希望布局頂部出現(xiàn)一個標(biāo)題欄,我們可能會調(diào)用requestWindowFeature(Window.FEATURE_NO_TITLE);方法检柬,這里我們簡單了解一下Android的界面架構(gòu)献联。

界面上一個點擊事件發(fā)生時,它最先被傳遞的是給當(dāng)前的Activity何址,由Activity的dispatchTouchEvent來進(jìn)行事件分發(fā)里逆,而Activity內(nèi)部其實是包含一個Window的,這個抽象Window的實現(xiàn)是PhoneWindow用爪,Activity把事件傳遞給PhoneWindow原押,PhoneWindow里又包含DecorView,PhoneWindow繼續(xù)把事件傳遞給DecorView偎血,DecorWindow里包含有我們設(shè)置的布局诸衔,DecorView繼承自FrameLayout,事件最終傳遞給我們設(shè)置的布局颇玷,一般來說設(shè)置的布局是一個ViewGroup笨农。所以,觸摸事件最后就是在ViewGroup中的分發(fā)過程亚隙。

三磁餐、源碼解析

前面我們已經(jīng)提到,事件分發(fā)機制其實是觸摸事件在ViewGroup和View兩種情況下的分發(fā)過程,下面我們結(jié)合源碼來分析诊霹,因為View的過程相對來說較為簡單羞延,我們先看ViewGroup事件分發(fā)。

ViewGroup事件分發(fā)

ViewGroup事件分發(fā)過程簡述主要如下脾还,事件到達(dá)ViewGroup后會調(diào)用方法dispatchTouchEvent伴箩,在其中會調(diào)用onInterceptTouchEvent進(jìn)行判斷是否攔截,如果返回true表示攔截則事件由ViewGroup處理鄙漏,如果返回false不攔截嗤谚,則事件會傳遞給子View,子View的dispatchTouchEvent會被調(diào)用怔蚌。默認(rèn)情況下巩步,onInterceptTouchEvent返回false.

下面我們看下源碼。

1桦踊、首先是dispatchTouchEvent方法里判斷是否攔截椅野。

final boolean intercepted;

if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {

    //默認(rèn)是false 允許攔截
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

    if (!disallowIntercept) {
        
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); 

    } else {
        intercepted = false;
    }
}else {
    intercepted = true;
}

這里可以看到,ViewGroup會在兩種情況下進(jìn)行是否攔截的判斷籍胯,第一種是發(fā)生ACTION_DOWN事件竟闪,第二種是mFirstTouchTarget != null。第二種情況是指杖狼,ViewGroup是否不攔截事件并把事件交由子View處理炼蛤,如果是,那么mFirstTouchTarget != null就成立蝶涩。

進(jìn)行判斷時理朋,會看變量disallowIntercept的值,這個值默認(rèn)是false不允許攔截子寓,所以!disallowIntercept為true暗挑,然后調(diào)用onInterceptTouchEvent為false,即不攔截斜友。有種情況炸裆,如果ACTION_DOWN判斷時被ViewGroup攔截,那么mFirstTouchTarget!=null就不成立鲜屏,那么同一事件序列中的剩余事件ACTION_MOVE或者ACTION_UP來臨時烹看,不進(jìn)行判斷,直接攔截洛史。

這里有兩條結(jié)論惯殊,某個View一旦決定攔截一個事件后,那么系統(tǒng)會把同一個事件序列的其它方法都交給這個View處理也殖。某個View如果不消耗ACTION_DOWN事件交給了子View處理土思,那么同一個事件序列的其它方法都不會交給它處理务热。

2、當(dāng)ViewGroup不攔截事件己儒,事件分發(fā)給子View處理崎岂。

                        
//子View
final View[] children = mChildren;
//循環(huán)遍歷
for (int i = childrenCount - 1; i >= 0; i--) {
                            
     ... ...

     //如果子View接收不到事件 或者 不在播動畫 就不分發(fā)
     if (!canViewReceivePointerEvents(child)
           || !isTransformedTouchPointInView(x, y, child, null)) {
        ev.setTargetAccessibilityFocus(false);
        continue;
     }
                            
     //分發(fā)事件給子View
     newTouchTarget = getTouchTarget(child);
     if (newTouchTarget != null) {
                                
        newTouchTarget.pointerIdBits |= idBitsToAssign;
        break;
     }

     resetCancelNextUpFlag(child);
     //調(diào)用子元素的dispatchTouchEvent
     if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        // Child wants to receive touch within its bounds.
        mLastTouchDownTime = ev.getDownTime();
        if (preorderedList != null) {
        // childIndex points into presorted list, find original index
           for (int j = 0; j < childrenCount; j++) {
                if (children[childIndex] == mChildren[j]) {
                    mLastTouchDownIndex = j;
                    break;
                }
           }
        } else {
            mLastTouchDownIndex = childIndex;
        }
        mLastTouchDownX = ev.getX();
        mLastTouchDownY = ev.getY();
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        alreadyDispatchedToNewTouchTarget = true;
        break;
     }
                         
     ev.setTargetAccessibilityFocus(false);
}

可以看到大概流程如下,循環(huán)遍歷子View闪湾,判斷子元素能否接收到點擊事件冲甘。能否接收到事件主要由兩點衡量,一是是否在播放動畫途样,二是點擊事件的坐標(biāo)是否落在子元素的區(qū)域內(nèi)江醇。如果子元素滿足條件,則事件傳遞給子View處理何暇。dispatchTransformedTouchEvent方法里調(diào)用了子View的dispatchTouchEvent方法陶夜。

如果子View的dispatchTouchEvent返回true,那么終止子元素的遍歷赖晶,如果返回false律适,則繼續(xù)分發(fā)給下個子元素。如果遍歷所有的子元素后事件都沒處理遏插,那么ViewGroup就自己處理事件。


**綜上纠修,觸摸事件傳遞到ViewGroup時胳嘲,會執(zhí)行方法dispatchTouchEvent()進(jìn)行事件分發(fā),如果事件是Down類型(或者同一事件序列沒被攔截已經(jīng)交由子元素處理)扣草,那么就調(diào)用方法onInterceptTouchEvent進(jìn)行攔截判斷了牛,默認(rèn)情況下不會攔截事件。ViewGroup不攔截的話辰妙,那么就會遍歷它的子View鹰祸,判斷能否接收到事件,如果接收到那么就調(diào)用子View的dispatchTouchEvent方法繼續(xù)進(jìn)行分發(fā)密浑。如果遍歷子View后都沒處理事件蛙婴,那么ViewGroup自己處理事件。
**


View事件分發(fā)

View的事件分發(fā)比ViewGroup簡單尔破,因為View不包含子View街图,所以它只能自己處理事件。

下面是它的dispatchTouchEvent方法內(nèi)的部分源碼懒构。

    public boolean dispatchTouchEvent(MotionEvent event) {

        ...

        boolean result = false;

        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        ...

        return result;
    }

View對點擊事件的處理餐济,首先會判斷有沒有設(shè)置OnTouchListener,因為OnTouchListener的優(yōu)先級高于onTouchEvent胆剧。

onTouchEvent中絮姆,即使View處于不可用狀態(tài),照樣會消耗點擊事件。下面代碼可以看出來篙悯。

if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }

A disabled view that is clickable still consumes the touch events, it just doesn't respond to them冤灾,一個不可用的View仍然可以消耗事件,只是不做任何響應(yīng)辕近。

onTouchEvent中對點擊事件的具體處理流程大概如下韵吨,只要View的CLICKABLE和LONG_CLICKABLE有一個為true,那么它就會消耗事件移宅,返回true归粉。總的來說漏峰,View的可不可用不影響是否消耗事件糠悼,只要clickable或者longClickable有一個為true,那么它就會消耗事件浅乔。


**綜上倔喂,觸摸事件傳遞到View時,會執(zhí)行方法dispatchTouchEvent()進(jìn)行事件分發(fā)靖苇,這里會判斷有沒有設(shè)置OnTouchListener席噩,如果OnTouchListener的onTouch方法返回true,那么onTouchEvent就不會被調(diào)用贤壁。View的onTouchEvent默認(rèn)都會消耗事件悼枢,除非它是不可點擊的(clickable和longClickable同時為false),而View的enable屬性并不影響onTouchEvent的返回值脾拆。
**


四馒索、滑動沖突解決

上面主要主要介紹了View的事件分發(fā)機制的整個過程,在平常的開發(fā)中名船,在熟悉整個分發(fā)過程后绰上,滑動沖突問題應(yīng)該就不再是難題了。下面主要以一個典型的例子渠驼,介紹下滑動沖突問題的解決蜈块。

滑動沖突的產(chǎn)生主要是因為界面中內(nèi)外兩層都可以滑動,比如一個界面外部可以左右滑動渴邦,內(nèi)部可以上下滑動疯趟。這時就可以采取外部攔截法,前面我們提到分發(fā)過程中方法onInterceptTouchEvent主要是用于判斷是否攔截谋梭,那么外部攔截中我們可以重寫父容器的onInterceptTouchEvent方法信峻,根據(jù)需要決定是否攔截。

public boolean onInterceptHoverEvent(MotionEvent event) {

        boolean intercepted = false;

        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                break;

            case MotionEvent.ACTION_MOVE:
                if(父容器需要當(dāng)前點擊事件){
                    intercepted = true;
                }else {
                    intercepted = false;
                }
                break;

            case MotionEvent.ACTION_UP:
                break;

            default:
                break;
        }
        
        mLastXIntercept = x;
        mLastYIntercept = y;
        
        return intercepted;
    }

五瓮床、總結(jié)

到這里關(guān)于Android中View的事件分發(fā)機制就介紹的差不多了盹舞,歡迎指正批評产镐。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市踢步,隨后出現(xiàn)的幾起案子癣亚,更是在濱河造成了極大的恐慌,老刑警劉巖获印,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件述雾,死亡現(xiàn)場離奇詭異,居然都是意外死亡兼丰,警方通過查閱死者的電腦和手機玻孟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鳍征,“玉大人黍翎,你說我怎么就攤上這事⊙薮裕” “怎么了匣掸?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長氮双。 經(jīng)常有香客問我碰酝,道長,這世上最難降的妖魔是什么眶蕉? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任砰粹,我火速辦了婚禮,結(jié)果婚禮上造挽,老公的妹妹穿的比我還像新娘。我一直安慰自己弄痹,他們只是感情好饭入,可當(dāng)我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著肛真,像睡著了一般谐丢。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蚓让,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天乾忱,我揣著相機與錄音,去河邊找鬼历极。 笑死窄瘟,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的趟卸。 我是一名探鬼主播蹄葱,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼氏义,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了图云?” 一聲冷哼從身側(cè)響起惯悠,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎竣况,沒想到半個月后克婶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡丹泉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年汪拥,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片橘忱。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡实幕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出睬塌,到底是詐尸還是另有隱情泉蝌,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布揩晴,位于F島的核電站勋陪,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏硫兰。R本人自食惡果不足惜诅愚,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望劫映。 院中可真熱鬧违孝,春花似錦、人聲如沸泳赋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽祖今。三九已至校坑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間千诬,已是汗流浹背耍目。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留徐绑,地道東北人邪驮。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像泵三,于是被迫代替她去往敵國和親耕捞。 傳聞我的和親對象是個殘疾皇子衔掸,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,527評論 2 349

推薦閱讀更多精彩內(nèi)容