【源碼分析】Android觸摸事件的分發攔截

Android中View的分發攔截機制是一塊重要的內容,網上也有很多大神進行過相關的分析。
在這篇文章裏我將以自己的理解儘量全面地分析整個流程,有些分析結果是很多文章沒有提及的。
整個分析過程將通過demo與源碼進行,做到有理有據。
###demo結構
這個demo的地址在這兒

首先要知道,ViewGroup的相關方法有dispatchTouchEventonInterceptTouchEventonTouchEventrequestDisallowInterceptTouchEvent

View的相關方法有dispatchTouchEventonTouchEvent,一般來說,這些方法返回true表示已經消耗了觸摸事件,不會再向下分發事件,返回false則相反。
demo中需要重寫相關方法並添加日誌輸出,便於我們觀察並理清正確的過程。

自定義ViewGroupA 繼承FrameLayout如下:

/**
 * Author: Sbingo
 * Date:   2017/6/28
 */

public class ViewGroupA extends FrameLayout{

    Logger myLogger = Logger.getLogger("Sbingo ViewGroupA");

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

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

    public ViewGroupA(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        myLogger.log(Level.INFO, "dispatchTouchEvent");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        myLogger.log(Level.INFO, "requestDisallowInterceptTouchEvent");
        super.requestDisallowInterceptTouchEvent(disallowIntercept);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        myLogger.log(Level.INFO, "onInterceptTouchEvent");
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        myLogger.log(Level.INFO, "onTouchEvent");
        return super.onTouchEvent(event);
    }
}

類似地,自定義ViewGroupB 繼承FrameLayout,添加相關打印日誌。
接着自定義MyView繼承TextView如下:

/**
 * Author: Sbingo
 * Date:   2017/6/28
 */

public class MyView extends android.support.v7.widget.AppCompatTextView {

    Logger myLogger = Logger.getLogger("Sbingo MyView");

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

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        myLogger.log(Level.INFO, "dispatchTouchEvent");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        myLogger.log(Level.INFO, "onTouchEvent:" + event.getAction());
        return true;
    }

}

給它們定義如下的佈局層次:

    <com.sbingo.viewsample.ViewGroupA
        android:id="@+id/vga"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/black">

        <com.sbingo.viewsample.ViewGroupB
            android:id="@+id/vgb"
            android:layout_width="300dp"
            android:layout_height="400dp"
            android:layout_gravity="center"
            android:background="@color/colorPrimary"
            tools:layout_editor_absoluteX="0dp"
            tools:layout_editor_absoluteY="0dp">

            <com.sbingo.viewsample.MyView
                android:id="@+id/my_view"
                android:layout_width="150dp"
                android:layout_height="200dp"
                android:layout_gravity="center"
                android:background="@color/colorAccent" />
        </com.sbingo.viewsample.ViewGroupB>
    </com.sbingo.viewsample.ViewGroupA>

佈局很簡單,MyView在ViewGroupB正中間,ViewGroupB在ViewGroupA正中間。
一切準備就緒,開始玩起來!
###事實來說話,log作用大

點擊MyView
06-28 11:33:32.947 24137-24137/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
06-28 11:33:32.948 24137-24137/com.sbingo.viewsample I/Sbingo ViewGroupA: onInterceptTouchEvent
06-28 11:33:32.948 24137-24137/com.sbingo.viewsample I/Sbingo ViewGroupB: dispatchTouchEvent
06-28 11:33:32.948 24137-24137/com.sbingo.viewsample I/Sbingo ViewGroupB: onInterceptTouchEvent
06-28 11:33:32.948 24137-24137/com.sbingo.viewsample I/Sbingo MyView: dispatchTouchEvent
06-28 11:33:32.948 24137-24137/com.sbingo.viewsample I/Sbingo MyView: onTouchEvent
06-28 11:33:32.949 24137-24137/com.sbingo.viewsample I/Sbingo ViewGroupB: onTouchEvent
06-28 11:33:32.949 24137-24137/com.sbingo.viewsample I/Sbingo ViewGroupA: onTouchEvent

觸摸事件到達MyView後沒有被消耗,又向上傳遞

點擊ViewGroupB
06-28 11:33:48.721 24137-24137/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
06-28 11:33:48.721 24137-24137/com.sbingo.viewsample I/Sbingo ViewGroupA: onInterceptTouchEvent
06-28 11:33:48.721 24137-24137/com.sbingo.viewsample I/Sbingo ViewGroupB: dispatchTouchEvent
06-28 11:33:48.721 24137-24137/com.sbingo.viewsample I/Sbingo ViewGroupB: onInterceptTouchEvent
06-28 11:33:48.721 24137-24137/com.sbingo.viewsample I/Sbingo ViewGroupB: onTouchEvent
06-28 11:33:48.721 24137-24137/com.sbingo.viewsample I/Sbingo ViewGroupA: onTouchEvent

如果點擊的是ViewGroupB,ViewGroupB就會攔截事件,但因爲也沒有消耗事件,事件又向上傳遞

點擊ViewGroupA
06-28 11:35:04.516 24137-24137/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
06-28 11:35:04.517 24137-24137/com.sbingo.viewsample I/Sbingo ViewGroupA: onInterceptTouchEvent
06-28 11:35:04.517 24137-24137/com.sbingo.viewsample I/Sbingo ViewGroupA: onTouchEvent

如果點擊的是ViewGroupA,ViewGroupA就直接攔截了觸摸事件。
這3種輸出是最基本的分發流程,相信大部分開發是沒有疑問的。

點擊MyView,MyView消耗事件
06-28 11:49:05.194 13115-13115/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
06-28 11:49:05.194 13115-13115/com.sbingo.viewsample I/Sbingo ViewGroupA: onInterceptTouchEvent
06-28 11:49:05.194 13115-13115/com.sbingo.viewsample I/Sbingo ViewGroupB: dispatchTouchEvent
06-28 11:49:05.195 13115-13115/com.sbingo.viewsample I/Sbingo ViewGroupB: onInterceptTouchEvent
06-28 11:49:05.195 13115-13115/com.sbingo.viewsample I/Sbingo MyView: dispatchTouchEvent
06-28 11:49:05.195 13115-13115/com.sbingo.viewsample I/Sbingo MyView: onTouchEvent:0
06-28 11:49:05.327 13115-13115/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
06-28 11:49:05.327 13115-13115/com.sbingo.viewsample I/Sbingo ViewGroupA: onInterceptTouchEvent
06-28 11:49:05.328 13115-13115/com.sbingo.viewsample I/Sbingo ViewGroupB: dispatchTouchEvent
06-28 11:49:05.328 13115-13115/com.sbingo.viewsample I/Sbingo ViewGroupB: onInterceptTouchEvent
06-28 11:49:05.328 13115-13115/com.sbingo.viewsample I/Sbingo MyView: dispatchTouchEvent
06-28 11:49:05.328 13115-13115/com.sbingo.viewsample I/Sbingo MyView: onTouchEvent:1

MyView消耗觸摸事件後,事件不再向上傳遞,之後該事件的所有動作都傳到MyView這裏,其中可能包含多次的move(示例中沒有這種情況)。

點擊MyView,ViewGroupB攔截
06-28 11:48:15.129 12066-12066/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
06-28 11:48:15.129 12066-12066/com.sbingo.viewsample I/Sbingo ViewGroupA: onInterceptTouchEvent
06-28 11:48:15.129 12066-12066/com.sbingo.viewsample I/Sbingo ViewGroupB: dispatchTouchEvent
06-28 11:48:15.129 12066-12066/com.sbingo.viewsample I/Sbingo ViewGroupB: onInterceptTouchEvent
06-28 11:48:15.130 12066-12066/com.sbingo.viewsample I/Sbingo ViewGroupB: onTouchEvent
06-28 11:48:15.130 12066-12066/com.sbingo.viewsample I/Sbingo ViewGroupA: onTouchEvent

這和直接點擊ViewGroupB的輸出一樣。

現在爲ViewGroupB和MyView設置OnTouchListener和OnClickListener,並添加打印日誌。

點擊MyView,MyView在onTouchEvent消耗事件
06-28 16:28:24.140 6251-6251/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
06-28 16:28:24.140 6251-6251/com.sbingo.viewsample I/Sbingo ViewGroupA: onInterceptTouchEvent
06-28 16:28:24.140 6251-6251/com.sbingo.viewsample I/Sbingo ViewGroupB: dispatchTouchEvent
06-28 16:28:24.141 6251-6251/com.sbingo.viewsample I/Sbingo ViewGroupB: onInterceptTouchEvent
06-28 16:28:24.141 6251-6251/com.sbingo.viewsample I/Sbingo MyView: dispatchTouchEvent
06-28 16:28:24.141 6251-6251/com.sbingo.viewsample I/Sbingo MyView: onTouch
06-28 16:28:24.141 6251-6251/com.sbingo.viewsample I/Sbingo MyView: onTouchEvent:0
06-28 16:28:24.238 6251-6251/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
06-28 16:28:24.238 6251-6251/com.sbingo.viewsample I/Sbingo ViewGroupA: onInterceptTouchEvent
06-28 16:28:24.238 6251-6251/com.sbingo.viewsample I/Sbingo ViewGroupB: dispatchTouchEvent
06-28 16:28:24.238 6251-6251/com.sbingo.viewsample I/Sbingo ViewGroupB: onInterceptTouchEvent
06-28 16:28:24.238 6251-6251/com.sbingo.viewsample I/Sbingo MyView: dispatchTouchEvent
06-28 16:28:24.238 6251-6251/com.sbingo.viewsample I/Sbingo MyView: onTouch
06-28 16:28:24.239 6251-6251/com.sbingo.viewsample I/Sbingo MyView: onTouchEvent:1

點擊MyView,MyView不消耗事件
06-28 16:39:52.048 17495-17495/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
06-28 16:39:52.049 17495-17495/com.sbingo.viewsample I/Sbingo ViewGroupA: onInterceptTouchEvent
06-28 16:39:52.049 17495-17495/com.sbingo.viewsample I/Sbingo ViewGroupB: dispatchTouchEvent
06-28 16:39:52.049 17495-17495/com.sbingo.viewsample I/Sbingo ViewGroupB: onInterceptTouchEvent
06-28 16:39:52.049 17495-17495/com.sbingo.viewsample I/Sbingo MyView: dispatchTouchEvent
06-28 16:39:52.049 17495-17495/com.sbingo.viewsample I/Sbingo MyView: onTouch
06-28 16:39:52.049 17495-17495/com.sbingo.viewsample I/Sbingo MyView: onTouchEvent:0
06-28 16:39:52.136 17495-17495/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
06-28 16:39:52.137 17495-17495/com.sbingo.viewsample I/Sbingo ViewGroupA: onInterceptTouchEvent
06-28 16:39:52.137 17495-17495/com.sbingo.viewsample I/Sbingo ViewGroupB: dispatchTouchEvent
06-28 16:39:52.137 17495-17495/com.sbingo.viewsample I/Sbingo ViewGroupB: onInterceptTouchEvent
06-28 16:39:52.137 17495-17495/com.sbingo.viewsample I/Sbingo MyView: dispatchTouchEvent
06-28 16:39:52.137 17495-17495/com.sbingo.viewsample I/Sbingo MyView: onTouch
06-28 16:39:52.137 17495-17495/com.sbingo.viewsample I/Sbingo MyView: onTouchEvent:1
06-28 16:39:52.162 17495-17495/com.sbingo.viewsample I/Sbingo MyView: onClick

可以發現
1.onTouch方法先於onTouchEvent方法執行
2.當MyView不消耗觸摸事件時,onClick方法纔得到執行,否則不會進入onClick方法。
3.不管MyView是否消耗事件,事件都沒有向上傳遞

點擊MyView,MyView的onTouch方法消耗事件
07-07 11:08:42.158 21627-21627/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
07-07 11:08:42.158 21627-21627/com.sbingo.viewsample I/Sbingo ViewGroupA: onInterceptTouchEvent
07-07 11:08:42.158 21627-21627/com.sbingo.viewsample I/Sbingo ViewGroupB: dispatchTouchEvent
07-07 11:08:42.158 21627-21627/com.sbingo.viewsample I/Sbingo ViewGroupB: onInterceptTouchEvent
07-07 11:08:42.159 21627-21627/com.sbingo.viewsample I/Sbingo MyView: dispatchTouchEvent
07-07 11:08:42.159 21627-21627/com.sbingo.viewsample I/Sbingo MyView: onTouch
07-07 11:08:42.255 21627-21627/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
07-07 11:08:42.255 21627-21627/com.sbingo.viewsample I/Sbingo ViewGroupA: onInterceptTouchEvent
07-07 11:08:42.255 21627-21627/com.sbingo.viewsample I/Sbingo ViewGroupB: dispatchTouchEvent
07-07 11:08:42.255 21627-21627/com.sbingo.viewsample I/Sbingo ViewGroupB: onInterceptTouchEvent
07-07 11:08:42.256 21627-21627/com.sbingo.viewsample I/Sbingo MyView: dispatchTouchEvent
07-07 11:08:42.256 21627-21627/com.sbingo.viewsample I/Sbingo MyView: onTouch

剛纔已經發現onTouch方法先於onTouchEvent方法執行,現在onTouch方法消耗事件後,onTouchEvent方法便不會執行,之後該事件的所有動作都傳到MyView的onTouch方法。

點擊MyView,ViewGroupB攔截
06-28 16:44:51.593 20418-20418/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
06-28 16:44:51.594 20418-20418/com.sbingo.viewsample I/Sbingo ViewGroupA: onInterceptTouchEvent
06-28 16:44:51.594 20418-20418/com.sbingo.viewsample I/Sbingo ViewGroupB: dispatchTouchEvent
06-28 16:44:51.594 20418-20418/com.sbingo.viewsample I/Sbingo ViewGroupB: onInterceptTouchEvent
06-28 16:44:51.594 20418-20418/com.sbingo.viewsample I/Sbingo ViewGroupB: onTouch
06-28 16:44:51.594 20418-20418/com.sbingo.viewsample I/Sbingo ViewGroupB: onTouchEvent
06-28 16:44:51.691 20418-20418/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
06-28 16:44:51.691 20418-20418/com.sbingo.viewsample I/Sbingo ViewGroupA: onInterceptTouchEvent
06-28 16:44:51.692 20418-20418/com.sbingo.viewsample I/Sbingo ViewGroupB: dispatchTouchEvent
06-28 16:44:51.692 20418-20418/com.sbingo.viewsample I/Sbingo ViewGroupB: onTouch
06-28 16:44:51.692 20418-20418/com.sbingo.viewsample I/Sbingo ViewGroupB: onTouchEvent
06-28 16:44:51.698 20418-20418/com.sbingo.viewsample I/Sbingo ViewGroupB: onClick

當ViewGroupB攔截事件後,之後對於該事件的所有動作ViewGroupB的dispatchTouchEvent方法將返回true,
ViewGroupB的onInterceptTouchEvent方法將不再執行。

去除MyView的OnClickListener,點擊MyView,MyView不消耗事件
07-07 11:29:06.078 7138-7138/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
07-07 11:29:06.078 7138-7138/com.sbingo.viewsample I/Sbingo ViewGroupA: onInterceptTouchEvent
07-07 11:29:06.078 7138-7138/com.sbingo.viewsample I/Sbingo ViewGroupB: dispatchTouchEvent
07-07 11:29:06.078 7138-7138/com.sbingo.viewsample I/Sbingo ViewGroupB: onInterceptTouchEvent
07-07 11:29:06.079 7138-7138/com.sbingo.viewsample I/Sbingo MyView: dispatchTouchEvent
07-07 11:29:06.079 7138-7138/com.sbingo.viewsample I/Sbingo MyView: onTouch
07-07 11:29:06.079 7138-7138/com.sbingo.viewsample I/Sbingo MyView: onTouchEvent:0
07-07 11:29:06.079 7138-7138/com.sbingo.viewsample I/Sbingo ViewGroupB: onTouch
07-07 11:29:06.079 7138-7138/com.sbingo.viewsample I/Sbingo ViewGroupB: onTouchEvent
07-07 11:29:06.125 7138-7138/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
07-07 11:29:06.126 7138-7138/com.sbingo.viewsample I/Sbingo ViewGroupA: onInterceptTouchEvent
07-07 11:29:06.126 7138-7138/com.sbingo.viewsample I/Sbingo ViewGroupB: dispatchTouchEvent
07-07 11:29:06.126 7138-7138/com.sbingo.viewsample I/Sbingo ViewGroupB: onTouch
07-07 11:29:06.126 7138-7138/com.sbingo.viewsample I/Sbingo ViewGroupB: onTouchEvent
07-07 11:29:06.144 7138-7138/com.sbingo.viewsample I/Sbingo ViewGroupB: onClick

接着去除ViewGroupB的OnClickListener,點擊MyView,MyView不消耗事件
07-07 11:42:36.186 17561-17561/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
07-07 11:42:36.186 17561-17561/com.sbingo.viewsample I/Sbingo ViewGroupA: onInterceptTouchEvent
07-07 11:42:36.187 17561-17561/com.sbingo.viewsample I/Sbingo ViewGroupB: dispatchTouchEvent
07-07 11:42:36.187 17561-17561/com.sbingo.viewsample I/Sbingo ViewGroupB: onInterceptTouchEvent
07-07 11:42:36.187 17561-17561/com.sbingo.viewsample I/Sbingo MyView: dispatchTouchEvent
07-07 11:42:36.187 17561-17561/com.sbingo.viewsample I/Sbingo MyView: onTouch
07-07 11:42:36.187 17561-17561/com.sbingo.viewsample I/Sbingo MyView: onTouchEvent:0
07-07 11:42:36.187 17561-17561/com.sbingo.viewsample I/Sbingo ViewGroupB: onTouch
07-07 11:42:36.187 17561-17561/com.sbingo.viewsample I/Sbingo ViewGroupB: onTouchEvent
07-07 11:42:36.187 17561-17561/com.sbingo.viewsample I/Sbingo ViewGroupA: onTouchEvent

可以發現

  1. 當沒有OnClickListener時,觸摸事件就會向上傳遞,否則即使不消耗事件也不會;
  2. 向上傳遞時其實也是先到onTouch再到onTouchEvent方法。

到這裏,我們已經基本瞭解了分發流程,對於如何攔截也都知道了。

那麼如果子View不想被攔截,如何實現反攔截呢?
ViewGroup有一個requestDisallowInterceptTouchEvent`方法,是專門用來反攔截的,傳入true表示子View希望反攔截,false表示由父佈局決定是否攔截。
我們來試一試這個方法:

app啓動時調用ViewGroupB的requestDisallowInterceptTouchEvent(true)方法,點擊MyView,ViewGroupB攔截
07-07 13:22:20.082 12098-12098/com.sbingo.viewsample I/Sbingo ViewGroupB: requestDisallowInterceptTouchEvent
07-07 13:22:20.082 12098-12098/com.sbingo.viewsample I/Sbingo ViewGroupA: requestDisallowInterceptTouchEvent
07-07 13:22:41.056 12098-12098/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
07-07 13:22:41.056 12098-12098/com.sbingo.viewsample I/Sbingo ViewGroupA: onInterceptTouchEvent
07-07 13:22:41.057 12098-12098/com.sbingo.viewsample I/Sbingo ViewGroupB: dispatchTouchEvent
07-07 13:22:41.057 12098-12098/com.sbingo.viewsample I/Sbingo ViewGroupB: onInterceptTouchEvent
07-07 13:22:41.057 12098-12098/com.sbingo.viewsample I/Sbingo ViewGroupB: onTouch
07-07 13:22:41.057 12098-12098/com.sbingo.viewsample I/Sbingo ViewGroupB: onTouchEvent
07-07 13:22:41.057 12098-12098/com.sbingo.viewsample I/Sbingo ViewGroupA: onTouchEvent

當調用ViewGroupB的requestDisallowInterceptTouchEvent(true)方法時,
會自動向上調用ViewGroupA的requestDisallowInterceptTouchEvent方法,
之後在ViewGroupA的onInterceptTouchEvent方法中返回了true進行攔截,攔截成功了。
可見此時requestDisallowInterceptTouchEvent(true)並沒有起作用。
這次因爲
1.每次點擊時,都會重置。所以我們在啓動時的設置沒有作用。
2.ACTION_DOWN不能被攔截,否則後續事件都不會向下傳遞。

ViewGroupB只在ACTION_DOWN時不攔截,其餘情況攔截,點擊MyView
07-07 13:36:44.715 25397-25397/com.sbingo.viewsample I/Sbingo ViewGroupB: requestDisallowInterceptTouchEvent
07-07 13:36:44.715 25397-25397/com.sbingo.viewsample I/Sbingo ViewGroupA: requestDisallowInterceptTouchEvent
07-07 13:36:49.350 25397-25397/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
07-07 13:36:49.350 25397-25397/com.sbingo.viewsample I/Sbingo ViewGroupA: onInterceptTouchEvent
07-07 13:36:49.350 25397-25397/com.sbingo.viewsample I/Sbingo ViewGroupB: dispatchTouchEvent
07-07 13:36:49.351 25397-25397/com.sbingo.viewsample I/Sbingo ViewGroupB: onInterceptTouchEvent
07-07 13:36:49.352 25397-25397/com.sbingo.viewsample I/Sbingo MyView: dispatchTouchEvent
07-07 13:36:49.352 25397-25397/com.sbingo.viewsample I/Sbingo MyView: onTouch
07-07 13:36:49.352 25397-25397/com.sbingo.viewsample I/Sbingo MyView: onTouchEvent:0
07-07 13:36:49.435 25397-25397/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
07-07 13:36:49.435 25397-25397/com.sbingo.viewsample I/Sbingo ViewGroupA: onInterceptTouchEvent
07-07 13:36:49.435 25397-25397/com.sbingo.viewsample I/Sbingo ViewGroupB: dispatchTouchEvent
07-07 13:36:49.436 25397-25397/com.sbingo.viewsample I/Sbingo ViewGroupB: onInterceptTouchEvent
07-07 13:36:49.436 25397-25397/com.sbingo.viewsample I/Sbingo MyView: dispatchTouchEvent
07-07 13:36:49.436 25397-25397/com.sbingo.viewsample I/Sbingo MyView: onTouch
07-07 13:36:49.436 25397-25397/com.sbingo.viewsample I/Sbingo MyView: onTouchEvent:3

MyView只收到了ACTION_DOWN和ACTION_CANCEL兩個動作。

接下來演示如何正確地實現反攔截:

ViewGroupB只在ACTION_DOWN時不攔截,其餘情況攔截,
重寫MyView的dispatchTouchEvent方法如下:
 @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        myLogger.log(Level.INFO, "dispatchTouchEvent");
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            default:
        }
        return super.dispatchTouchEvent(ev);
    }
點擊MyView,MyView消耗事件
07-07 14:13:50.178 26911-26911/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
07-07 14:13:50.179 26911-26911/com.sbingo.viewsample I/Sbingo ViewGroupA: onInterceptTouchEvent
07-07 14:13:50.179 26911-26911/com.sbingo.viewsample I/Sbingo ViewGroupB: dispatchTouchEvent
07-07 14:13:50.179 26911-26911/com.sbingo.viewsample I/Sbingo ViewGroupB: onInterceptTouchEvent
07-07 14:13:50.179 26911-26911/com.sbingo.viewsample I/Sbingo MyView: dispatchTouchEvent
07-07 14:13:50.179 26911-26911/com.sbingo.viewsample I/Sbingo ViewGroupB: requestDisallowInterceptTouchEvent
07-07 14:13:50.179 26911-26911/com.sbingo.viewsample I/Sbingo ViewGroupA: requestDisallowInterceptTouchEvent
07-07 14:13:50.179 26911-26911/com.sbingo.viewsample I/Sbingo MyView: onTouch
07-07 14:13:50.179 26911-26911/com.sbingo.viewsample I/Sbingo MyView: onTouchEvent:0
07-07 14:13:50.251 26911-26911/com.sbingo.viewsample I/Sbingo ViewGroupA: dispatchTouchEvent
07-07 14:13:50.251 26911-26911/com.sbingo.viewsample I/Sbingo ViewGroupB: dispatchTouchEvent
07-07 14:13:50.251 26911-26911/com.sbingo.viewsample I/Sbingo MyView: dispatchTouchEvent
07-07 14:13:50.251 26911-26911/com.sbingo.viewsample I/Sbingo MyView: onTouch
07-07 14:13:50.251 26911-26911/com.sbingo.viewsample I/Sbingo MyView: onTouchEvent:1

這裏我們更換了調用requestDisallowInterceptTouchEvent方法的時機,
調用getParent().requestDisallowInterceptTouchEvent(true)後,
之後對於該事件的所有動作,ViewGroupA和ViewGroupB的onInterceptTouchEvent方法都沒有執行,
直接執行了子View的dispatchTouchEvent方法, 反攔截成功。
至此,所有可能的流程基本都已分析過。
至於爲什麼這樣就能反攔截成功,請接着往下看。

###源碼分析
對於以上的分析結論,基本是沒有疑問的。
主要困惑應該在於requestDisallowInterceptTouchEvent方法的使用上,我們着重講一下這個方法。先看一下它的源碼:

	@Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

這個方法對mGroupFlags進行位運算,並依次調用父佈局的requestDisallowInterceptTouchEvent方法,這和之前的log輸出相同。

	 @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

    	......

	    	final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

    	......

    }

dispatchTouchEvent方法中,如果觸摸動作是ACTION_DOWN,第15行就會調用resetTouchState方法:

    /**
     * Resets all touch state in preparation for a new cycle.
     */
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

這個重置方法的第7行又對mGroupFlags進行了位運算,再回到dispatchTouchEvent方法,看第22行的判斷,使得ACTION_DOWN情況下第24行的onInterceptTouchEvent總是得到執行。
也就是說,ACTION_DOWN時,會重置觸摸狀態,onInterceptTouchEvent方法必定會執行。
所以之前在app啓動時調用requestDisallowInterceptTouchEvent(true)方法,反攔截無效;在MyView收到ACTION_DOWN時調用requestDisallowInterceptTouchEvent(true)方法就有效,之後的動作就跳過onInterceptTouchEvent方法,直接分發給了MyView的dispatchTouchEvent方法。
第24、27、32行3處的intercepted賦值分別表示以下3種情況:
ACTION_DOWN或允許攔截、
不允許攔截(就是反攔截情況)
已經決定攔截且不是ACTION_DOWN動作。

說了這麼久的攔截,攔截方法是怎麼樣的呢?來看一下:

	public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }

可以看到,一般情況下是不會攔截的。

###總結
根據上面的所有分析,可以畫出如下的事件分發攔截流程圖:
這裏寫圖片描述

這張圖涵蓋了大部分的情況,有助於我們理解和記憶。
除了這張圖以外,關於事件分發攔截,我還有以下總結:

  1. ViewGroup比View多了兩個攔截相關的方法。
  2. 有很多方法可以獲取觸摸事件,可以根據業務場景在合適的時機獲取並實現業務邏輯。
  3. 父佈局攔截可以一直攔截,也可以根據業務邊界動態改變。
  4. 子View反攔截也可以分爲靜態和動態實現,但要注意反攔截方法的調用時機。
  5. 核心要點就是熟悉整個流程,在合適的時機和View層次實現業務邏輯。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章