在 Android 的诸多点击事件中,似乎存在很多问题,为什么图片轮播器里的图片使用ImageView无法监听?为什么按钮和布局同时监听会相互干扰?类似一系列的问题本质上就是 Android 中事件分发机制的问题,只有理解事件分发机制,我们才能对事件响应作出正确的判断。
概述
我们通过一个例子来看看事件分发机制的过程,我们有这样一个简单的界面,一个 Activity 中有一个全屏的MyLayout(继承自Linearlayout),在MyLayout中间有一个按钮 MyButton (继承自 Button),上面写着CLICK ME
代码如下:
<?xml version="1.0" encoding="utf-8"?>
<com.example.hkxlegend.shijianfenfa.app.MyLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<com.example.hkxlegend.shijianfenfa.app.MyButton
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="click me" />
</com.example.hkxlegend.shijianfenfa.app.MyLayout>
然后我们看一下Activity、自定义ViewGroup和MyButton的代码
主Activity的代码:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "tag";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.v(TAG, "activity-touchEvent" + event.getAction());
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.v(TAG, "activity-dispatchTouchEvent" + ev.getAction());
return super.dispatchTouchEvent(ev);
}
}
MyLayout的代码:
/**
* @since 2016
*/
public class MyLayout extends LinearLayout {
private static final String TAG = "tag";
public MyLayout(Context context) {
super(context);
}
public MyLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.v(TAG,"layout-touchEvent"+event.getAction());
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.v(TAG,"layout-dispatchTouchEvent"+ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.v(TAG,"layout-interceptTouchEvent"+ev.getAction());
return super.onInterceptTouchEvent(ev);
}
}
MyButton的代码:
/**
* @since 2016
*/
public class MyButton extends Button {
private static final String TAG = "tag";
public MyButton(Context context) {
super(context);
}
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.v(TAG, "button-dispatchTouchEvent" + event.getAction());
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.v(TAG, "button=touchEvent" + event.getAction());
return super.onTouchEvent(event);
}
}
然后我们点击一下界面上的按钮,通过Log日志,我们可以看到如下输出
在Android中,Touch事件都是从ACTION_DOWN开始的,一次完整的触摸事件中,Down和Up都只有一个,Move有若干个。触摸事件的传递过程主要涉及三个Touch事件:dispatchTouchEvent、onInterceptTouchEvent 和 onTouchEvent。从上面的Log日志我们可以看出来,Event0代表着Down事件,Event1代表着Up事件,Event2代表着Move事件。
dispatchTouchEvent:这个方法用来分发TouchEvent(Activity/ViewGroup/View)
onInterceptTouchEvent:这个方法用来拦截TouchEvent,默认返回false,返回true表示拦截(ViewGroup)
onTouchEvent:这个方法用来处理TouchEvent(Activity/ViewGroup/View)
下面我们就从这三个事件开始说起
Touch 事件
Android中默认情况下事件传递是由最终的view的接收到,传递过程是从父布局到子布局,也就是从Activity到ViewGroup到View的过程,默认情况,ViewGroup起到的是透传作用。总的事件流程我们可以根据这个图看出来
dispatchTouchEvent事件
dispatchTouchEvent 会将一个Touch事件进行分发,事件分发时从根节点的ViewGroup分发开始,ViewGroup遍历它包含着的子View,调用每个View的dispatchTouchEvent方法,而当子View为ViewGroup时,又会通过调用ViewGroup的dispatchTouchEvent方法继续调用其内部的View的dispatchTouchEvent方法来向下分发
ViewGroup的dispatchTouchEvent是真正在执行“分发”工作,而View的dispatchTouchEvent方法,并不执行分发工作,根据源码我们可以看到
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
在View中,首先有一个判断,这三个条件分别是:是否给控件注册了touch事件 & 判断当前点击的控件是否是enable的(button默认是enable) & 回调控件注册touch事件时的onTouch方法,也就是说如果我们在onTouch方法里返回true,就会让这三个条件全部成立;
满足以上三个条件,整个方法直接返回true。如果我们在onTouch方法里返回false,就会再去执行onTouchEvent(event)方法。
一般情况下,我们不该在普通View内重写dispatchTouchEvent方法,因为它并不执行分发逻辑。当Touch事件到达View时,我们该做的就是是否在onTouchEvent事件中处理它。
关注返回值:
- 默认:默认继续传递事件
- true:事件会分发给当前 View 并由 dispatchTouchEvent 方法进行消费,同时事件会停止向下传递
- false:会将事件返回给父 View 的 onTouchEvent 进行消费
如果当前 View 获取的事件直接来自 Activity,则会将事件返回给 Activity 的 onTouchEvent 进行消费
如果当前 View 获取的事件来自外层父控件,则会将事件返回给父 View 的 onTouchEvent 进行消费
在项目开始阶段,每个返回值都是默认super形式的,这样的形式会形成一个完整的事件传递(如上图),现在我们通过改动返回值测试一下其他情况,测试中全部用Event0事件举例
1. 将Activity中dispatchTouchEvent的值改为true和false
- 当返回值为true时
V/tag: activity-dispatchTouchEvent0
返回为true时候,事件分发停止,由本Activity的dispatchTouchEvent方法处理事件
- 当返回值为false时
V/tag: activity-dispatchTouchEvent0
交由父View的onTouchEvent事件,由于没有父View,没有后续执行步骤
2. 将MyLayout中dispatchTouchEvent的值改为true和false
- 当返回值为true时
V/tag: activity-dispatchTouchEvent0
V/tag: layout-dispatchTouchEvent0
返回为true时候,事件分发停止,由本MyLayout的dispatchTouchEvent方法处理事件
- 当返回值为false时
V/tag: activity-dispatchTouchEvent0
V/tag: layout-dispatchTouchEvent0
V/tag: activity-touchEvent0
交由父View即Activity的onTouchEvent事件执行
3. 将MyButton中dispatchTouchEvent的值改为true和false
当返回值为true时
V/tag: activity-dispatchTouchEvent0
V/tag: layout-dispatchTouchEvent0
V/tag: layout-interceptTouchEvent0
V/tag: button-dispatchTouchEvent0
事件被MyButton的dispatchTouchEvent本身消费
当返回值为false时
V/tag: activity-dispatchTouchEvent0
V/tag: layout-dispatchTouchEvent0
V/tag: layout-interceptTouchEvent0
V/tag: button-dispatchTouchEvent0
V/tag: layout-touchEvent0
分发给父View的onTouchEvent方法进行了消费
通过以上的测试,我们就可以知道dispatchTouchEvent的事件分发是什么形式,总结起来可以归类为一张图,图上标注的都是dispatchTouchEvent事件的返回值
onInterceptTouchEvent事件
onInterceptTouchEvent这个方法的返回值是最简单的,表示是否拦截事件,这个只有在ViewGroup组件中存在,所以ViewGroup可以通过onInterceptTouchEvent方法对事件传递进行拦截,onInterceptTouchEvent方法返回true代表不允许事件继续向子View传递,事件交由本ViewGroup的onTouchEvent事件处理;返回false代表不对事件进行拦截,默认返回false。
onTouchEvent事件
onTouchEvent 的默认返回值为true ,表示对事件进行了处理
如果某个控件的onTouchEvent返回值为true,ACTION_DOWN以及后续的n个ACTION_MOVE与1个ACTION_UP都会逐层传递到这个控件的onTouchEvent进行处理。我们在MyBotton的onTouchEvent方法中返回true(默认值),可以看到Log日志:
V/tag: activity-dispatchTouchEvent0
V/tag: layout-dispatchTouchEvent0
V/tag: layout-interceptTouchEvent0
V/tag: button-dispatchTouchEvent0
V/tag: button-touchEvent0
V/tag: activity-dispatchTouchEvent2
V/tag: layout-dispatchTouchEvent2
V/tag: layout-interceptTouchEvent2
V/tag: button-dispatchTouchEvent2
V/tag: button-touchEvent2
V/tag: activity-dispatchTouchEvent1
V/tag: layout-dispatchTouchEvent1
V/tag: layout-interceptTouchEvent1
V/tag: button-dispatchTouchEvent1
V/tag: button-touchEvent1
如果返回值是false,则会将ACTION_DOWN传递给其父ViewGroup的onTouchEvent进行处理,直到由哪一层ViewGroup消费了ACTION_DOWN事件为止,后续的n个ACTION_MOVE与1个ACTION_UP都会逐层传递到处理这个事件的那一层View或ViewGroup,我们现在将MyButton的onTouchEvent返回为false,MyLayout的onTouchEvent返回值为true看一下Log日志:
V/tag: activity-dispatchTouchEvent0
V/tag: layout-dispatchTouchEvent0
V/tag: layout-interceptTouchEvent0
V/tag: button-dispatchTouchEvent0
V/tag: button-touchEvent0
V/tag: layout-touchEvent0
V/tag: activity-dispatchTouchEvent2
V/tag: layout-dispatchTouchEvent2
V/tag: layout-touchEvent2
V/tag: activity-dispatchTouchEvent2
V/tag: layout-dispatchTouchEvent2
V/tag: layout-touchEvent2
V/tag: activity-dispatchTouchEvent2
V/tag: layout-dispatchTouchEvent2
V/tag: layout-touchEvent2
V/tag: activity-dispatchTouchEvent1
V/tag: layout-dispatchTouchEvent1
V/tag: layout-touchEvent1
Touch 事件说完后,我们来看一下onClick事件,看一下onClick 和 Touch 有没有什么关系
onClick 事件
还是这一段代码,View的dispatchTouchEvent代码
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
可以看到,第三个判断比较重要,如果对一个控件添加onClick事件,当控件中onTouch方法返回是true的时候,onClick事件没有响应。 这是为什么呢?我们来看一下第三个判断
第三个判断回调控件注册touch事件时的onTouch方法,也就是说如果我们在onTouch方法里返回true,就会让这三个条件全部成立,无法执行下面的onTouchEvent方法,我们要知道onClick的调用是在onTouchEvent(event)方法中的performClick()方法,所以如果此时onTouch返回了true,onClick()方法就无法执行。
我们还要知道,performClick()这个方法是在onTouchEvent()方法的Up事件中的,所以执行流程也是先onTouch()然后才会执行onClick()事件。
通过这些,我们就大概了解了Android中事假的分发机制