Android中View的分发拦截机制是一块重要的内容,网上也有很多大神进行过相关的分析。
在这篇文章里我将以自己的理解尽量全面地分析整个流程,有些分析结果是很多文章没有提及的。
整个分析过程将通过demo与源码进行,做到有理有据。
###demo结构
这个demo的地址在这儿
首先要知道,ViewGroup的相关方法有dispatchTouchEvent
、onInterceptTouchEvent
、onTouchEvent
、requestDisallowInterceptTouchEvent
,
View的相关方法有dispatchTouchEvent
、onTouchEvent
,一般来说,这些方法返回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
可以发现
- 当没有OnClickListener时,触摸事件就会向上传递,否则即使不消耗事件也不会;
- 向上传递时其实也是先到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;
}
可以看到,一般情况下是不会拦截的。
###总结
根据上面的所有分析,可以画出如下的事件分发拦截流程图:
这张图涵盖了大部分的情况,有助于我们理解和记忆。
除了这张图以外,关于事件分发拦截,我还有以下总结:
- ViewGroup比View多了两个拦截相关的方法。
- 有很多方法可以获取触摸事件,可以根据业务场景在合适的时机获取并实现业务逻辑。
- 父布局拦截可以一直拦截,也可以根据业务边界动态改变。
- 子View反拦截也可以分为静态和动态实现,但要注意反拦截方法的调用时机。
- 核心要点就是熟悉整个流程,在合适的时机和View层次实现业务逻辑。