dispatch、onIntercept、onTouch三者对MotionEvent的分发、拦截机制分析

近期正在测试一个通过左右触划MainView,平滑切换两侧菜单的项目,如果仅仅是触划切换也许我就不必顾虑什么dispatch、onIntercept了,估计一个onTouch就差不多搞定了,但是恼人的是MainView自身嵌套了比较复杂的可以上拉、下拉刷新的自定义PullToFreshView,而PullToFresh内部又嵌套了一个可以上下Scroll的ScrollView和GridView,MainView策划切换到菜单的手势必须与ScrollView中上下滚动Item的手势分开才行,这样必须搞一个MotionEvent拦截覆写在MainView(PullToFreshView)中,onTouch研究的并不深入,但用起来还是比较上手的,而Intercept看着眼熟,没深究过其拦截机制,恰恰再此项目中手势的拦截又写的很复杂,看来有必要把这三个方法理一理了。也许很多新手还想想不到onIntercept能有何作为,不过看看模拟的示意图,可以试着考虑:在右侧List打开状态,该如何分离一系列MotionEvent是拉回主界面的手势呢,还是上下翻动List的手势呢?

侧滑菜单示意图

查阅关于oninterceptTouchEvent的使用方法,有很多资料,其用法就是判断当前ViewGroup是否有必要将MotionEvent继续往内(顶)层子view(Group)进行传递,而查询dispatchTouchEvent的使用方法则收获很少,经过大量翻阅,才发现自己做了很多无用功,不过好在对其略知一二了,便把这个方法的调用机制分享给那些同样在做无用功的同学们吧。

盲点一:它们的返回值决定了什么样的行为?

方法一:onTouchEvent(父(Group)View默认返回false;最外层可被点击的子View默认返回true

onTouchEvent是最常见、最容易上手的方法,它是三兄弟中最基层的MotionEvent消费方法,处于流水线的最下游,负责着大部分MotionEvent的分析处理任务,是响应触摸指令的最直接单位。其意义在于消费一个MotionEvent对象,一旦吸收、转化了该对象,该对象就从对象流中被抹杀掉了,不会再往子View(Group)传递。

方法二:onInterceptTouchEvent(默认返回false)

onInterceptTouchEvent是初学者很少见的方法,该方法用于筛选有利于该层面的View(Group)的MotionEvent对象,根据筛选的返回值判断一个MotionEvent对象是消费在该层还是继续传递给内层嵌套的子View(Group),通常父View拦截、消费触摸是没有意义的,因此默认不拦截这些对象流,都发给最顶层或者最内层的View。当然位于最内层的View即便拦截也是没有意义的,对象传到这里就必须得出个结果,所以在最顶层的子View拦截也得进入onTouchEvent处理,不拦截还是要进入这个方法处理的,因此没必要覆写什么拦截方法了。

方法三:dispatchTouchEvent(默认返回true)

dispatchTouchEvent对初学者来说更加少见,而涉及其详细处理逻辑的文章非常少见,小生作本文的初衷便是窥得该方法的冰山一角,以为它是条大鱼,想再挖掘点好玩的出来,可费了老大劲后才发现,其内部无非是对前两个方法的迭代调用,只不过逻辑有点耐人寻味而已。闻其名而知其意,dispatch--分发,该方法是前两者的集合,而且其返回状态直接决定MotionEvent对象是进入该层View(Group)的拦截过滤、分析处理模块,还是直接丢弃。该方法如此霸道,自有其霸道的资本,因为任何MotionEvent对象默认都是先走他的父类方法的,既然产生触摸事件,那这些对象自然是默认进行分发操作的,只因为很少如此绝决的阻断某一系列Event对象,初级程序员很少有必要对该方法进行覆写罢了,即便覆写该方法,其内部迭代逻辑我们是改变不了的,只能在直接丢弃某些MotionEvent对象时有所作为,不过通常类似丢垃圾的操作都在onTouch里面解决,当然要想保持onTouch的整洁,可以考虑在dispatch中干掉目标Event,免去后顾之忧。

小结:我想到了一个很具体的机构可以用来形容三个方法的关系--快递公司。

dispatch相当于收集快递的卡车;onIntercept相当于分拣员;onTouch相当于快递员。只有有价值的快递才会被装进收件的卡车,碰到砖头、空箱之类的废物就直接丢弃。等开车载着值得分发的快递回到公司,该轮到分拣员工作了。分拣员可能会把这一车快递分成两类,一类是本地的,另一类是外地的,本地的快递自然要找本地的快递员递送到客户手中,而外地的快递还要重新装车拉往外地子公司再次进行以上集中、筛选、递送流程。下图是我自己做的关于拦截原理的示意图:

拦截原理模拟图

盲点二:它们的协作逻辑是怎样的?

要描述三者之间的逻辑,还是用伪代码来演示更明了了吧:

//首先进入分发环节
dispatchTouchEvent()
{ //进入dispatchTouchEvent方法
	//判断当前对象的返回值,true - 进行分发(拦截、处理);false - 不进行分发,直接丢弃
	if(dispatch== true)
	{//进入onInterceptTouchEvent方法
		//其次判断当前onInterceptTouchEvent的返回值(是否被拦截)
		if(Intercept== false)
		{
			//没被拦截,得到此ViewGroup的全部子类:
			for (int i = count - 1; i >= 0; i--){
			//获取子View(Group)对象,对其进行分发检查
     			final View child = children[i];
			child.dispatchTouchEvent(this){···};
			}
    		}  else  {//进入onTouchEvent方法
			//如果ViewGroup被打断(onInterceptTouchEvent返回true),或者当前为最内(顶)层的纯View
			onTouchEvent();
		}
	}  else  {
	//将返回标志false的MotionEvent对象统统丢弃
	}
 }


小结:

MotionEvent对象首先流经dispatch,直接决定该对象分发、处理的必要性;dispatch返回true,才进入本层面的intercept拦截检查;拦截检查返回true的对象直接进入本层面的onTouch进行处理;拦截返回false的对象,将继续从底到上,从外到内传递给子类迭代这个分发、拦截、处理过程。

严正声明

上述观点大部分源于对开源知识的总结,小部分为个人通过Demo调试、分析获得,因此文章内容仅供参考,如有异议,小生洗耳恭听,在技术认知上求同存异、共同提高。下面是个人Demo的介绍。

yaongDemo主界面

提醒:本人习惯上把宿主(基本视图)相对于寄生者(内嵌视图)叫做(下),不适应的请转换一下思考角度。

本视图包含三层(View):

深褐色区域-自定义LinearLayout--MainView

墨绿色区域-自定义LinearLayout--InnerView

灰白色区域-自定义Button--BtnView

粉红色文字-仅作提示之用

操作方法:

触摸上半屏可以拦截该层以上(inner、btn)的Action_Move;

触摸下半屏可以拦截该层以上(btn)的的Action_Move

MainView.java如下(InnerView、BtnView的主体与此相同不再列出,只是对标签加以区分,便于分析日志):

package com.yaong.myview;

public class MainView extends LinearLayout {

	private final String TAG = "111";
	private int iStart = 0 ;
	public MainView(Context context) {
		super(context);
		// TODO Auto-generated constructor stub
	}

	public MainView(Context context, AttributeSet attrs) {
		super(context, attrs);
		// TODO Auto-generated constructor stub
	}

	@SuppressLint("NewApi")
	public MainView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		// TODO Auto-generated constructor stub
	}

	@SuppressLint("NewApi")
	@Override
	public boolean onInterceptTouchEvent(MotionEvent event) {
		// TODO Auto-generated method stub
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			Log.v(TAG, "III    DDD");
			break;
		case MotionEvent.ACTION_MOVE:
			Log.v(TAG, "III    MMM");
			//触摸屏幕上半边,拦截该View的所有ActionMove对象
			if (event.getY()<Constant.iCenterY) {
				return true;
			}
			break ;
		case MotionEvent.ACTION_CANCEL:
			Log.v(TAG, "III    CCC");
			break;
		case MotionEvent.ACTION_UP:
			Log.v(TAG, "III    UUU");
			break;
		default:
			break;
		}
		return super.onInterceptTouchEvent(event) ;
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		// TODO Auto-generated method stub
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			Log.v(TAG, "TTT    DDD");
			break;
		case MotionEvent.ACTION_MOVE:
			Log.v(TAG, "TTT    MMM");
			break;
		case MotionEvent.ACTION_CANCEL:
			Log.v(TAG, "TTT    CCC");
			break;
		case MotionEvent.ACTION_UP:
			Log.v(TAG, "TTT    UUU");
			break;
		default:
			break;
		}
		return super.onTouchEvent(event);
	}

	@Override
	public boolean dispatchTouchEvent(MotionEvent event) {
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			Log.v(TAG, "DDD    DDD");
			break ;
		case MotionEvent.ACTION_MOVE:
			Log.v(TAG, "DDD    MMM");
			break ;
		case MotionEvent.ACTION_CANCEL:
			Log.v(TAG, "DDD    CCC");
		case MotionEvent.ACTION_UP:
			Log.v(TAG, "DDD    UUU");
			break;
		default:
			break;
		}
		return super.dispatchTouchEvent(event);
	}
}

布局文件:activity_main.xml如下:

<com.yaong.myview.MainView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#44F00F00"
    android:orientation="vertical"
    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" >

    <com.yaong.myview.ViewInner1
        android:id="@+id/inner_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#4400FF00"
        android:orientation="vertical"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin" >


        <com.yaong.myview.View_MyButton
            android:id="@+id/btn_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="ABCABC"
            android:textColor="@color/clr3"
             >
        </com.yaong.myview.View_MyButton>

    </com.yaong.myview.ViewInner1>

</com.yaong.myview.MainView>
打印日志说明

标签Tag		111		代表MainView
		222 		代表InnerView
		333或444 	代表BtnView或TxtView
Text前半段 	DDD 		代表dispatch方法内
		III 		代表intercept方法内
		TTT 		代表touch方法内
Text后半段 	DDD 		代表Action_Down
		MMM 		代表Action_Move
		CCC 		代表Action_Cancel
		UUU 		代表Action_Up

罗列部分日志供分析参考、分析:

情况一:轻点目标MainView一下,TAG=111,

01-23 12:25:32.159: V/111(15128): DDD    DDD
01-23 12:25:32.159: V/111(15128): III    DDD
01-23 12:25:32.159: V/111(15128): TTT    DDD
01-23 12:25:32.279: V/111(15128): DDD    UUU
01-23 12:25:32.279: V/111(15128): TTT    UUU
01-23 12:25:32.279: E/MainAct(15128): main click
点击最底层红褐色区域,

Action_Down流程 dispatch(111)-->intercept(111)-->touch(111)

Action_Up流程 dispatch(111)-->touch(111)

分析:触摸事件的终点便是MainView,虽然在布局中内部嵌套了子View,但触摸与上层子View无关,只能由被点击View消费该事件,而Up事件作为Down的后续事件不必再进行拦截检测。

情况二:轻点目标InnerView一下,TAG=222,

01-23 13:34:52.559: V/111(17377): DDD    DDD
01-23 13:34:52.559: V/111(17377): III    DDD
01-23 13:34:52.559: I/222(17377): DDD    DDD
01-23 13:34:52.559: I/222(17377): III    DDD
01-23 13:34:52.559: I/222(17377): TTT    DDD
01-23 13:34:52.649: V/111(17377): DDD    UUU
01-23 13:34:52.649: V/111(17377): III    UUU
01-23 13:34:52.649: I/222(17377): DDD    UUU
01-23 13:34:52.649: I/222(17377): TTT    UUU
01-23 13:34:52.649: E/MainAct(17377): inner click
点击墨绿色环形区域,
Action_Down流程 dispatch(111)-->intercept(111)-->dispatch(222)-->intercept(222)-->touch(222)
Action_Up流程 dispatch(111)-->intercept(111)-->dispatch(222)-->touch(222)
分析:触摸事件的目标View是InnerView,触摸事件必然从父ViewGroup(111)传到子View(222),111中的拦截检测默认返回false,触摸事件继续向子View内传递,由于目标是InnerView,触摸事件将被222消费掉。

情况三:轻点目标BtnView一下,TAG=333,

01-23 13:46:14.679: V/111(17377): DDD    DDD
01-23 13:46:14.679: V/111(17377): III    DDD
01-23 13:46:14.679: I/222(17377): DDD    DDD
01-23 13:46:14.679: I/222(17377): III    DDD
01-23 13:46:14.679: E/333(17377): DDD    DDD
01-23 13:46:14.679: E/333(17377): TTT    DDD
01-23 13:46:14.769: V/111(17377): DDD    UUU
01-23 13:46:14.769: V/111(17377): III    UUU
01-23 13:46:14.769: I/222(17377): DDD    UUU
01-23 13:46:14.769: I/222(17377): III    UUU
01-23 13:46:14.769: E/333(17377): DDD    UUU
01-23 13:46:14.769: E/333(17377): TTT    UUU
01-23 13:46:14.769: E/MainAct(17377): btn click


点击中间白色区域,
Action_Down流程 dispatch(111)-->intercept(111)-->dispatch(222)-->intercept(222)-->dispatch(333)-->touch(333)
Action_Up流程 dispatch(111)-->intercept(111)-->dispatch(222)-->intercept(222)-->dispatch(333)-->touch(333)
分析:触摸事件的目标View是BtnView,触摸事件必然途径111、222,再传到333中,111、222中拦截状态皆是false,直到对象传至333中被消费掉。

情况四:触划上半屏BtnView(MainVIew将拦截该层的所有ActionMove对象)

01-23 14:49:31.189: V/111(18763): DDD    DDD
01-23 14:49:31.189: V/111(18763): III    DDD
01-23 14:49:31.189: I/222(18763): DDD    DDD
01-23 14:49:31.189: I/222(18763): III    DDD
01-23 14:49:31.189: E/333(18763): DDD    DDD
01-23 14:49:31.189: E/333(18763): TTT    DDD
01-23 14:49:31.259: V/111(18763): DDD    MMM
01-23 14:49:31.259: V/111(18763): III    MMM
01-23 14:49:31.259: I/222(18763): DDD    CCC
01-23 14:49:31.259: I/222(18763): DDD    UUU
01-23 14:49:31.259: I/222(18763): III    CCC
01-23 14:49:31.259: E/333(18763): DDD    CCC
01-23 14:49:31.259: E/333(18763): TTT    CCC
01-23 14:49:31.289: V/111(18763): DDD    MMM
01-23 14:49:31.289: V/111(18763): TTT    MMM
//此处省略无限多111的Move状态日志······
01-23 14:49:31.399: V/111(18763): DDD    UUU
01-23 14:49:31.399: V/111(18763): TTT    UUU
由于在MainView的onInterceptTouchEvent种对Action_Move进行了拦截,那么本应该传到Btn的Touch中的Action_Move对象将被拦截在111中,除了底层111的onTouch能接收Action_Move,其嵌套的InnerView、BtnView都将接收不到Move事件,也就是上面日志中随着移动手指,111将产生无限多Move事件,而另外两者则一直沉默。不过,困扰我的是日志中粉色背景色的周围的日志,ActionMove被拦截在111中后,222中产生一个ActionCancel事件,然后演变成UP事件,但222有Down记录,也再此产生了Up记录,但并没有产生一个对222的click事件。

情况五:触划下半屏BtnView(InnerVIew将拦截该层的所有ActionMove对象)

01-23 15:26:12.739: V/111(18763): DDD    DDD
01-23 15:26:12.739: V/111(18763): III    DDD
01-23 15:26:12.739: I/222(18763): DDD    DDD
01-23 15:26:12.739: I/222(18763): III    DDD
01-23 15:26:12.739: E/333(18763): DDD    DDD
01-23 15:26:12.739: E/333(18763): TTT    DDD
01-23 15:26:12.819: V/111(18763): DDD    MMM
01-23 15:26:12.819: V/111(18763): III    MMM
01-23 15:26:12.819: I/222(18763): DDD    MMM
01-23 15:26:12.819: I/222(18763): III    MMM
01-23 15:26:12.819: E/333(18763): DDD    CCC
01-23 15:26:12.819: E/333(18763): TTT    CCC
01-23 15:26:12.839: V/111(18763): DDD    MMM
01-23 15:26:12.839: V/111(18763): III    MMM
01-23 15:26:12.839: I/222(18763): DDD    MMM
01-23 15:26:12.839: I/222(18763): TTT    MMM
//此处省略无限多111、222的Move状态日志······
01-23 15:26:12.849: V/111(18763): DDD MMM
01-23 15:26:12.849: V/111(18763): III MMM
01-23 15:26:12.849: I/222(18763): DDD MMM
01-23 15:26:12.849: I/222(18763): TTT MMM
01-23 15:26:12.899: V/111(18763): DDD UUU
01-23 15:26:12.899: V/111(18763): III UUU
01-23 15:26:12.899: I/222(18763): DDD UUU
01-23 15:26:12.899: I/222(18763): TTT UUU
在该测试中,触划按钮,InnerView将对ActionMove对象进行拦截,只不过比情况四拦截的晚一个阶段,有日志信息可知Move事件在222被拦截住后,再333中产生了一个Cancel事件,该Cancel在BtnView的onTouchEvent中消费完后就陷入了沉默,而111、222依然在很有规律的打印Move信息。
情况六:更改222拦截位置到ActionDown,触划BtnView(InnerVIew将拦截该层的所有ActionDown对象)
01-23 15:43:36.129: V/111(23276): DDD    DDD
01-23 15:43:36.129: V/111(23276): III    DDD
01-23 15:43:36.129: I/222(23276): DDD    DDD
01-23 15:43:36.129: I/222(23276): III    DDD
01-23 15:43:36.129: I/222(23276): TTT    DDD
01-23 15:43:36.329: V/111(23276): DDD    MMM
01-23 15:43:36.329: V/111(23276): III    MMM
01-23 15:43:36.329: I/222(23276): DDD    MMM
01-23 15:43:36.329: I/222(23276): TTT    MMM
//此处省略无限多111、222的Move状态日志······
01-23 15:43:36.689: V/111(23276): DDD UUU
01-23 15:43:36.689: V/111(23276): III UUU
01-23 15:43:36.689: I/222(23276): DDD UUU
01-23 15:43:36.699: I/222(23276): TTT UUU
01-23 15:43:36.699: E/MainAct(23276): inner click
这种情况与情况五相似,而其差别在于,一旦Down事件被拦截,BtnView将不可能受到任何MotionEvent对象,也未在333中发生 收不到Move事件,莫名产生一个Cancel事件的情况,而且222产生了一个完整的Click事件。 
情况七:更改222的dispatch方法,在ActionMove事件后返回标志false(不分发Move事件)
01-23 15:55:01.849: V/111(23981): DDD    DDD
01-23 15:55:01.849: V/111(23981): III    DDD
01-23 15:55:01.849: I/222(23981): DDD    DDD
01-23 15:55:01.849: I/222(23981): III    DDD
01-23 15:55:01.849: E/333(23981): DDD    DDD
01-23 15:55:01.849: E/333(23981): TTT    DDD
01-23 15:55:01.899: V/111(23981): DDD    MMM
01-23 15:55:01.899: V/111(23981): III    MMM
01-23 15:55:01.899: I/222(23981): DDD    MMM
01-23 15:55:01.929: V/111(23981): DDD    MMM
01-23 15:55:01.929: V/111(23981): III    MMM
01-23 15:55:01.929: I/222(23981): DDD    MMM
01-23 15:55:01.949: V/111(23981): DDD    MMM
01-23 15:55:01.949: V/111(23981): III    MMM
01-23 15:55:01.949: I/222(23981): DDD    MMM
01-23 15:55:01.959: V/111(23981): DDD    MMM
01-23 15:55:01.959: V/111(23981): III    MMM
01-23 15:55:01.959: I/222(23981): DDD    MMM
01-23 15:55:01.979: V/111(23981): DDD    MMM
01-23 15:55:01.979: V/111(23981): III    MMM
01-23 15:55:01.979: I/222(23981): DDD    MMM
01-23 15:55:01.999: V/111(23981): DDD    MMM
01-23 15:55:01.999: V/111(23981): III    MMM
01-23 15:55:01.999: I/222(23981): DDD    MMM
01-23 15:55:02.069: V/111(23981): DDD    UUU
01-23 15:55:02.069: V/111(23981): III    UUU
01-23 15:55:02.069: I/222(23981): DDD    UUU
01-23 15:55:02.069: I/222(23981): III    UUU
01-23 15:55:02.069: E/333(23981): DDD    UUU
01-23 15:55:02.069: E/333(23981): TTT    UUU
01-23 15:55:02.069: E/MainAct(23981): btn click
如果在dispatch中改动Move事件的返回标志,则每个Move对象传递到dispatch时都卡住了,不能进入本层以及内层的intercept、onTouch方法,因此归结其原因为dispatch返回false的所有对象都被丢弃了,不可能再往内层传递。因此dispatch是MotionEvent处理的重要方法,但一般不轻易在dispatch里面做手脚。

总结
经过对Demo的各种改最终就得到了上面那点理解,可能测试过程比较混乱,导致结果与预期的有所偏差,因此贴出此文以求改进,如果某位也愿意考究onTouch、onIntercept、dispatch里面的学问,可以考虑去下载我的测试Demo,当然自己写一个也不费啥事,文中不实之处,还望指正,共同完善。






















































  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值