Android ViewGroup/View 事件分發(fā)機制詳解

博客原文鏈接:https://zhujun2730.github.io/2015/11/08/touchevent/

對于大多數(shù)Android開發(fā)者來說却音,Android的事件分發(fā)機制一直以來都是一塊心頭病改抡。似懂非懂的狀態(tài),應(yīng)該是大多數(shù)人的真實寫照系瓢。最近在看任玉剛老師寫的《Android開發(fā)藝術(shù)探索》阿纤,算是做個讀書筆記吧,希望能提供多一點啟發(fā)夷陋、多一點角度的理解欠拾。

一、事件分發(fā)機制的一些概念

事件分發(fā)的本質(zhì):其實就是對MotionEvent事件的分發(fā)過程骗绕。

1.1 為什么要有事件機制 藐窄?

當你在一個布局中,有一個LinearLayout酬土,里面又有一個小的LinearLayout荆忍,然后在這個小的LinearLayout中又有一個View,這個時候撤缴。這個時候刹枉,你點擊這個View,為什么LinearLayout不會響應(yīng)屈呕?其實你點擊的也是LinearLayout的區(qū)域啊微宝。帶著這個疑問,就可以猜想到了虎眨,事件分發(fā)機制蟋软,其實就是為了統(tǒng)一協(xié)調(diào)這些view的事件。

在這里安利一篇愛哥的《Android事件分發(fā)完全解析之為什么是她》嗽桩,這篇較生動的講解了事件機制的由來岳守,十分推薦一看。

1.2 MotionEvent 主要分為以下幾個事件類型:

  1. ACTION_DOWN 手指開始觸摸到屏幕的那一刻響應(yīng)的是DOWN事件
  2. ACTION_MOVE 接著手指在屏幕上移動響應(yīng)的是MOVE事件
  3. ACTION_UP 手指從屏幕上松開的那一刻響應(yīng)的是UP事件

所以事件順序是: ACTION_DOWN -> ACTION_MOVE -> ACTION_UP

1.3 事件分發(fā)機制的三個主要方法:

  • public boolean dispatchTouchEvent(MotionEvent event) —— 分發(fā)事件

    作用是用來進行事件的分發(fā)碌冶。一般在這個方法里必須寫 return super.dispatchTouchEvent 棺耍。如果不寫super.dispatchTouchEvent,而直接改成return true 或者 false种樱,則事件傳遞到這里時便終止了,既不會繼續(xù)分發(fā)也不會回傳給父元素俊卤。

  • public boolean onInterceptTouchEvent(MotionEvent event) —— 攔截事件

    只有ViewGroup才有這個方法嫩挤。View只有dispatchTouchEvent和onTouchEvent兩個方法。因為View沒有子View消恍,所以不需要攔截事件岂昭。而ViewGroup里面可以包裹子View,所以通過onInterceptTouchEvent方法狠怨,ViewGroup可以實現(xiàn)攔截约啊,攔截了的話邑遏,ViewGroup就不會把事件繼續(xù)分發(fā)給子View了,也就是說在這個ViewGroup中的子View都不會響應(yīng)到任何事件了恰矩。onInterceptTouchEvent 返回true時记盒,表示ViewGroup會攔截事件。

  • public boolean onTouchEvent(MotionEvent event) —— 消費事件

    onTouchEvent 返回true時外傅,表示事件被消費掉了纪吮。一旦事件被消費掉了,其他父元素的onTouchEvent方法都不會被調(diào)用萎胰。如果沒有人消耗事件碾盟,則最終當前Activity會消耗掉。則下次的MOVE技竟、UP事件都不會再傳下去了冰肴。

需要注意的一些事項:

  • 一般我們在自定義ViewGroup時不會攔截Down事件,因為一旦攔截了Down事件榔组,那么后續(xù)的Move和Up事件都不會再傳遞下去到子元素了熙尉,事件以后都會只交給ViewGroup這里。
  • 一個Down事件分發(fā)完了之后瓷患,還有回傳的過程骡尽。因為一個事件分發(fā)包括了Action_Down、Action_Move擅编、Action_Up這幾個動作攀细。當手指觸摸到屏幕的那一刻,首先分發(fā)Action_Down事件爱态,事件分發(fā)完后還要回傳回去谭贪,然后繼續(xù)從頭開始分發(fā),執(zhí)行下一個Aciton_Move操作锦担,直到執(zhí)行完Action_Up事件俭识,整個事件分發(fā)過程便到此結(jié)束。

1.4 事件分發(fā)機制的三個主要方法的關(guān)系:

【注:ViewGroupA洞渔、ViewGroupB套媚、View的布局結(jié)構(gòu)參考下面的布局圖】

當事件分發(fā)到ViewGroupA時,會執(zhí)行到ViewGroupA的dispatchTouchEvent方法磁椒。剛剛提到了堤瘤。在這里必須寫成return super.dispatchTouchEvent(ev);因為事件的分發(fā)需要ViewGroupA 在父類ViewGroup的dispatchTouchEvent中才能進行事件分發(fā)。否則不這樣寫浆熔,事件根本無法繼續(xù)分發(fā)下去本辐。

class ViewGroupA {

    public boolean dispatchTouchEvent(MotionEvent ev){
        
        return super.dispatchTouchEvent(ev);
    }

}

在ViewGroup的dispatchTouchEvent源碼中,簡單化的歸納了事件分發(fā)的整個流程。該代碼出自任老師之手慎皱。
【當consume 返回 true 時老虫,表明事件已經(jīng)被消費了∶6啵】

class ViewGroup {

    public boolean dispatchTouchEvent(MotionEvent ev){
        
        boolean consume = false;
        
        if(onInterceptTouchEvent(MotionEvent ev)) {
            consume = onTouchEvent(ev);
        } else {
            consume = child.dispatchTouchEvent(ev);
        }
        
        return consume;
    }

}

當ViewGroupA 的 onInterceptTouchEvent 方法返回true時祈匙,表示它要攔截事件,此時會執(zhí)行它自己的onTouchEvent方法地梨。當返回false時菊卷,表明它不想攔截,則事件會傳遞給子View child宝剖。于是開始執(zhí)行child.dispatchTouchEvent(ev)洁闰。

我們來看View的dispatchTouchEvent方法。

public boolean dispatchTouchEvent(MotionEvent event) {  
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
            mOnTouchListener.onTouch(this, event)) {  
        return true;  
    }  
    return onTouchEvent(event);  
} 

從View的dispatchTouchEvent方法中可以得出一個結(jié)論:
事件一旦分發(fā)到了View万细,則默認一定會執(zhí)行它的onTouchEvent方法扑眉,除非符合了if的三個條件

所以View的 onTouchEvent 方法如果返回true,則它的dispatchTouchEvent的返回值也會返回true赖钞。在ViewGroup 的dispatchTouchEvent 中則 consume 的值為true腰素,表示事件被消費。

結(jié)論:View / ViewGroup 事件消費是在onTouchEvent方法中被消費的雪营。

二弓千、事件分發(fā)機制的流程

下面通過demo案例來演示,詳細的說明事件分發(fā)的流程献起。ViewGroupA包裹ViewGroupB,ViewGroupB里面又包裹一個View洋访。我們現(xiàn)在來分析下它的事件分發(fā)執(zhí)行的流程。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity">

    <me.anany.ViewGroupA
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/holo_blue_bright">

        <me.anany.ViewGroupB
            android:layout_width="300dp"
            android:layout_height="300dp"
            android:background="@android:color/holo_green_dark">

            <me.anany.CustomView
                android:id="@+id/btn"
                android:text="Button"
                android:background="@android:color/holo_red_dark"
                android:layout_width="100dp"
                android:layout_height="100dp"
                />

        </me.anany.ViewGroupB>
    </me.anany.ViewGroupA>
</RelativeLayout>

2.1 點擊View區(qū)域但View不消耗事件

當一個事件產(chǎn)生后谴餐,它的傳遞流程是從:Activity -> Window ->View

下圖描述了姻政,當點擊View時,事件分發(fā)的執(zhí)行流程岂嗓、以及事件回傳的流程汁展。


流程圖解析:

  • 事件分發(fā)

當在屏幕上點擊一個View時,首先執(zhí)行到的是MainActivity的dispatchTouchEvent方法厌殉,這里便是事件分發(fā)的起點食绿。紅色箭頭流向便是事件分發(fā)的流向。

事件傳遞到ViewGroupA時公罕,因為它不攔截事件器紧,所以它要先去問它的子控件ViewGroupB是否要消費事件,然后將事件分發(fā)給ViewGroupB熏兄。事件到了ViewGroupB時,它不攔截事件,所以它也要先去問它的子控件們要不要消費事件摩桶,然后將事件分發(fā)給View桥状。事件到了View時開始執(zhí)行dispatchTouchEvent,因為已經(jīng)到了最底層了硝清,View接下來便開始執(zhí)行onTouchEvent方法來決定是否消費事件辅斟。

  • 事件回傳

由于View沒有消費事件,所以它開始回傳信息芦拿,(紫色箭頭的流向便是事件回傳方向)士飒,以告訴ViewGroupB我不消費事件了,view 的 onTouchEvent 便return false蔗崎。然后ViewGroupB才開始有權(quán)利決定我是否要開始消費事件(因為它已經(jīng)問過它的子控件是否要消費事件了酵幕,而它的子控件并沒有消費),所以開始執(zhí)行ViewGroupB的onTouchEvent方法缓苛,由于ViewGroupB也不消費事件芳撒,所以它也 return false 。事件繼續(xù)回傳給ViewGroupA未桥,這個時候它終于開始有權(quán)利決定我是否要消費事件了笔刹,所以開始執(zhí)行ViewGroupA的onTouchEvent方法,由于ViewGroupA也不感興趣不消費事件冬耿,所以它也return false舌菜。最終你們這些孩兒們都不消費事件,那事件最終只能扔給MainActivity去消費了亦镶。

下面這段Log日月,便記錄了事件分發(fā)的整個過程。由于ViewGroupA染乌、ViewGroupB山孔、View 在 Action_Down事件時,就沒有消費事件荷憋。所以后續(xù)的事件MOVE台颠、UP都只由MainActivity來處理了。


2.2 點擊View區(qū)域且View消耗事件

流程圖解析:

  • 事件分發(fā)

當在屏幕上點擊一個View時勒庄,首先執(zhí)行到的是MainActivity的dispatchTouchEvent方法串前,這里便是事件分發(fā)的起點。紅色箭頭流向便是事件分發(fā)的流向实蔽。

事件傳遞到ViewGroupA時荡碾,因為它不攔截事件,所以它要先去問它的子控件ViewGroupB是否要消費事件局装,然后將事件分發(fā)給ViewGroupB坛吁。事件到了ViewGroupB時劳殖,它不攔截事件,所以它也要先去問它的子控件們要不要消費事件拨脉,然后將事件分發(fā)給View哆姻。事件到了View時開始執(zhí)行dispatchTouchEvent,因為已經(jīng)到了最底層了玫膀,View接下來便開始執(zhí)行onTouchEvent方法來決定是否消費事件矛缨。

  • 事件回傳

由于View消費了事件,所以它開始回傳帖旨,(紫色箭頭的流向便是事件回傳方向)箕昭,以告訴ViewGroupB我已經(jīng)消費事件了,view 的 onTouchEvent 便return true解阅。然后ViewGroupB 收到了View return true 就知道事件已經(jīng)被View消費掉了落竹,所以不會執(zhí)行ViewGroupB的onTouchEvent方法,只能往上回傳 return true 去告訴ViewGroupA 事件已經(jīng)被消費掉了瓮钥,你沒機會了 筋量。然后事件繼續(xù)回傳給ViewGroupA,A收到return true 便知道 事件被消費了碉熄,所以它也return true桨武。最終事件回傳到了MainActivity,由于事件被消費了锈津,所以不會執(zhí)行MainActivity的onTouchEvent方法呀酸。接下來又開始執(zhí)行Move事件了,流程又和之前的一樣重新開始處理琼梆。

下面這段Log性誉,便記錄了事件分發(fā)的整個過程。當Down事件被View消費后茎杂,事件會重新開始從ViewGroupA错览、ViewGroupB 這樣下來進行分發(fā),直到UP事件結(jié)束煌往。

結(jié)論:onTouchEvent被View消費后倾哺,ViewGroupA、ViewGroupB的onTouchEvent都不會執(zhí)行

2.3 點擊ViewGroupB區(qū)域但不消耗事件

這里的流程就不細說了刽脖,前面已經(jīng)詳細描述了兩遍了羞海。總的來說就是曲管,ViewGroupB和ViewGroupA都不消費事件却邓,那最終只能交給老大MainActivity去消費事件。

先看Log院水,為什么這里ViewGroupB并沒有攔截View 但是View完全接受不到事件呢腊徙?


我們來看ViewGroup的dispatchTouchEvent源碼

public boolean dispatchTouchEvent(MotionEvent ev) {  
    ...
    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  
    if (action == MotionEvent.ACTION_DOWN) {  
        if (mMotionTarget != null) {  
            mMotionTarget = null;  
        }  
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
            ... 
            if (isTransformedTouchPointInView(x,y,point))  {  
               if (child.dispatchTouchEvent(ev))  {  
                            mMotionTarget = child;  
                            return true;  
            }   
          }  
                   
        }  
    }  

從源碼中可以看到简十,執(zhí)行到if (isTransformedTouchPointInView) 這行代碼時,就是去判斷當前點擊的坐標是否屬于View的區(qū)域內(nèi)撬腾,假如是勺远,就開始執(zhí)行View的dispatchTouchEvent方法。很顯然在這里點擊的ViewGroupB區(qū)域时鸵,并不在View的范圍內(nèi),所以事件也不會分發(fā)到View厅瞎。

2.4 點擊View區(qū)域饰潜,View消耗事件,但設(shè)置了View.onTouchListener

設(shè)置View.onTouchListener中的onTouch()方法 return true和簸。


當事件分發(fā)到View時彭雾,我們先來看View的dispatchToucnEvent源碼:

public boolean dispatchTouchEvent(MotionEvent event) {  
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
            mOnTouchListener.onTouch(this, event)) {  
        return true;  
    }  
    return onTouchEvent(event);  
}

看到?jīng)]验烧,當mOnTouchListener.onTouch(this, event)這個條件為true的時候寨辩,View的dispatchTouchEvent方法將直接return true划址。后續(xù)也不會執(zhí)行View的onTouchEvent方法了嗅蔬。

結(jié)論:View的mOnTouchListener.onTouch方法優(yōu)先于View的onTouchEvent方法被執(zhí)行兴垦。

附上log:


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末如暖,一起剝皮案震驚了整個濱河市率挣,隨后出現(xiàn)的幾起案子清寇,更是在濱河造成了極大的恐慌浩村,老刑警劉巖做葵,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異心墅,居然都是意外死亡酿矢,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門怎燥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瘫筐,“玉大人,你說我怎么就攤上這事铐姚〔吒危” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵谦屑,是天一觀的道長驳糯。 經(jīng)常有香客問我,道長氢橙,這世上最難降的妖魔是什么酝枢? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮悍手,結(jié)果婚禮上帘睦,老公的妹妹穿的比我還像新娘袍患。我一直安慰自己,他們只是感情好竣付,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布诡延。 她就那樣靜靜地躺著,像睡著了一般古胆。 火紅的嫁衣襯著肌膚如雪肆良。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天逸绎,我揣著相機與錄音惹恃,去河邊找鬼。 笑死棺牧,一個胖子當著我的面吹牛巫糙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播颊乘,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼参淹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了乏悄?” 一聲冷哼從身側(cè)響起浙值,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎檩小,沒想到半個月后亥鸠,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡识啦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年负蚊,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颓哮。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡家妆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出冕茅,到底是詐尸還是另有隱情伤极,我是刑警寧澤,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布姨伤,位于F島的核電站哨坪,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏乍楚。R本人自食惡果不足惜当编,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望徒溪。 院中可真熱鬧忿偷,春花似錦金顿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至茶凳,卻和暖如春嫂拴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背贮喧。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工顷牌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人塞淹。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像罪裹,于是被迫代替她去往敵國和親饱普。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

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