【源码分析】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层次实现业务逻辑。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值