正确理解Android中的事件分发和拦截机制,对于在多个ViewGroup和View嵌套以及自定义控件的时候能够正常处理用户行为至关重要。
如果要把源码一行一行看,研究事件处理机制就要到天荒地老了。这是为什么这篇博客叫轻量级源码分析,简单易懂。屏蔽掉了冗长的无需太过关心的源码,把握一下几行重点,其实事件处理就可以有一定的了解了。这篇博文就是根据源码一步一步走了一下事件处理的流程,分模块来看应该思路应该会比较清晰一点~
事件分发和拦截101
开始之前首先明确一下,在源码当中,理解事件分发,关注一下dispatchTouchEvent()方法;理解事件拦截,再多关注一个onInterceptTouchEvent()方法即可~
还有噢,dispatchTouchEvent()方法返回true表示响应全部事件,而返回false则表示只响应第一个事件,忽略后续的事件~onInterceptTouchEvent()方法返回true表示拦截事件,子View不会接收到该事件;返回false表示不拦截事件,向子View传递该事件,让子View尝试处理~
哟西~
Let`s hacking…
一、View的事件处理机制
View中的事件分发对于本身可以被点击和不可被点击的View有些许不同~
1. View的事件分发之不可被点击的View
先看一个小示例哟~这一小段代码中有两个控件,一个ImageView,一个Button,这两个控件最终都是继承View的,就用这两个控件来阐述一下View的事件处理机制
这一小段代码,还是粘上来吧~
imageView.setOnTouchListener(new OnTouchListener() {
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent event) {
System.out.println("imageView eventAction = "+event.getAction());
return false;
}
先说一说本身不可被点击的ImageView~这里给imageView添加一个onTouchListener,我们看看在return false的情况下,单击一次,打印了几次输出信息~
运行之后发现输出了imageView eventAction = 0。由于一次单击包括按下和抬起两个操作(0表示按下的操作,1表示抬起的操作),因此从这个现象可以看出,抬起的操作在这个情况下没有被监测到。想知道为什么,就来看看源码咯~
ImageView是继承View的(继承了dispatchTouchEvent()方法),所以应该到View的源码中查看一下dispatchTouchEvent()(我们要关注的就是这个方法嘛)方法,看看View是怎么进行事件分发的(就知道ImageView是怎么处理事件的了)。过滤了不必要的代码之后,应该关注的代码如下:
dispatchTouchEvent()方法
public boolean dispatchTouchEvent(MotionEvent event) {
....
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
View的dispatchTouchEvent()方法中,这个if语句的意思是说,如果mOnTouchListener 引用不为空,并且当前要接收事件的控件(此处就是这个IamgeView)处于可用状态(只要不设置setEnable = false,这个条件是始终成立的,所以后面就不说这个条件了),并且mOnTouchListener这个引用的onTouch方法返回了true,那么整个dispatchTouchEvent()就返回true,否则返回onTouchEvent(event)方法执行的结果。
那么ImageView被点击之后,是否执行了if语句呢?下面来判断一下if中的条件该ImageView是否满足~
1. `mOnTouchListener `引用不为空
View中有一个
public void setOnTouchListener(OnTouchListener l) {
mOnTouchListener = l;
}
的方法。在示例代码中,ImageView是调用了这个方法的。因此,匿名内部类OnTouchListener()被作为参数l赋值给了mOnTouchListener 。那么mOnTouchListener 引用不为空条件成立;
2. 当前控件可用
此条件始终成立;
3.mOnTouchListener 这个引用的onTouch方法返回了true
mOnTouchListener指向的是当前的imageView的OnTouchListener()监听器。看ImageView的监听器,我们在监听器的onTouch方法中,返回了false,因此该条件不成立;
所以呢,此时的ImageView是不会执行dispatchTouchEvent()方法中的if语句块的,而会执行其所继承的onTouchEvent(event)方法,并返回相应的布尔结果。
到这里,还无法得知为什么ImageView上的抬起事件没有被监测到。那就来看看这个onTouchEvent(event)方法到底干了什么。这个方法代码超长的,哈哈,轻量级源码如下:
onTouchEvent()方法
public boolean onTouchEvent(MotionEvent event) {
....
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
...
}
return true;
}
return false;
}
这里又有一个if语句块,这个if说,如果你这个接收事件的控件可以被点击,或者可以被长点击,那么就执行if中的逻辑,然后返回true,否则整个方法返回false。那就来看看我们的ImageView是否满足条件咯~
挂了,ImageView本身是没有被点击的能力的,所以这个if语句不会被执行;也就是说,此时的onTouchEvent()方法返回了false
然后再回到dispatchTouchEvent()方法,既然此时的ImageView不能执行if中的代码,而onTouchEvent()方法又返回了false,所以整个dispatchTouchEvent()就返回了false。根据文章开头所说dispatchTouchEvent()返回true和false的意义得知,这个单击事件整体只有按下会被处理,所以就只会打印一次输出信息咯~
如果想让ImageView同时响应按下和抬起的事件,该怎么办呢?有两个方式:
1.在OnTouchListener()的onTouch()方法中返回true;那么ImageView将满足dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent event) {
....
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
方法的if语句中的所有条件;执行if语句,最终dispatchTouchEvent()返回true,单击事件整体被响应,打印两次输出信息;
2.ImageView的onTouch()方法仍然返回false,同时为ImageView再添加一个setOnClickListener();View中的setOnClickListener()方法的源码做了这么一件事情
public void setOnClickListener(OnClickListener l) {
//如果当前控件没有点击事件,设置一个点击事件
if (!isClickable()) {
setClickable(true);
}
...
}
如果当前控件不可被点击,则设置其为可点击;这样一来,ImageView就满足了onTouchEvent()方法
public boolean onTouchEvent(MotionEvent event) {
....
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
...
}
return true;
}
return false;
}
中if语句可点击的条件;进入了if语句之后,不管switch里面执行了多少代码,在switch结束之后,使用返回了true;所以,单击事件被整体响应,打印两次输出信息~
2. View的事件分发之可以被点击的View
接下来就说说Button这个东西~
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
System.out.println("Button eventAction = "+event.getAction());
return false;
}
});
在同样的环境下,同样地为Button添加了一个触摸事件监听器,在onTouch()方法中返回了false;运行之后,输出了Button eventAction = 0和Button eventAction = 1。这里同样返回false,为什么就能打印两次信息,为什么Button的整个单击事件可以被完全监测呢?
原因就在于Button本身是可以被点击的;在其他条件都相同的情况下,Button本身可以被点击的属性,可以让其执行onTouchEvent()方法中的if语句
onTouchEvent()方法
public boolean onTouchEvent(MotionEvent event) {
....
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
...
}
return true;
}
return false;
}
从而返回true;进而dispatchTouchEvent()方法返回true,单击事件的按下和抬起都会被监测~
3. onClick()、onTouch()和onTouchEvent的调用顺序
上面一点很好理解。那进一步,如果也给Button再来一个setOnClickListener()呢?就像这样
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
System.out.println("button onclick");
}
});
在onTouch()方法返回false的情况下,还会打印"button onclick"的信息吗?
运行之后发现,"button onclick"的信息在Button eventAction = 0和Button eventAction = 1之后输出了。
但是为什么是在onTouch()方法之后呢?
那就继续看源码咯~
这里直接看onTouchEvent()方法
onTouchEvent()方法
public boolean onTouchEvent(MotionEvent event) {
....
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
...
performClick();
...
}
return true;
}
return false;
}
由于Button的onTouch()方法返回了false,导致dispatchTouchEvent()方法的if语句不会执行;而会执行if下面的onTouchEvent()方法(对于这个逻辑请参看ImageView的讲解)。到了onTouchEvent()方法中,又由于Button本身可以被点击,显然会进入if语句执行;注意到在这个onTouchEvent()的switch语句处理抬起操作的代码中,有一个performClick()方法;顺势找到performClick()方法的源码~
performClick()
public boolean performClick() {
...
if (mOnClickListener != null) {
....
mOnClickListener.onClick(this);
...
}
...
}
performClick()的源码主要部分,在mOnClickListener不为空的情况下,调用了mOnClickListener身上的onClick()方法;来看看这个mOnClickListener在什么地方赋值,指的是什么~还记得View中的setOnClickListener()方法吗~
setOnClickListener()
public void setOnClickListener(OnClickListener l) {
...
mOnClickListener = l;
}
原来在为Button添加click监听事件的时候,new出来的匿名内部类OnClickListener()传递给了setOnClickListener(),并赋值给了mOnClickListener;因此,mOnClickListener指的就是OnClickListener(),其在performClick()方法中调用的onClick()方法,就是Button处理点击事件所执行的onClick()方法。
这么一来,输出顺序之谜就明了了。
首先,在Button被点击的时候,执行了其监听器中的onTouch()方法;接着,onTouch()方法返回了false,就执行了onTouchEvent()方法;再接着,由于Button满足了if条件,进入switch语句,调用了performClick()方法,最终执行了onClick()方法;调用顺序即:
onTouch() --> onTouchEvent() --> onClick()
至此,View的事件分发就很清楚了~以后触摸事件和点击事件,在处理的时候,就不会蒙圈咯~
在了解了View的事件分发机制之后,再来看看ViewGroup对于事件是怎么进行处理的。
二、ViewGroup的事件处理机制
大家都知道,说到ViewGroup的事件处理机制,就有那么一张图说ViewGroup的事件是一级一级往下传的,传到最后消费了就消费了,不消费再传回来云云~但是底层的实现不得而知的~真相弄明白,还是扎到源码看看~
同样以一个小示例来阐释一下ViewGroup的事件处理机制~
先来一个自定义Layout继承LinearLayout~
MyLayout
public class MyLayout extends LinearLayout {
public MyLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//此时该方法返回false
return false;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
}
}
在MyLayout中放置一个按钮
activity_main.xml
<com.example.viewgrouponclickdmeo.MyLayout
android:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />
</com.example.viewgrouponclickdmeo.MyLayout>
在MainActivity中进行测试
MainActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
layout = (MyLayout) findViewById(R.id.layout);
button = (Button) findViewById(R.id.button);
layout.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.i(tag, "click layout ...........................");
}
});
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.i(tag, "click button ...........................");
}
});
}
为MyLayout和Button添加了点击事件,输出相应内容~
来小小分析一下~
首先,这个小示例自定义了一个MyLayout,继承了LinearLayout;而LinearLayout继承自ViewGroup,因此这个MyLayout最终是继承ViewGroup的;
其次,MyLayout重写了ViewGroup的onInterceptTouchEvent(MotionEvent ev)方法和dispatchTouchEvent(MotionEvent ev)方法,重新定义了事件的处理规则;这里在onInterceptTouchEvent(MotionEvent ev)中返回了false;
最后,在MainActivity运行之后,发现当MyLayout的onInterceptTouchEvent(MotionEvent ev)方法返回false的时候,打印了"click button ………………………";而当返回true的时候,打印了"click layout ………………………";
为什么onInterceptTouchEvent(MotionEvent ev)的返回值决定了输出内容呢?来看看onInterceptTouchEvent(MotionEvent ev)方法在源码中的作用~
onInterceptTouchEvent(MotionEvent ev)在dispatchTouchEvent(MotionEvent ev)方法中被调用~
上源码~
ViewGroup中的dispatchTouchEvent(MotionEvent ev)方法
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
//让mMotionTarget始终为null
if (mMotionTarget != null) {
mMotionTarget = null;
}
//onInterceptTouchEvent返回false,说明向下传递
//onInterceptTouchEvent返回true,说明拦截
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
//1. 找到当前控件的子控件
//2. 判断当前事件所触发的位置(手指的位置)所在的坐标值(x,y)在哪个子控件的内部
//3. 判断当前子控件是ViewGroup的子类对象,还是View的子类对象
//3.1 如果是ViewGroup的子类,那么再重复以上逻辑,继续寻找子控件
//3.2 如果是View的子类,就尝试去处理该事件
//3.2.1 如果View的子类的dispatchTouchEvent()方法返回true,事件被响应,方法完结
//3.2.2 如果View的子类的dispatchTouchEvent()方法返回false,继续走下面的逻辑
}
}
...
target = mMotionTarget
//target一定是null
if (target == null) {
...
//调用当前viewgroup的view处理事件的方法
return super.dispatchTouchEvent(ev);
}
...
}
1. 解决disallowIntercept~
先找到if (disallowIntercept || !onInterceptTouchEvent(ev)) 这一行~通过观察这一行代码可以发现,onInterceptTouchEvent(ev)方法是否起作用,取决于disallowIntercept的值;只有当disallowIntercept的值为false的时候,onInterceptTouchEvent(ev)方法的返回值才能生效,否则无需判断onInterceptTouchEvent(ev)方法,if语句始终会执行。那我们先解决掉这个disallowIntercept~
disallowIntercept的值代表着是否允许拦截触摸事件~false表示拦截,true表示不拦截~
dispatchTouchEvent(MotionEvent ev)方法的源码中有这么一句话
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
这句话为disallowIntercept变量赋了初始值;这个初始值是多少呢?
先来看看这个mGroup的值是多少~
protected int mGroupFlags;
源码中只声明了mGroupFlags,因此mGroupFlags的默认值为0;
再来看看FLAG_DISALLOW_INTERCEPT常量是多少~
protected static final int FLAG_DISALLOW_INTERCEPT = 0x80000;
原来FLAG_DISALLOW_INTERCEPT被声明为0x80000。
所以就可以得到
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
中mGroupFlags & FLAG_DISALLOW_INTERCEPT的值是0(0 & 0x80000 = 0,0 & 上任何数都是0);意味着disallowIntercept = false(0 != 0),也就意味着onInterceptTouchEvent(ev)方法在disallowIntercept是默认值的情况下是起作用的~
2. 关于disallowIntercept
ViewGroup的源码中有这么一个方法
requestDisallowInterceptTouchEvent()方法
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if(disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0) {
//we're already in this state, assume our ancesters are too
return;
}
if(disallowIntercept) {
//相当于mGroupFlags = FLAG_DISALLOW_INTERCEPT
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
//相当于mGroupFlags = 0
mGroupFlagfs &= ~FLAG_DISALLOW_INTERCEPT;
}
...
}
我们可以通过这个方法来设置disallowIntercept的默认值;如果大家想永远不拦截触摸事件,让事件往子View传递的话,直接复写这个方法,传入一个true即可~
3. 判断onInterceptTouchEvent(MotionEvent ev)方法返回值
3.1 onInterceptTouchEvent(ev)方法返回false
继续看源码,可以看到,if (disallowIntercept || !onInterceptTouchEvent(ev)) 这一行对onInterceptTouchEvent(ev)做了取非的处理;示例代码中MyLayout复写的onInterceptTouchEvent(ev)方法,返回了false;那么这里取非,也就是!onInterceptTouchEvent(ev) = true;所以会执行if语句中的代码;将if语句中的代码逻辑归纳为以下几点(如注释):
//1. 找到当前控件的子控件
//2. 判断当前事件所触发的位置(手指的位置)所在的坐标值(x,y)在哪个子控件的内部
//3. 判断当前子控件是ViewGroup的子类对象,还是View的子类对象
//3.1 如果是ViewGroup的子类,那么再重复以上逻辑,继续寻找子控件
//3.2 如果是View的子类,就尝试去处理该事件
//3.2.1 如果View的子类的dispatchTouchEvent()方法返回true,事件被响应,方法完结
//3.2.2 如果View的子类的dispatchTouchEvent()方法返回false,继续走下面的逻辑
看到这里,相信大家一定明白了为什么说ViewGroup处理事件会一级一级往下分发,就是因为上述的第一步;
而为什么有第二步,我们用一个图来解释~
如图,当点击事件发生的时候,系统会记录点击发生地点的x,y坐标值;例如,如果是点击事件1,那么最外层的ViewGroup往下分发事件的时候,找到了两个子View,到底哪个子View该处理这个事件呢?就根据点击事件1的坐标来判断。当前坐标在上面这个子View中,所以应该有它来负责处理这个事件;如果是点击事件2,ViewGroup分发事件后找不到对应的子View可以处理这个事件,就会返回ViewGroup尝试自己处理。
第二步完结,就会判断该子控件是ViewGroup的子类还是View的子类,来决定是否继续分发事件还是就地处理~
如果继续分发,就再从第一步逻辑开始执行;如果就地处理,返回true,代表该事件被该子View处理完毕;返回false,表示该子View没有处理该事件;此时该事件会沿着原路向其父控件传递,直至有控件处理;如果到顶部的父控件都不处理该事件,则该事件交由View的dispatchTouchEvent()方法做默认处理~
回到我们的示例~根据上面的分析,当我们点击Button按钮的时候,事件会从MyLayout向下分发(由于onInterceptTouchEvent(ev)方法返回的是false),分发到Button控件的时候,由于Button已经是最后一级子View,系统发现点击的地方的x,y坐标在Button按钮之内,又因为Button是View的子类,所以判定应该由Button来处理该事件;此时,根据上面讲述的View的事件处理机制,在Button没有复写dispatchTouchEvent()方法的情况下,其dispatchTouchEvent()始终返回true;因此,根据3.2.1步所述, 如果View的子类的dispatchTouchEvent()方法返回true,事件被响应,方法完结,打印出"click button ………………………";
3.2 onInterceptTouchEvent(ev)方法返回true
反之,如果MyLayout复写的onInterceptTouchEvent(ev)方法返回了true,将会是什么情况呢?
再找到if (disallowIntercept || !onInterceptTouchEvent(ev)) 这一行~如果onInterceptTouchEvent(ev)方法返回了true,那么if语句将不会执行~这时就要跳出if语句,继续看下面的代码~
关键代码
...
target = mMotionTarget
//target一定是null
if (target == null) {
...
//调用当前viewgroup的view处理事件的方法
return super.dispatchTouchEvent(ev);
}
...
这段代码的注释中写到,//target一定是null
,原因在这~
在ViewGroup的dispatchTouchEvent(ev)方法中有这么一段逻辑(见上述ViewGroup源码)
//让mMotionTarget始终为null
if (mMotionTarget != null) {
mMotionTarget = null;
}
如果mMotionTarget不等于空,就将mMotionTarget置为空。由于这个逻辑是在判断是否拦截(if (disallowIntercept || !onInterceptTouchEvent(ev)))事件的逻辑之前。因此,这个mMotionTarget是始终为null的。所以
...
target = mMotionTarget
//target一定是null
if (target == null) {
...
return super.dispatchTouchEvent(ev);
}
...
这段代码的if语句一定会执行。if语句中,调用了当前控件的父控件的dispatchTouchEvent(ev)方法来处理该事件;当前控件的父控件,就是View,所以这里又可以参看View的事件处理机制的逻辑,来处理这次触摸或者点击事件了。要注意的是,这里所说的父控件,不是布局中的父控件,而是Java代码中所继承的父类;无论是继承ViewGroup还是View的子控件,其父控件都是View。因此,这时就相当于MyLayout拦截了该点击事件,调用了其最终父类View中的dispatchTouchEvent(ev)方法,处理了点击事件,输出了"click layout ………………………"
三、总结
从View的事件处理机制中可以得出
结论1:如果当前控件继承自View,想要事件被完全监听,必须保证其dispatchTouchEvent()方法返回true;因此,可以选择在触摸事件监听器中返回true或者使控件具备可被点击的能力即可;
从Button同时添加触摸事件和点击事件的示例可以得出
结论2:无论当前是什么控件,onClick()事件都是在onTouchEvent()方法中被响应的;响应onClick()事件时的事件处理机制方法的调用顺序为:onTouch()(设置了onTouchListener,并在onTouch()方法中返回false) --> onTouchEvent()(dispatchTouchEvent()方法不走if的逻辑时) --> onClick()(peformClick()方法);
从ViewGroup的事件处理机制中可以得出
结论3:其实onInterceptTouchEvent(MotionEvent ev)决定了ViewGroup中的事件是否往其子View中传递;在不复写dispatchTouchEvent(MotionEvent ev)方法的前提下(不改变父类处理逻辑),onInterceptTouchEvent(MotionEvent ev)返回true,事件被拦截;返回false,事件往子View传递;
结论4:没有任何控件响应的事件,会调用View的dispatchTouchEvent()方法做默认处理~
理解事件分发和拦截的逻辑比较跳跃,写这篇博文也很慎重,检查了很多次,希望自己在复习知识的同时,能简化一些流程,帮助大家一起啃一啃这块硬骨头~如果不慎还是出现错误,请大家指出,我们一起讨论,一起进步~~