前言
Android应用开发中,难免会对View进行封装和定制,除了静态的绘制工作,就是对事件的响应,本文是对Android事件传递机制的讨论,重点是对onInterceptTouchEvent方法和onTouchEvent方法的逻辑进行解释。
用来测试的Activity和View结构如下:
View树结构
其中ViewGroup1和ViewGroup2均为自定义的LinearLayout,覆写了onInterceptTouchEvent和onTouchEvent方法;View1为自定义的View,覆写了onTouchEvent方法。VG1、VG2、V1覆写的方法的返回值,由上方的5个Checkbox控制,可以在调试时动态设置,不必重新Deploy;同时,在Activity中配置了onTouchListener和onClickListener监听器,onTouch方法返回false,以便测试onTouchEvent和onTouch、onClick方法的调用顺序(注意,onClick只有在调用父类的onTouchEvent方法时才有效,因为onClick是在View的onTouchEvent中触发调用的)。
名词
在进入讨论之前,先确定几个名词的解释:
l 事件:屏幕捕获的最小动作单位:DOWN、MOVE、UP,不包括CLICK等组合事件;一次对屏幕的操作,可以理解为事件流,1次DOWN、UP,多次MOVE。
l 传递:事件由上一层控件分发给下一层控件,即上层控件调用下层控件的onXxx方法,将事件对象作为参数传递。
l 消耗:在onTouchEvent或onTouchListener的onTouch方法中返回true。返回值由调用者(即上层控件)接收并做相应处理。
l 抛弃:事件没有消耗,即被抛弃,同时会阻塞下一个事件。
代码
VG1 关键代码如下。VG2和V1类似,就不贴了。
@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. 方框表示View或ViewGroup的IT或T方法,因为VG的IT方法是ViewGroup的dispatchTouchEvent方法的依据,也是T方法是否调用的决定者(不考虑事件回传的情况下),所以将T画在了IT的内部,这样也方便辨析各个控件。
2. E表示事件
3. 绿色返回值线路表示默认线路。
好了,下面是总结了:
1. Android的屏幕事件均为touch事件或touch事件在不同空间和时间组成,所以在讨论事件传递时,可以只讨论touch事件。
2. touch事件按照事件流顺序依次传递给控件树,前一个事件没有被消耗时,后一个事件不会传递给控件树。
3. 事件被屏幕捕获之后,在Android的控件树上由上至下传递。如果事件被某个View消耗,则开始传递下一个事件。
4. onInterceptTouchEvent是ViewGroup的方法,作用是在事件传递过程中,将事件拦截,不再向下层传递,而是传递给自己的onTouchEvent方法进行处理。
5. onTouchEvent方法返回值为false时,表示没有消耗事件,事件将传递给上层控件的onTouchEvent方法。
6. 处理touch事件的方式有两个,一个是继承View,覆写onTouchEvent方法,一个是在Activity中使用setOnTouchEventListener,在onTouch方法中处理。其中后者的优先级高于前者,即先调用Listener的onTouch,后调用onTouchEvent。
7. 两个方法的默认返回值为:onInterceptTouchEvent返回false,onTouchEvent返回true,即ViewGroup默认将事件传递给子View,而所有的View的onTouchEvent都会消耗事件。所以默认情况下:事件会由最底层的View处理,即,上层的所有ViewGroup接收到事件之后立刻传递给下一层。
8. ViewGroup的onInterceptTouchEvent方法针对的是“是否让自己的T方法直接消耗事件流”这个问题,所以如果事件流最终确定由自身消耗,那么IT方法只对事件流的DOWN事件有效,一旦消耗者确定,该方法将不再调用,而是直接传递事件给自己的T方法。
后记
我当初在这个问题上绕了很久,后来才想通了。虽然看了一些源码,但是看的还不够,还没有完全把握到这个机制的实现方式。如果大家对上面的结论或验证方法有什么建议,欢迎讨论。