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

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

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

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

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

1.1 為什么要有事件機制 ?

當你在一個布局中,有一個LinearLayout,里面又有一個小的LinearLayout,然后在這個小的LinearLayout中又有一個View,這個時候。這個時候,你點擊這個View,為什么LinearLayout不會響應?其實你點擊的也是LinearLayout的區(qū)域啊。帶著這個疑問,就可以猜想到了,事件分發(fā)機制,其實就是為了統(tǒng)一協(xié)調這些view的事件。

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

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

  1. ACTION_DOWN 手指開始觸摸到屏幕的那一刻響應的是DOWN事件
  2. ACTION_MOVE 接著手指在屏幕上移動響應的是MOVE事件
  3. ACTION_UP 手指從屏幕上松開的那一刻響應的是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都不會響應到任何事件了。onInterceptTouchEvent 返回true時,表示ViewGroup會攔截事件。

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

    onTouchEvent 返回true時,表示事件被消費掉了。一旦事件被消費掉了,其他父元素的onTouchEvent方法都不會被調用。如果沒有人消耗事件,則最終當前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ā)過程便到此結束。

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

【注:ViewGroupA、ViewGroupB、View的布局結構參考下面的布局圖】

當事件分發(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 時,表明事件已經被消費了。】

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方法中可以得出一個結論:
事件一旦分發(fā)到了View,則默認一定會執(zhí)行它的onTouchEvent方法,除非符合了if的三個條件

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

結論: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不消耗事件

當一個事件產生后,它的傳遞流程是從:Activity -> Window ->View

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


流程圖解析:

  • 事件分發(fā)

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

事件傳遞到ViewGroupA時,因為它不攔截事件,所以它要先去問它的子控件ViewGroupB是否要消費事件,然后將事件分發(fā)給ViewGroupB。事件到了ViewGroupB時,它不攔截事件,所以它也要先去問它的子控件們要不要消費事件,然后將事件分發(fā)給View。事件到了View時開始執(zhí)行dispatchTouchEvent,因為已經到了最底層了,View接下來便開始執(zhí)行onTouchEvent方法來決定是否消費事件。

  • 事件回傳

由于View沒有消費事件,所以它開始回傳信息,(紫色箭頭的流向便是事件回傳方向),以告訴ViewGroupB我不消費事件了,view 的 onTouchEvent 便return false。然后ViewGroupB才開始有權利決定我是否要開始消費事件(因為它已經問過它的子控件是否要消費事件了,而它的子控件并沒有消費),所以開始執(zhí)行ViewGroupB的onTouchEvent方法,由于ViewGroupB也不消費事件,所以它也 return false 。事件繼續(xù)回傳給ViewGroupA,這個時候它終于開始有權利決定我是否要消費事件了,所以開始執(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,因為已經到了最底層了,View接下來便開始執(zhí)行onTouchEvent方法來決定是否消費事件。

  • 事件回傳

由于View消費了事件,所以它開始回傳,(紫色箭頭的流向便是事件回傳方向),以告訴ViewGroupB我已經消費事件了,view 的 onTouchEvent 便return true。然后ViewGroupB 收到了View return true 就知道事件已經被View消費掉了,所以不會執(zhí)行ViewGroupB的onTouchEvent方法,只能往上回傳 return true 去告訴ViewGroupA 事件已經被消費掉了,你沒機會了 。然后事件繼續(xù)回傳給ViewGroupA,A收到return true 便知道 事件被消費了,所以它也return true。最終事件回傳到了MainActivity,由于事件被消費了,所以不會執(zhí)行MainActivity的onTouchEvent方法。接下來又開始執(zhí)行Move事件了,流程又和之前的一樣重新開始處理。

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

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

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

這里的流程就不細說了,前面已經詳細描述了兩遍了。總的來說就是,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ū)域內,假如是,就開始執(zhí)行View的dispatchTouchEvent方法。很顯然在這里點擊的ViewGroupB區(qū)域,并不在View的范圍內,所以事件也不會分發(fā)到View。

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

設置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);  
}

看到沒,當mOnTouchListener.onTouch(this, event)這個條件為true的時候,View的dispatchTouchEvent方法將直接return true。后續(xù)也不會執(zhí)行View的onTouchEvent方法了。

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

附上log:


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容