Android事件传递机制
在实际的开发中,我们经常需要自定义一些控件,这就需要我们对控件的事件响应机制有所了解。探讨Android事件传递机制前,需要明确android的两大基础控件类型:View和ViewGroup。View即普通的控件,没有子布局的,如Button、TextView。 ViewGroup继承自View,表示可以有子控件,如Linearlayout、Listview这些。我们所关注的事件操作主要就是发生在View和ViewGroup之间。View和ViewGroup中主要有如下的3个方法来处理事件的响应操作。
- public boolean dispatchTouchEvent(MotionEvent event)
- public boolean onTouchEvent(MotionEvent event)
- public boolean onInterceptTouchEvent(MotionEvent event)
需要注意的是,dispatchTouchEvent和onTouchEvent方法在View和viewGroup中均存在,而onInterceptTouchEvent方法只存在于ViewGroup中。我们还可以观察到这些方法的返回值全部都是boolean型,都返回true或者是false,这是因为事件传递的过程就是一个接一个,某一个点后根据方法boolean的返回值判断是否要继续往下传递。在Android中,所有的事件都是一步步的传递下去,直到被消费了为止。这些方法的返回值就决定了某一事件是否是继续往下传,还是被拦截了,或是被消费了。返回true表示事件被消费了,这是后续的事件响应函数便不会再被执行。返回false,则事件继续传递下去。
此外,view或ViewGroup还可以设置点击监听onClick和触摸监听onTouch,onClick和onTouch也是事件的响应的函数,所以对于View来说,事件的响应函数一共有4个,对于ViewGroup来说一共有5个。我们可以通过下面的代码来设置onClick和onTouch:
设置onClick:
myView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
}
});
设置onTouch:
myView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub
return false;
}
});
从原理上来讲,明确事件的传递机制,就是弄清楚dispatchTouchEvent、onTouchEvent、onInterceptTouchEvent、onTouch、onClick的执行顺序的关系。下面我们就分别研究View和ViewGroup的事件传递机制。
View中事件传递
对于View来说,事件传递机制有四个函数:dispatchTouchEvent、onTouchEvent、onClick和onTouch。其中dispatchTouchEvent和onTouchEvent是View中的,onClick和onTouch是我们自己设置的回调函数,当然,我们也可以不设置onClick和onTouch,这样事件传递时会直接忽略这两个函数。为了直观的了解,我们先在Eclipse中新建一个工程,并新建一个MyButton类继承于Button,用来实现我们对按钮事件的跟踪。
MyButton.java的代码如下:
package com.example.eventdispatch;
import android.content.Context;
import android.view.MotionEvent;
import android.widget.Button;
public class MyButton extends Button {
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
System.out.println("MyButton---dispatchTouchEvent---Down");
break;
case MotionEvent.ACTION_UP:
System.out.println("MyButton---dispatchTouchEvent---Up");
break;
case MotionEvent.ACTION_MOVE:
System.out.println("MyButton---dispatchTouchEvent---Move");
break;
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
System.out.println("MyButton---onTouchEvent---Down");
break;
case MotionEvent.ACTION_UP:
System.out.println("MyButton---onTouchEvent---Up");
break;
case MotionEvent.ACTION_MOVE:
System.out.println("MyButton---onTouchEvent---Move");
break;
}
return super.onTouchEvent(event);
}
}
activity_main.xml如下
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
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="com.example.eventdispatch.MainActivity" >
<com.example.eventdispatch.MyButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MyButton"
android:id="@+id/btn1" />
</RelativeLayout>
MainActivity.java如下:
package com.example.eventdispatch;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
import android.widget.Button;
public class MainActivity extends ActionBarActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyButton btn = (MyButton) findViewById(R.id.btn1);
btn.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
System.out.println("MyButton---onTouch---Down");
break;
case MotionEvent.ACTION_UP:
System.out.println("MyButton---onTouch---Up");
break;
case MotionEvent.ACTION_MOVE:
System.out.println("MyButton---onTouch---Move");
break;
}
return false;
}
});
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
System.out.println("MyButton clicked!");
}
});
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
System.out.println("Activity---onTouchEvent---Down");
break;
case MotionEvent.ACTION_UP:
System.out.println("Activity---onTouchEvent---Up");
break;
case MotionEvent.ACTION_MOVE:
System.out.println("Activity---onTouchEvent---Move");
break;
}
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// TODO Auto-generated method stub
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
System.out.println("Activity---dispatchTouchEvent---Down");
break;
case MotionEvent.ACTION_UP:
System.out.println("Activity---dispatchTouchEvent---Up");
break;
case MotionEvent.ACTION_MOVE:
System.out.println("Activity---dispatchTouchEvent---Move");
break;
}
return super.dispatchTouchEvent(ev);
}
}
运行工程,点击Activity界面中除Button以外的其它地方,我们会看到如下的输出结果:
点击Button,我们会看到如下的输出结果:
通过日志输出可以看到,点击屏幕ACTION_DOWN时候首先执行了Activity的dispatchTouchEvent方法进行事件分发,然后执行了onTouchEvent(MotionEvent event)方法;点击Button的时候,首先执行了Activity的dispatchTouchEvent方法进行事件分发,然后执行了Button的dispatchTouchEvent方法进行事件分发,然后执行了Button的onTouch和onTouchEvent(MotionEvent event)方法。流程图如下:
我们知道Android中所有事件处理方法的返回值都是boolean类型的,现在我们来修改这个返回值,来看看会发生什么变化。首先从Activity开始,根据之前的日志输出结果,首先执行的是Activity的dispatchTouchEvent方法,现在将之前的返回值super.dispatchTouchEvent(event)修改为true,然后重新编译运行并点击按钮,看到如下的日志输出结果。
可以看到,事件执行到dispatchTouchEvent方法就没有再继续往下分发了,这表示事件在这个函数中已经被消耗了或是被处理了,后续的函数不需要继续执行了。
接着,将上述修改还原,让事件在Activity这继续往下分发,接着就分发到了MyButton,将MyButton的dispatchTouchEvent方法的返回值修改为true,重新编译运行并查看输出日志结果。
从结果可以看到,事件在MyButton的dispatchTouchEvent方法中就没有再继续往下分发了。接着将上述修改还原,将MyButton的onTouchEvent方法返回值修改为true,让其消费事件,根据之前的分析,onClick方法是在onTouchEvent方法中被调用的,事件在这被消费后将不会调用onClick方法了,编译运行,得到如下日志输出结果。
跟分析结果一样,onClick方法并没有被执行,因为事件在MyButton的onTouchEvent方法中被消费了。下图是整个事件传递的流程图。
到这里,我们已经对Android的事件拦截机制有了一个大概的了解。下面我们来看看ViewGroup的事件传递机制。
Android嵌套布局事件传递
对于ViewGroup来说,事件传递机制有三个函数:dispatchTouchEvent、onTouchEvent和onInterceptTouchEvent。onInterceptTouchEvent表示拦截的意思,该函数后面会讲到,这里可以先不管。
同样,我们新建一个MyLayout.java文件,代码如下:
package com.example.eventdispatch;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.LinearLayout;
public class MyLayout extends LinearLayout {
public MyLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
System.out.println("MyLayout---dispatchTouchEvent---Down");
break;
case MotionEvent.ACTION_UP:
System.out.println("MyLayout---dispatchTouchEvent---Up");
break;
case MotionEvent.ACTION_MOVE:
System.out.println("MyLayout---dispatchTouchEvent---Move");
break;
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
System.out.println("MyLayout---onInterceptTouchEvent---Down");
break;
case MotionEvent.ACTION_UP:
System.out.println("MyLayout---onInterceptTouchEvent---Up");
break;
case MotionEvent.ACTION_MOVE:
System.out.println("MyLayout---onInterceptTouchEvent---Move");
break;
}
return super.onInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
System.out.println("MyLayout---onTouchEvent---Down");
break;
case MotionEvent.ACTION_UP:
System.out.println("MyLayout---onTouchEvent---Up");
break;
case MotionEvent.ACTION_MOVE:
System.out.println("MyLayout---onTouchEvent---Move");
break;
}
return super.onTouchEvent(event);
}
}
同时修改activity_main.xml如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
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="com.example.eventdispatch.MainActivity" >
<com.example.eventdispatch.MyLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/layout1">
<com.example.eventdispatch.MyButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MyButton"
android:id="@+id/btn1" />
</com.example.eventdispatch.MyLayout>
</LinearLayout>
最后在MainActivity.java中为mylayout设置onClick和onTouch事件:
MyLayout ml = (MyLayout) findViewById(R.id.layout1);
ml.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
System.out.println("MyLayout clicked!");
}
});
ml.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
System.out.println("MyLayout---onTouch---DOWN");
break;
case MotionEvent.ACTION_MOVE:
System.out.println("MyLayout---onTouch---MOVE");
break;
case MotionEvent.ACTION_UP:
System.out.println("MyLayout---onTouch---UP");
break;
default:
break;
}
return false;
}
});
运行工程,点击Button,输出如下:
注意:上面只有Button响应了click事件,Layout是没有响应click事件的,这一点很重要,它与Layout的消息截取有关,具体的关系后面会讲到。
我们将MyButton的dispatchTouchEvent方法的返回值修改为true,重新编译运行并查看输出日志结果。
从结果可以看到,事件在MyButton的dispatchTouchEvent方法中就没有再继续往下分发了。接着将上述修改还原,将MyButton的onTouchEvent方法返回值修改为true,让其消费事件,编译运行,得到如下日志输出结果。
可以看到,事件传递到了mybutton的onTouchEvent之后就没有再继续传递下去。这与我们上面所讲的相吻合。
onInterceptTouchEvent
现在我们来看看onInterceptTouchEvent方法。修改onInterceptTouchEvent的返回值为true。重新编译运行,如下
可以看到,我们明明点击的按钮,但输出结果显示MyLayout点击事件被执行了,再通过输出结果分析,对比上次的输出结果,发现本次的输出结果完全没有MyButton的信息,没错,由于onInterceptTouchEvent方法我们返回了true,在这里就将事件拦截了,所以他不会继续分发给MyButton了,反而交给自身的onTouchEvent方法执行了,理所当然,最后执行的就是MyLayout的点击事件了。
总结
Android系统中事件是通过层级传递的,一次事件传递对应一个完整的层级关系。事件传递是从ViewGroup传递到View的,而不是反过来传递的。 Android事件最先由系统调用Activity的dispatchTouchEvent方法,分发该事件。根据触摸事件的坐标,将此事件传递给容器控件或者显示控件的dispatchTouchEvent处理,如果是容器控件则调用onInterceptTouchEvent 判断事件是由自己处理,还是继续分发给子View。此处如果容器控件不处理Touch事件,故根据事件发生坐标,将事件传递给它的直接子View(容器控件或者显示控件)。onTouch事件要先于onClick事件执行,onTouch后于dispatchTouchEvent中调用,而onClick在事件处理方法后于onTouchEvent被调用,onTouchEvent要后于dispatchTouchEvent方法的调用。其他控件事件处理过程同上。最后附上一张图。