Android Touch事件传递

前言

Android应用开发中,难免会对View进行封装和定制,除了静态的绘制工作,就是对事件的响应,本文是对Android事件传递机制的讨论,重点是对onInterceptTouchEvent方法和onTouchEvent方法的逻辑进行解释。

用来测试的ActivityView结构如下:


View树结构

其中ViewGroup1ViewGroup2均为自定义的LinearLayout,覆写了onInterceptTouchEventonTouchEvent方法;View1为自定义的View,覆写了onTouchEvent方法。VG1VG2V1覆写的方法的返回值,由上方的5Checkbox控制,可以在调试时动态设置,不必重新Deploy;同时,在Activity中配置了onTouchListeneronClickListener监听器,onTouch方法返回false,以便测试onTouchEventonTouchonClick方法的调用顺序(注意,onClick只有在调用父类的onTouchEvent方法时才有效,因为onClick是在ViewonTouchEvent中触发调用的)。

名词

在进入讨论之前,先确定几个名词的解释:

l 事件:屏幕捕获的最小动作单位:DOWNMOVEUP,不包括CLICK等组合事件;一次对屏幕的操作,可以理解为事件流,1DOWNUP,多次MOVE

l 传递:事件由上一层控件分发给下一层控件,即上层控件调用下层控件的onXxx方法,将事件对象作为参数传递。

l 消耗:在onTouchEventonTouchListeneronTouch方法中返回true。返回值由调用者(即上层控件)接收并做相应处理。

l 抛弃:事件没有消耗,即被抛弃,同时会阻塞下一个事件。

代码

    VG1 关键代码如下。VG2V1类似,就不贴了。

@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {

		switch (ev.getAction()) {
		case MotionEvent.ACTION_DOWN:
			Log.d(TAG, TAG + ".onInterceptTouchEvent ACTION_DOWN");
			break;

		case MotionEvent.ACTION_MOVE:
			Log.d(TAG, TAG + ".onInterceptTouchEvent ACTION_MOVE");
			break;

		case MotionEvent.ACTION_UP:
			Log.d(TAG, TAG + ".onInterceptTouchEvent ACTION_UP");
			break;

		case MotionEvent.ACTION_CANCEL:
			Log.d(TAG, TAG + ".onInterceptTouchEvent ACTION_CANCEL");
			break;
		}

		return TEDApplication.VG1IT;
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {

		switch (event.getAction()) {

		case MotionEvent.ACTION_DOWN:
			Log.d(TAG, TAG + ".onTouchEvent ACTION_DOWN");
			break;

		case MotionEvent.ACTION_MOVE:
			Log.d(TAG, TAG + ".onTouchEvent ACTION_MOVE");
			break;

		case MotionEvent.ACTION_UP:
			Log.d(TAG, TAG + ".onTouchEvent ACTION_UP");
			break;

		case MotionEvent.ACTION_CANCEL:
			Log.d(TAG, TAG + ".onTouchEvent ACTION_CANCEL");
			break;

		}
		return TEDApplication.VG1T;
	}

测试Acitivity代码:

public class MainActivity extends Activity implements OnCheckedChangeListener, OnClickListener, OnTouchListener {

	private final String TAG = "MainActivity";

	private ViewGroup1 vg1;
	private ViewGroup2 vg2;
	private View1 v1;

	private CheckBox[] cbs = new CheckBox[5];

	private int[] ids = new int[] { R.id.cb1, R.id.cb2, R.id.cb3, R.id.cb4, R.id.cb5 };

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		requestWindowFeature(Window.FEATURE_NO_TITLE);
		setContentView(R.layout.activity_main);

		for (int i = 0; i < 5; i++) {
			cbs[i] = (CheckBox) findViewById(ids[i]);
			cbs[i].setOnCheckedChangeListener(this);
		}

		cbs[0].setChecked(TEDApplication.VG1IT);
		cbs[1].setChecked(TEDApplication.VG1T);
		cbs[2].setChecked(TEDApplication.VG2IT);
		cbs[3].setChecked(TEDApplication.VG2T);
		cbs[4].setChecked(TEDApplication.V1T);

		vg1 = (ViewGroup1) findViewById(R.id.vg1);
		vg1.setClickable(true);
		vg1.setOnClickListener(this);
		vg1.setOnTouchListener(this);
		vg2 = (ViewGroup2) findViewById(R.id.vg2);
		vg2.setClickable(true);
		vg2.setOnClickListener(this);
		vg2.setOnTouchListener(this);
		v1 = (View1) findViewById(R.id.v1);
		v1.setClickable(true);
		v1.setOnClickListener(this);
		v1.setOnTouchListener(this);

	}

	@Override
	public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
		switch (buttonView.getId()) {
		case R.id.cb1:
			TEDApplication.VG1IT = isChecked;
			break;
		case R.id.cb2:
			TEDApplication.VG1T = isChecked;
			break;
		case R.id.cb3:
			TEDApplication.VG2IT = isChecked;
			break;
		case R.id.cb4:
			TEDApplication.VG2T = isChecked;
			break;
		case R.id.cb5:
			TEDApplication.V1T = isChecked;
			break;

		default:
			break;
		}
	}

	@Override
	public boolean onTouch(View v, MotionEvent event) {
		switch (v.getId()) {
		case R.id.vg1:
			Log.d(TAG, "vg1.onTouch");
			break;
		case R.id.vg2:
			Log.d(TAG, "vg2.onTouch");
			break;
		case R.id.v1:
			Log.d(TAG, "v1.onTouch");
			break;
		default:
			break;
		}
		return false;
	}

	@Override
	public void onClick(View v) {
		switch (v.getId()) {
		case R.id.vg1:
			Log.d(TAG, "vg1.onClick");
			break;
		case R.id.vg2:
			Log.d(TAG, "vg2.onClick");
			break;
		case R.id.v1:
			Log.d(TAG, "v1.onClick");
			break;
		default:
			break;
		}
	}
}

不同返回值组合的返回结果

为简化书写,对于上方5个Checkbox的值,用5位二进制表示,每位对应相应位置的值,1为true,即对应的方法返回true,0为false,x表示任意。触摸位置无特别说明均为V1。

1.  00000

效果:不管touch的时间有多长,只打印以下log:

分析:

a)  因为返回值均为false,依次调用VG1和VG2的IT方法,将事件传递给V1,因为V1依然返回false,所以依次再向上传递给VG2和VG1的T方法,注意,向上传递时是倒序。

b)  在Listener中的onTouch方法均在该View的T方法之前调用(这是View的处理机制决定的~),以下为了简洁,关闭Listener中的onTouch方法。

2.  00000,触摸VG2

效果:同上,不管触摸多长时间,只显示以下log:


分析:同上,因为返回值均为false,只处理DOWN事件,且没有任何一层进行消耗,所以抛弃,并阻塞MOVE事件。

3. 0x0x1,也是默认的返回值模式,IT均返回为false,T返回true

效果:根据事件流依次处理DOWN、MOVE、UP事件。


分析:

a)        事件经VG1和VG2的IT传递到V1的T方法,V1的T方法消耗事件,同时View树接收下一个事件。具体流程见下文的事件传递流程图。

b)       因为V1消耗了事件,所以事件不会传递个VG2和VG1的T方法,所以返回值对结果无影响。

4.        0010x,VG2的IT方法返回true

效果:无论触摸多长时间,只打印以下log:


分析:

a)        因为VG2的IT方法返回true,所以事件在VG2一层遭到拦截,直接传递给VG2(自身)的T事件。

b)       VG2的T返回false,所以事件向上传递给VG1的T事件,随后抛弃,并阻塞下一个事件。

c)        V1没有收到事件,所以其T方法的返回值对结果没有影响。同时可以得出结论,只要ViewGroup的IT方法返回true,其包含的View均收不到事件!(其实这是废话,要不Google设计IT方法就没用了……不过这里比较绕,废话一点没坏处^-^)

5.  0011x,这可能是经常用到的模式

效果:事件不会阻塞, VG2的IT方法只接收DOWN事件,其余的MOVE和UP事件直接由VG1的IT方法传递给VG2的T方法


分析:

a)        由于VG2的IT方法返回true,即表示拦截事件,同时VG2的T方法返回true,表示消耗事件,所以事件不会阻塞。

b)       这里有一个值得注意的点,就是ViewGroup的IT方法,实际上针对的是事件流,Google的工程师很高明!事件流是从DOWN事件开始的,也就是说,只要在传递某个事件流的DOWN事件时发现VG2的IT方法返回true,则以后这个事件流的所有事件均直接传递给VG2的T方法。

6.        0110x

效果:和上一个模式类似:


分析:

a)        事件经过VG1、VG2的IT方法传递到VG2的T方法,但是VG2的T方法返回false,所以向上传递给VG1的T方法,事件在此处被消耗。此后事件不再传递给VG1和VG2的IT方法,直接传递给VG1的T方法!

b)       联合上一个模式可以得出结论:IT方法只是在事件流的DOWN事件传递时有效,作用是确定事件是否在ViewGroup处拦截,也就是确定事件最终的消耗者,之后便直接将事件传递给消耗者的T事件进行处理。再次膜拜Google的工程师!

c)        测试时可以看到,在确定事件的最终消耗者之后,即DOWN事件被消耗之后,log有短暂停顿,之后,MOVE事件才流畅传递,应该是系统在对事件传递路径做存储(纯猜测)。算是从侧面验证了上面的结论吧~

7.        10xxx,大家应该猜得到效果了吧~

效果:

分析:事件由VG1的IT方法传递给VG1的T方法,之后抛弃,事件流阻塞。

8.        11xxx

效果:额……如果不看第一个DOWN,跟普通View的效果没差别


分析:VG1的IT方法返回true,拦截事件,由VG1的T消耗,此后直接传递给VG1。

 


总结

经过上面的Demo,大家应该都了解这个机制了。下面在做总结前先贴出一张图,帮助大家整理思路:



在上面的流程图中:

1. 方框表示ViewViewGroupITT方法,因为VGIT方法是ViewGroupdispatchTouchEvent方法的依据,也是T方法是否调用的决定者(不考虑事件回传的情况下),所以将T画在了IT的内部,这样也方便辨析各个控件。

2. E表示事件

3. 绿色返回值线路表示默认线路。

好了,下面是总结了:

1. Android的屏幕事件均为touch事件或touch事件在不同空间和时间组成,所以在讨论事件传递时,可以只讨论touch事件。

2. touch事件按照事件流顺序依次传递给控件树,前一个事件没有被消耗时,后一个事件不会传递给控件树。

3. 事件被屏幕捕获之后,在Android的控件树上由上至下传递。如果事件被某个View消耗,则开始传递下一个事件。

4. onInterceptTouchEventViewGroup的方法,作用是在事件传递过程中,将事件拦截,不再向下层传递,而是传递给自己的onTouchEvent方法进行处理。

5. onTouchEvent方法返回值为false时,表示没有消耗事件,事件将传递给上层控件的onTouchEvent方法。

6. 处理touch事件的方式有两个,一个是继承View,覆写onTouchEvent方法,一个是在Activity中使用setOnTouchEventListener,在onTouch方法中处理。其中后者的优先级高于前者,即先调用ListeneronTouch,后调用onTouchEvent

7. 两个方法的默认返回值为:onInterceptTouchEvent返回falseonTouchEvent返回true,即ViewGroup默认将事件传递给子View,而所有的ViewonTouchEvent都会消耗事件。所以默认情况下:事件会由最底层的View处理,即,上层的所有ViewGroup接收到事件之后立刻传递给下一层。

8. ViewGrouponInterceptTouchEvent方法针对的是“是否让自己的T方法直接消耗事件流这个问题,所以如果事件流最终确定由自身消耗,那么IT方法只对事件流的DOWN事件有效,一旦消耗者确定,该方法将不再调用,而是直接传递事件给自己的T方法。

后记

我当初在这个问题上绕了很久,后来才想通了。虽然看了一些源码,但是看的还不够,还没有完全把握到这个机制的实现方式。如果大家对上面的结论或验证方法有什么建议,欢迎讨论。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值