Android开发中经常会遇到多个View、ViewGroup嵌套的情况,
此时就可能遇到滑动冲突的问题。
为了这种问题,就必须对View的事件传递机制有一定的了解。
本篇博客就以一些简单的例子,
来看看Activity、View、ViewGroup三者的触摸事件传递机制。
一、基本概念
Android中的触摸事件对应于MotionEvent类,事件的类型包括ACTION_DOWN、ACTION_UP、ACTION_MOVE等。
不同事件类型的意义,大家可以参看源码对应的注释信息,此处不做赘述。
一次完整的事件传递主要包括三个阶段,分别是事件的分发、拦截和消费。
分发:
事件的分发对应着dispatchTouchEvent方法,原型如下:
public boolean dispatchTouchEvent(MotionEvent event)
Android系统中,所有的触摸事件都是通过该方法来分发的。
自定义视图时,可以复写该方法实现自己的事件分发逻辑。
这个方法一般根据当前视图的具体实现,决定是直接消费事件,
还是将事件继续分发给子视图处理。
若该方法返回true,则表示事件被当前视图消费掉,不再继续分发;
若该方法返回super.dispatchTouchEvent,则表示继续分发事件。
拦截:
事件的拦截对应着onInterceptTouchEvent方法,原型如下:
public boolean onInterceptTouchEvent(MotionEvent ev)
该方法仅在ViewGroup及其子类中存在。
自定义视图时,可以复写该方法实现自己的事件拦截逻辑。
当该方法返回true时,表示当前视图拦截事件,不再将事件分发给子视图,
同时会将事件交由当前视图消费;
当该方法返回false或super.onInterceptTouchEvent时,
表示当前视图不对事件进行拦截,将事件分发给子视图。
消费:
事件的消费对应着onTouchEvent,方法原型如下:
public boolean onTouchEvent(MotionEvent event)
自定义视图时,可以复写该方法实现自己的事件消费逻辑。
当该方法返回true时,表示当前视图可以处理对应的事件,事件不会在向上传递给父视图;
当该方法返回false时,表示当前视图不处理这个事件,事件会被递交给父视图的onTouchEvent处理。
二、事件传递机制
前文已经提到过了,在Android系统中,拥有事件传递、处理能力的类有以下三种:
Activity: 拥有dispatchTouchEvent和onTouchEvent两个方法;
ViewGroup:拥有dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三个方法;
View:拥有dispatchTouchEvent和onTouchEvent两个方法。
接下来我们通过例子,主要分析下View和ViewGroup的事件传递机制。
Activity传递事件时表现得与View比较相似,就不专门分析了。
2.1 View的事件传递机制
首先我们结合例子,看看View的事件传递机制。
考虑到ViewGroup本身就是View的子类,此处的View指的是除去ViewGroup外的控件。
即本身已经是最小的单位,不能再作为其它View容器的View控件。
为了比较直观的看清出事件传递的过程,我们自定义一个继承TextView的类,
并复写分发和消费对应的函数,并增加一些打印日志:
/**
* @author zhangjian
*/
public class MyTextView extends AppCompatTextView {
private static final String TAG = "MyTextView";
public MyTextView(Context context) {
super(context);
}
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
//主要关注事件分发的流程, 就简单以ACTION_DOWN事件为例了
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.v(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.v(TAG, "onTouchEvent ACTION_DOWN");
break;
default:
break;
}
return super.onTouchEvent(ev);
}
}
同时,我们定义一个MainActivity用来呈现MyTextView,并监听MyTextView的点击和触摸事件:
/**
* @author zhangjian
*/
public class MainActivity extends AppCompatActivity
implements View.OnClickListener, View.OnTouchListener {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = findViewById(R.id.my_text_view);
textView.setOnClickListener(this);
textView.setOnTouchListener(this);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.v(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.v(TAG, "onTouchEvent ACTION_DOWN");
break;
default:
break;
}
return super.onTouchEvent(ev);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.my_text_view:
Log.v(TAG, "MyTextView click");
break;
default:
break;
}
}
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (view.getId()) {
case R.id.my_text_view:
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.v(TAG, "TextView onTouch ACTION_DOWN");
break;
default:
break;
}
break;
default:
break;
}
return false;
}
}
运行上面的代码并点击MyTextView后,将打印如下log:
12-04 14:45:01.279 29626-29626/work.test V/MainActivity: dispatchTouchEvent ACTION_DOWN
12-04 14:45:01.279 29626-29626/work.test V/MyTextView: dispatchTouchEvent ACTION_DOWN
12-04 14:45:01.279 29626-29626/work.test V/MainActivity: TextView onTouch ACTION_DOWN
12-04 14:45:01.279 29626-29626/work.test V/MyTextView: onTouchEvent ACTION_DOWN
12-04 14:45:01.409 29626-29626/work.test V/MainActivity: MyTextView click
从上面的代码和日志可以看出,dispatchTouchEvent和onTouchEvent这两个函数的返回值可能存在以下三种情况:
返回true、返回false和返回父类的同名方法。
不同的返回值将导致事件传递流程相差甚远,通过不断修改这些方法的返回值,
我们大概可以得到类似如下的事件处理流程图:
从上面的流程图可以得出如下结论:
1、触摸事件的传递流程是从dispatchTouchEvent开始的,如果不进行人为干预(即返回父类的同名函数),
那么事件将按照视图的嵌套层次,从外层视图逐步向内层传递,到达最内层的View时,就由它的onTouchEvent处理。
该方法如果能够消费该事件,则返回true;如果处理不了,则返回false。
这时事件会重新向外层传递,并由外层View的onTouchEvent方法处理,并依此类推。
2、如果事件在向内层传递的过程中,若某个视图的dispatchTouchEvent返回true,
则会导致事件提前被消费掉,内层View将不会收到这个事件;
若某个视图的dispatchTouchEvent返回false,事件也不会继续分发,
而被交给其父视图的onTouchEvent处理。
3、对于View控件而言,消费事件的逻辑顺序从前到后依次为:
onTouch、onTouchEvent、onClick。
若优先级靠前的接口返回true,则表示事件已经被消费掉了,
不会再调用后续接口。
2.2 ViewGroup的事件传递机制
在Android中,ViewGroup主要作为View控件的容器。
前文已经提到过,与View控件相比,ViewGroup多了onInterceptTouchEvent函数。
在这一部分,我们同样自定义一个ViewGroup并增加打印信息,看看触摸事件传递的流程:
/**
* @author zhangjian
*/
public class MyRelativeLayout extends RelativeLayout {
private static final String TAG = "MyRelativeLayout";
public MyRelativeLayout(Context context) {
super(context);
}
public MyRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.v(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.v(TAG, "onInterceptTouchEvent ACTION_DOWN");
break;
default:
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.v(TAG, "onTouchEvent ACTION_DOWN");
break;
default:
break;
}
return super.onTouchEvent(ev);
}
}
同时,更新对应的layout文件:
<?xml version="1.0" encoding="utf-8"?>
<work.test.MyRelativeLayout 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"
tools:context="work.test.MainActivity">
<work.test.MyTextView
android:id="@+id/my_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/test" />
</work.test.MyRelativeLayout>
MainActivity和MyTextView的事件传递函数均返回super方法的情况下,
点击MyTextView的打印log如下:
12-05 10:26:17.584 12021-12021/work.test V/MainActivity: dispatchTouchEvent ACTION_DOWN
12-05 10:26:17.584 12021-12021/work.test V/MyRelativeLayout: dispatchTouchEvent ACTION_DOWN
12-05 10:26:17.584 12021-12021/work.test V/MyRelativeLayout: onInterceptTouchEvent ACTION_DOWN
12-05 10:26:17.584 12021-12021/work.test V/MyTextView: dispatchTouchEvent ACTION_DOWN
12-05 10:26:17.584 12021-12021/work.test V/MainActivity: TextView onTouch ACTION_DOWN
12-05 10:26:17.584 12021-12021/work.test V/MyTextView: onTouchEvent ACTION_DOWN
12-05 10:26:17.754 12021-12021/work.test V/MainActivity: MyTextView click
从log来看,事件分发的流程几乎没变,
依然从外层视图逐步向内层传递,然后调用内层视图的onTouchEvent处理。
不过,ViewGroup视图调用dispatchTouchEvent后,
会先调用onInterceptTouchEvent函数。
与前文类似,我们可以修改这些函数的返回值,得到下面的流程图:
从上面的流程图可以得出如下结论:
1、不考虑事件拦截时,ViewGroup传递事件的逻辑与View完全一样。
2、 ViewGroup通过onInterceptTouchEvent方法拦截事件时,
返回super方法或false时均表示拦截失败,事件将继续被传递给子视图;
返回true时,表示拦截成功,此时事件交给ViewGroup的onTouchEvent处理。
3、 ViewGroup的onTouchEvent处理事件时,
如果返回false或super方法(此处是RelativeLayout),那么表示事件未被消费掉,
需要继续将事件递交给父视图消费;
如果返回true,则表示ViewGroup消费掉了事件。
三、总结
至此,我们通过demo大致了解了View和ViewGroup的事件传递流程。
后续我们再看看源码中到底是如何实现的。