Android:View的事件分發與消費機制


寫在前面

最近一直在看自定義控件的一些知識,基本弄清楚自定義控件的一般流程。我們知道一般自定義控件都需要重寫控件的觸摸事件。而自定義控件需要繼承 View /ViewGroup或者其他已有的控件 ,這個時候我們就要考慮到View中一個非常重要且難懂的知識——事件分發與消費機制。我自己也在學習的過程中出現過一些由於沒有處理好觸摸事件的分發而導致的一系列滑動觸摸問題,所以花了接近三天的時間徹底弄明白View中的這樣一個機制。

在學習過程中,我參考了很多資料,除了黑馬的教程以外,最重要的兩個資料是《Android開發藝術探索》和一個開發大牛的博客。現在我用語言來把自己所理解的寫出來,內容也大部分參考這兩個資料。有些內容就是書中或博客中的原文,只是我用自己的理解來解釋說明。本文中所用的項目也是引用這個博客的項目,不是圖方便,而是沒有什么例子比這個項目更好且更能說明這個問題了。

借鑒的博客地址為:Android 編程下 Touch 事件的分發和消費機制

一、Touch的三個重要方法

在Android中,與觸摸事件也就是 Touch 相關的有三個重要方法,這三個方法共同完成觸摸事件的分發。

  • public boolean dispatchTouchEvent(MotionEvent ev) :事件分發
  • public boolean onInterceptTouchEvent(MotionEvent ev):事件攔截
  • public boolean onTouchEvent(MotionEvent ev):事件響應

下面就依次來分析這三個方法。

1、事件分發

public boolean dispatchTouchEvent(MotionEvent ev)

顧名思義,事件的分發就是當一個觸摸事件發生的時候,會按照Activity -> Window -> View的順序依次往下傳遞。也就是說系統會把這個事件傳遞給一個具體的View,從而來執行或者說響應這個事件。我們來看博客上是如何說明的:

Touch 事件發生時 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法會以隧道方式(從根元素依次往下傳遞直到最內層子元素或在中間某一元素中由於某一條件停止傳遞)將事件傳遞給最外層 View 的 dispatchTouchEvent(MotionEvent ev) 方法,並由該 View 的 dispatchTouchEvent(MotionEvent ev) 方法對事件進行分發。

這里要注意幾個地方:

第一觸摸事件傳遞的開始一定是Activity;

第二傳遞方式是通過隧道方式傳遞;

第三一直傳遞到一個最外層的View,也就是頂級View,由該View的這個方法來進行分發。

那么我們不禁有個疑問,那Activity能不能直接分發呢,換句話說,傳遞過程什么時候終止呢?答案就是通過判斷這個方法的返回值來處理分發的邏輯。我們看到,分發方法的返回值是 boolean ,所以返回值有 true 和 false ,再加上一個繼承超類的方法 super ,所以一共有三種返回值。
依次來看:

  • 如果 return true,事件會分發給當前 View 並由 dispatchTouchEvent 方法進行消費,同時事件會停止向下傳遞;

  • 如果 return false,事件會分發給事件來源Activity或者父級View的 onTouchEvent 進行消費;

  • 如果return super.dispatchTouchEvent(ev),事件會自動的分發給當前 View 的 onInterceptTouchEvent 方法。

博客上的這個結論單獨拿來看可能有點抽象,我當時看的時候也是,先不急着說清楚,稍后看案例就恍然大悟了。現在只要明白一個概念,事件的分發是按照依次往下的順序,並根據返回結果,決定由誰進行消費。

2、事件攔截

public boolean onInterceptTouchEvent(MotionEvent ev)

與事件分發不同的是,該方法是在事件分發的 dispatchTouchEvent 方法內部進行調用。是用來判斷在觸摸事件傳遞過程中,是否攔截某個事件。博客的解釋是這樣的:

在外層 View 的 dispatchTouchEvent(MotionEvent ev) 方法返回系統默認的 super.dispatchTouchEvent(ev) 情況下,事件會自動的分發給當前 View 的 onInterceptTouchEvent 方法。

同樣的是,該方法仍然通過返回值來判斷是否攔截當前事件。

  • 如果return true,則表示將事件進行攔截,並將攔截到的事件交由當前 View 的 onTouchEvent 進行處理;

  • 如果return false,則表示將事件放行,當前 View 上的事件會被傳遞到子 View 上,再由子 View 的 dispatchTouchEvent 來開始這個事件的分發;

  • 如果return super.onInterceptTouchEvent(ev),那就比較特殊,還要分情況討論:

1、繼承View

(1)如果點擊的是屏幕中的Child。事件將不會被攔截,會被傳遞到Child中的dispatchTouchEvent方法中;
(2)如果點擊的值Father則事件將會被攔截,Father中的onTouchEvent()方法將被執行。

2、繼承ViewGroup

ViewGroup里面super.onInterceptTouchEvent就相當於直接返回false,也就是不攔截。

由於onInterceptTouchEvent方法比較復雜,具體情況具體分析。但要注意,如果當前的View已經攔截了某一個事件,那么在觸摸事件的一整個事件序列中,也就是down -> move -> ... -> up一整個事件中,此方法不會被在調用。

3、事件響應

public boolean onTouchEvent(MotionEvent ev)

這個方法就是用來處理具體點擊事件的,它是在dispatchTouchEvent方法中調用,博客是這樣說的:

在 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev) 並且 onInterceptTouchEvent 返回 true 或返回 super.onInterceptTouchEvent(ev) 的情況下 onTouchEvent 會被調用。

它的返回值表示是否消費當前事件,也就是是否響應當前事件,具體邏輯如下:

  • 如果return true 則會接收並消費該事件。

  • 如果return false,那么這個事件會向上傳遞,並由上層 View 的 onTouchEvent 來接收,如果傳遞到上面的 onTouchEvent 還是返回 false,這個事件無效,且接收不到下一次事件。

  • 如果return super.onTouchEvent(ev) 默認處理事件的邏輯和返回 false 時相同。

值得注意的是,返回結果表示是否消費當前事件,如果不消費的話,那么當前View就無法再次接受到事件。

那么看到這里的話,我相信大家還是沒有一個清晰的認識,下面我們就從三者的聯系與區別上再次說明。

二、三種方法的區別與聯系

1、區別

三者的區別在博客開頭已經說明的非常清楚了,我也仿照文章中表格的形式,總結了一個表格。


Touch事件

也就是說,這三個觸摸事件相關的方法,Activity、View、ViewGroup及其子類都能夠響應。但是Activity對事件攔截不響應。

值得注意的是,如果當前View能夠添加子View或者整個View中有多個子View,那么方法都能響應。但是如果當前View本身已經是一個最小View,那就只能夠響應onTouchEvent。

原因簡單想一下就知道了,事件分發與事件攔截都是由上級往下級傳遞事件,如果一個View已經是最后一級了,它就無法進行事件分發或事件攔截的必要了。就相當於做汽車,中間站可能會停站進行讓乘客上車下車,但是到了終點站你只有下車,這是一樣的道理。

2、聯系

上面已經說明了三者的區別,那么三者的關系是怎么樣的呢?在《Android開發藝術探索》一書中,有這樣一串偽代碼,就跟書中說明的一樣,“已經將三者的關系表現得淋漓盡致”,我們來看這串偽代碼:

/**
* @Title: dispatchTouchEvent
* @Description: 三者關系的偽代碼
* @return: boolean
*
/
public boolean dispatchTouchEvent(MotionEvent ev){
    //默認返回值
    boolean consume = false;

    //如果事件發生了攔截
    if(onInterceptTouchEvent(ev)){
        //消費事件
        consume = onTouchEvent(ev);
    }else{
        //否則分發給子View
        consume = child.dispatchTouchEvent(ev);
    }

    //返回值
    return consume;
}

用語言來說就是,一旦發生觸摸事件,根 View/ViewGroup 會調用 dispatchTouchEvent 方法,如果這個方法返回 false ,觸摸事件不生效。但是如果它的 onInterceptTouchEvent 方法返回了true,代表事件被攔截,那么事件就會交給當前View的 onTouchEvent 方法對事件進行消費。但是如果沒有攔截呢,那么會繼續將事件分發給當前View的子View,子View繼續調用 dispatchTouchEvent 方法,如此循環,直到事件被消費。

但是需要注意的是,我們需要考慮另外一種情況,那就是最終的View的 onTouchEvent 方法仍然返回了false,那么此時,它的父View的 onTouchEvent 方法將會被調用,如果父View仍然沒有消費該事件,那么就繼續往上級傳遞,直到傳到最后的Activity調用 onTouchEvent 方法。

這就跟我們之前的返回值一一對應了起來,現在回頭看看,應該能對事件分發有了一個比較清晰的概念。好吧,如果還是沒有概念,我們只能上圖了。圖也是黑馬教程中的圖,我優化了一下,看的更加清晰。

 





事件消費

至此,我們理論上的東西基本已經講完了,現在我們就通過一個例子來說明具體的事件分發情況。

三、案例分析

1、案例說明

文章開頭已經說過了,沒有更好的例子能說明這個問題了。我將類的結構稍微更改了一下,內容不變。

先自定義兩個View繼承 LinearLayout ,其中為 TouchEventFather 為父View ,TouchEventChilds 為子View。

/**
 * @ClassName: TouchEventFather
 * @Description:父View
 * @author: iamxiarui@foxmail.com
 * @date: 2016年5月9日 下午9:54:14
 */
public class TouchEventFather extends LinearLayout {

     public TouchEventFather(Context context) {
         super(context);
     }

     public TouchEventFather(Context context, AttributeSet attrs) {
         super(context, attrs);
     }

     public boolean dispatchTouchEvent(MotionEvent ev) {
         Log.e("sunzn", "TouchEventFather | dispatchTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
         return super.dispatchTouchEvent(ev);
         // return false;
     }

     public boolean onInterceptTouchEvent(MotionEvent ev) {
         Log.i("sunzn", "TouchEventFather | onInterceptTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
         return super.onInterceptTouchEvent(ev);
         // return false;
     }

     public boolean onTouchEvent(MotionEvent ev) {
         Log.d("sunzn", "TouchEventFather | onTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
         return super.onTouchEvent(ev);
     }

}

/**
 * @ClassName: TouchEventFather
 * @Description:子View
 * @author: iamxiarui@foxmail.com
 * @date: 2016年5月9日 下午9:54:14
 */
public class TouchEventChilds extends LinearLayout {

     public TouchEventChilds(Context context) {
         super(context);
     }

     public TouchEventChilds(Context context, AttributeSet attrs) {
         super(context, attrs);
     }

     public boolean dispatchTouchEvent(MotionEvent ev) {
         Log.e("sunzn", "TouchEventChilds | dispatchTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
         return super.dispatchTouchEvent(ev);
         // return false;
     }

     public boolean onInterceptTouchEvent(MotionEvent ev) {
         Log.i("sunzn", "TouchEventChilds | onInterceptTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
         return super.onInterceptTouchEvent(ev);
         // return false;
     }

     public boolean onTouchEvent(MotionEvent ev) {
         Log.d("sunzn", "TouchEventChilds | onTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
         return super.onTouchEvent(ev);
     }

}

定義好這兩個自定義布局后,我們可以在布局文件中設置布局,注意一定要寫自定義布局的完整包名。

<xml version="1.0" encoding="utf-8"?>
<cn.sunzn.tevent.view.TouchEventFather xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="fill_parent"
     android:layout_height="fill_parent"
     android:background="#468AD7"
     android:gravity="center"
     android:orientation="vertical">

     <cn.sunzn.tevent.view.TouchEventChilds
         android:id="@+id/childs"
         android:layout_width="200dp"
         android:layout_height="200dp"
         android:layout_gravity="center"
         android:background="#E1110D" />

</cn.sunzn.tevent.view.TouchEventFather>

接下來就是主Activity:

/** 
 * @ClassName: TouchEventActivity
 * @Description:事件分發機制詳解
 * @author: iamxiarui@foxmail.com
 * @date: 2016年5月9日 下午9:53:27
 */
public class TouchEventActivity extends Activity {

     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.main);
     }

     public boolean dispatchTouchEvent(MotionEvent ev) {
         Log.w("sunzn", "TouchEventActivity | dispatchTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
         return super.dispatchTouchEvent(ev);
     }

     public boolean onTouchEvent(MotionEvent event) {
         Log.w("sunzn", "TouchEventActivity | onTouchEvent --> " + TouchEventUtil.getTouchAction(event.getAction()));
         return super.onTouchEvent(event);
     }

}

最后是一個工具類,只是將各個點擊狀態封裝到一個方法中:

/** 
 * @ClassName: TouchEventUtil
 * @Description:點擊事件工具類
 * @author: iamxiarui@foxmail.com
 * @date: 2016年5月9日 下午9:53:51
 */
public class TouchEventUtil {

     public static String getTouchAction(int actionId) {
         String actionName = "Unknow:id=" + actionId;
         switch (actionId) {
             case MotionEvent.ACTION_DOWN:
                 actionName = "ACTION_DOWN";
                 break;
             case MotionEvent.ACTION_MOVE:
                 actionName = "ACTION_MOVE";
                 break;
             case MotionEvent.ACTION_UP:
                 actionName = "ACTION_UP";
                 break;
             case MotionEvent.ACTION_CANCEL:
                 actionName = "ACTION_CANCEL";
                 break;
             case MotionEvent.ACTION_OUTSIDE:
                 actionName = "ACTION_OUTSIDE";
                 break;
        }
        return actionName;
    }

}

好了,代碼介紹完了,部署到手機上的時候,應該是這個樣子。


事件分發案例

我們現在就通過不同的返回值,來具體看事件分發的過程,注意我們將代碼部署到手機上的時候,默認做的動作是點擊中間紅色部分一下,這樣更能直觀的觀察日志情況。另外由於原博主總結的非常好,這里我就直接截圖過來,然后具體說明一下。

2、情況一


case1

過程及結果分析:

  • 事件首先由 TouchEventActivity 的 dispatchTouchEvent 方法分發給 TouchEventFather 控件的dispatchTouchEvent;

  • 而該TouchEventFather 控件的 dispatchTouchEvent 返回 false,表示對獲取到的事件停止向下傳遞,同時也不對該事件進行消費;

  • 由於 TouchEventFather 獲取的事件直接來自 TouchEventActivity ,則會將事件返回給 TouchEventActivity 的 onTouchEvent 進行消費;

  • 最后直接由 TouchEventActivity 來響應手指移動和抬起事件。

3、情況二


case2

過程及結果分析:

  • 事件首先由 TouchEventActivity 的 dispatchTouchEvent 方法分發給 TouchEventFather 控件的 dispatchTouchEvent;

  • 而該TouchEventFather 控件的 dispatchTouchEvent 返回 true,表示分發事件到 TouchEventFather 控件並由該控件的 dispatchTouchEvent 進行消費;

  • 又因為TouchEventActivity 不斷的分發事件到 TouchEventFather 控件的 dispatchTouchEvent,而 TouchEventFather 控件的 dispatchTouchEvent 也不斷的將獲取到的事件進行消費。

4、情況三


case3

過程及結果分析:

  • 事件首先由 TouchEventActivity 的 dispatchTouchEvent 方法分發給 TouchEventFather 控件的 dispatchTouchEvent;

  • 而該TouchEventFather 控件的 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev),表示對事件進行分發並向下傳遞給 TouchEventFather 控件的 onInterceptTouchEvent 方法;

  • 而該方法返回 true 表示對所獲取到的事件進行攔截並將事件傳遞給 TouchEventFather 控件的 onTouchEvent 進行處理,TouchEventFather 控件的 onTouchEvent 返回 super.onTouchEvent(ev) 表示對事件沒有做任何處理直接將事件返回給上級控件;

  • 由於 TouchEventFather 獲取的事件直接來自 TouchEventActivity,所以 TouchEventFather 控件的 onTouchEvent 會將事件以冒泡方式直接返回給 TouchEventActivity 的 onTouchEvent 進行消費;

  • 后續的事件則會跳過 TouchEventFather 直接由 TouchEventActivity 的 onTouchEvent 消費來自 TouchEventActivity 自身分發的事件。

5、情況四


case4

過程及結果分析:

  • 事件首先由 TouchEventActivity 的 dispatchTouchEvent 方法分發給 TouchEventFather 控件的 dispatchTouchEvent;

  • 而該控件的 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev),表示對事件進行分發並向下傳遞給 TouchEventFather 控件的 onInterceptTouchEvent 方法;

  • 該方法返回 false 表示事件會被放行並傳遞到子控件 TouchEventChilds 的 dispatchTouchEvent 方法;

  • 同樣 TouchEventChilds 的 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev),表示對事件進行分發並向下傳遞給 TouchEventChilds 控件的 onInterceptTouchEvent 方法;

  • 而TouchEventChilds 的 onInterceptTouchEvent 方法返回 super.onInterceptTouchEvent(ev) ,默認會將事件傳遞給 TouchEventChilds 的 onTouchEvent 進行處理;

  • 而TouchEventChilds 的 onTouchEvent 返回 super.onTouchEvent(ev) 表示對事件沒有做任何處理直接將事件返回給上級控件;

  • 由於 TouchEventChilds 獲取的事件直接來自 TouchEventFather,所以 TouchEventChilds 控件的 onTouchEvent 會將事件以冒泡方式直接返回給 TouchEventFather 的 onTouchEvent 進行消費;

  • 而 TouchEventFather 的 onTouchEvent 也返回了 super.onTouchEvent(ev),同樣 TouchEventFather 的 onTouchEvent 也會將事件返回給上級控件;

  • 而 TouchEventFather 獲取的事件直接來自 TouchEventActivity,所以 TouchEventFather 控件的 onTouchEvent 會將事件以冒泡方式直接返回給 TouchEventActivity 的 onTouchEvent 進行消費;

  • 后續的事件則會跳過 TouchEventFather 和 TouchEventChilds 直接由 TouchEventActivity 的 onTouchEvent 消費來自 TouchEventActivity 自身分發的事件。

6、情況五


case5

過程及結果分析:

  • 事件首先由 TouchEventActivity 的 dispatchTouchEvent 方法分發給 TouchEventFather 控件的 dispatchTouchEvent;

  • 該控件的 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev),事件會分發到 TouchEventFather 的 onInterceptTouchEvent,此方法返回 false 表示放行當先事件;

  • 事件會被傳遞到子控件 TouchEventChilds 的 dispatchTouchEvent 方法,dispatchTouchEvent 返回 true 表示事件被分發到 TouchEventChilds ,並由 dispatchTouchEvent 方法消費;

  • 后續的事件也會不斷的重復上面的邏輯最終被 TouchEventChilds 的 dispatchTouchEvent 消費。

四、總結與歸納

好了,看完理論與代碼結果,我想應該已經夠直觀的說明事件分發機制了。雖然在實際開發過程還是會遇到各種各樣的問題,但是有了理論基礎,處理起來應該不會太難。接下來我就總結一下一些比較重要的注意事項和結論。有些是書上的重要結論。

如果在你不知道返回什么的情況下,記住如果是完全自定義View就返回true,如果是繼承已有的控件或者View那就返回super;

正常情況下,一個事件序列只能被一個View攔截,這是肯定的,因為某個事件被攔截后,只能通過這個攔截View來處理。當然前提是正常情況下。

某個View一旦決定攔截,那么這一個事件序列都只能由它來處理,且不會調用onInterceptTouchEvent()。

在攔截事件中,View與ViewGroup應該區別討論。ViewGroup默認不攔截任何事件,源碼中的onInterceptTouchEvent()默認返回false。

View沒有onInterceptTouchEvent方法,只要有觸摸事件會直接調用onTouchEvent方法。

如果一個View設置了OnTouchListener方法,那么會優先調用onTouch方法,這個時候還要看onTouch方法的返回值,如果為false那么繼續調用onTouchEvent方法,如果為true則不調用。

如果onTouchEvent方法里面設置了OnClickListener之類的方法,它會在onTouchEvent方法調用之后調用其中的onClick方法。

一個事件一旦交給了一個View進行處理,那么它必須消費掉事件,否則剩下的事件序列將不再由其處理。




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM