Android事件分发机制
假设有以下布局:
Activity里嵌套了两个ViewGroup
,最下层的绿色ViewGroup1
和中间层的红色ViewGoup2
,最上层的是一个TextView
控件。
相关函数:
public boolean dispatchTouchEvent(MotionEvent event)
public boolean onInterceptTouchEvent(MotionEvent event)
public boolean onTouchEvent(MotionEvent event)
1.ACTION_DOWN的消息传递流程
图1-1展示了在不拦截消息时,ACTION_DOWN消息的完整传递流程。
可以看到在Activity 、ViewGroup、TextView中都有dispatchTouchEvent
函数和onTouchEvent
函数。而对于ViewGroup而言,则多了一个onInterceptTouchEvent
函数。
1.1 不包含onInterceptTouchEvent函数的ACTION_DOWN消息流程传递
1.1.1 dispatchTouchEvent返回值简介
- 默认的
super.dispatchTouchEvent
- 返回 true
- 返回 false
dispatchTouchEvent函数采用默认处理方式情况下的消息传递流程,这种情况我们成为正常的消息传递流程,如图1-1-1所示:
1.1.2 dispatchTouchEvent函数中返回true拦截消息
结论: 在
dispatchTouchEvent
中返回true拦截消息之后,消息会直接停止传递,后面的子控件都不会接收到这个消息。
图1-1-2-1 展示了在Activity的dispatchTouchEvent中返回true的消息传递流程:
图1-1-2-2 展示了在ViewGroup1的dispatchTouchEvent中返回true的消息传递流程:
图1-1-2-3 展示了在ViewGoup2的dispatchTouchEvent中返回true的消息传递流程:
图1-1-2-4 展示了在View的dispatchTouchEvent中返回true的消息传递流程:
通过这些流程可以看出:无论在哪个控件的dispatchTouchEvent函数中拦截消息,消息都会直接停止传递,后面的子控件都不会接收到这个消息。
1.1.3 dispatchTouchEvent函数中返回false拦截消息
结论:在dispatchTouchEvent函数中返回false拦截消息之后,消息并不会直接停止传递,而是向父控件的onTouchEvent函数回传。
图1-1-3-1展示了在TextView的dispatchTouchEvent
函数中返回false时的消息传递流程:
图1-1-3-2展示了在ViewGoup2的dispatchTouchEvent
函数中返回了false时的消息传递流程:
图1-1-3-3展示了在ViewGroup1的dispatchTouchEvent
函数中返回了false时的消息传递流程:
图1-1-3-4展示了在Activity的dispatchTouchEvent
函数中返回false时的消息传递流程:
1.1.4 在onTouchEvent函数中拦截ACTION_DOWN消息
在onTouchEvent函数中,默认返回的是false,即不拦截;当返回true时,表示拦截。
结论:无论在哪个控件的onTouchEvent函数中拦截ACTION_DOWN消息,即返回true时,消息都会直接停止传递,后面的父控件都不会接收到这个消息。
需要注意的是,onTouchEvent
函数的拦截效果看起来与dispatchTouchEvent
函数返回true时的拦截效果是一致的,但仔细观察会发现,dispatchTouchEvent
函数中的消息是从Activity向TextView传递,而onTouchEvent
函数中的消息是从TextView向Activity回传的。
图1-1-4-1 展示了在TextView的onTouchEvent函数中返回true时的消息传递流程
图1-1-4-2 展示了在ViewGroup的onTouchEvent函数中返回true时的消息传递流程
总结:
dispatchTouchEvent
和onTouchEvent
函数一旦返回true拦截消息,ACTION_DOWN消息就会停止传递,正常流程下的后续节点都不会收到ACTION_DOWN消息了。- 当
dispatchTouchEvent
函数返回false时,首先会拦截诶ACTION_DOWN消息向其子控件中传递,然后将消息向其父控件的onTouchEvent
函数中继续回传。
1.2 onInterceptTouchEvent函数的ACTION_DOWN消息传递流程
1.2.1 onInterceptTouchEvent函数的作用
只有ViewGroup具有onInterceptTouchEvent函数,Activity和View中都没有。
顾名思义,onInterceptTouchEvent就是一个拦截过滤器。每个ViewGroup每次在做消息分发的时候,都会问一问拦截器要不要拦截(也就是问问自己这个事件要不要自己来处理)。如果要自己处理,那么就在onInterceptTouchEvent
函数中返回true,这样就会将事件交给自己的onTouchEvent
函数处理了,如果不拦截就继续往子控件处理。默认其是不拦截的。
1.2.2在ViewGroup的onInterceptTouchEvent函数中拦截ACTION_DOWN消息
图1-2-2-1 展示了在ViewGoup2的onInterceptTouchEvent函数中拦截消息
需要注意的是,在图中没有标注return true时,其他直接流向的箭头全部表示返回false,即在正常的不拦截情况下的消息流向
可以看到,在ViewGroup2的onInterceptTouchEvent函数中拦截了消息以后,函数直接将消息传递到ViewGroup2的onTouchEvent函数中,因为没有任何一个onTouchEvent函数返回true拦截消息,所以消息会一直流向Activity的onTouchEvent函数并停止传递。
图1-2-2-2 展示了在ViewGroup2的onInterceptTouchEvent函数中拦截消息
图1-2-2-3 展示了在ViewGroup2的onInterceptTouchEvent和onTouchEvent函数中拦截消息
总结
总结:
- 若在dispatchTouchEvent和onTouchEvent函数中返回true拦截消息的话,会直接将消息截断,后续节点将不会收到ACTION_DOWN消息。
- 若在onInterceptTouchEvent函数中返回true拦截消息的话,只会改变ACTION_DOWN消息的正常流向,消息会直接流到自己的onTouchEvent函数中,并不会截断消息。
- 若在dispatchTouchEvent函数中返回false拦截消息,同样会改变ACTION_DOWN消息的正常流向,消息会直接流向其父控件的onTouchEvent函数中,同样不会截断消息。
- 一般我们在拦截消息时,都是共同使用onInterceptTouchEvent和onTouchEvent函数的,通过在onInterceptTouchEvent函数中返回true,将ACTION_DOWN消息流向自己的onTouchEvent函数中,然后在该onTouchEvent函数中返回true拦截消息。
2.ACTION_MOVE和ACTION_UP的消息传递流程
在这部分的消息传递流程中,红色表示ACTION_DOWN消息的传递,蓝色表示ACTION_MOVE/ACTION_UP消息的传递。
2.1 在dispatchTouchEvent函数中返回true拦截消息后的消息传递流程
结论:在dispatchTouchEvent函数中返回true拦截消息之后,ACTION_MOVE消息的流向与ACTION_DOEN消息的流向完全相同,消息会直接停止传递,后面的子控件都不会接受到这个消息。
图2-1-1 展示了在TextView的dispatchTouchEvent函数中返回true的消息传递:
图2-1-2 展示了在ViewGroup2的dispatchTouchEvent函数中返回true的消息传递:
图2-1-3 展示了在ViewGroup1的dispatchTouchEvent函数中返回true的消息传递:
图2-1-4 展示了在Activity的dispatchTouchEvent函数中返回true的消息传递:
2.2 传递到onTouchEvent函数后的ACTION_MOVE消息
从ACTION_DOWN消息的传递流程可以看出,只有在dispatchTouchEvent函数中返回true时,消息才会停止传递,而无论是dispatchTouchEvet函数返回false或者通过onInterceptTouchEvent函数拦截消息,消息最终都会流到onTouchEvent函数中。
结论:
- 一旦ACTION_DOWN消息流入到onTouchEvent函数,假使其最终会被控件A的onTouchEvnet函数消费,即在控件A的onTouchEvent函数中返回了true,那么ACTION_MOVE消息在dispatchTouchEvent这条线上只会传递到控件A的dispatchTouchEvent函数中,然后直接传递到控件A的onTouchEvent函数中。
2.2.1 正常情况下的ACTION_MOVE消息传递流程
正常情况也就是任何函数都采用默认的方式执行的情况,也就是在所有函数都不拦截消息的情况下,ACTION_MOVE消息传递流程如图2-2-1所示:
可以看到ACTION_DOWN完整地走完了所有函数,最终流入到Activity的onTouchEvent函数,因此ACTION_MOVE消息的流向就是先流到Activity的dispatchTouchEvent函数中,然后直接流到Activity的onTouchEvent函数中后消失。
对应前面的结论,这里的Activity就是控件A。
2.2.2 当在onTouchEvent函数中拦截ACTION_DOWN消息
图2-2-2展示了在ViewGroup2的onTouchEvent函数中返回true的ACTION_DOWN和ACTION_MOVE的消息传递流程:
可以看到ACTION_MOVE消息也是从Activity流到ViewGroup2的diapatchTouchEvent函数中,最终流入到ViewGroup2的onTouchView函数中。
对应前面的结论,这里的ViewGroup2就是控件A。
图2-2-2-1展示了在TextView的dispatchTouchEvent函数返回false,在ViewGroup1的onTouchEvent函数中返回true拦截消息时,ACTION_MOVE消息的传递流程:
可以看到ACTION_MOVE消息也是从Activity流到ViewGroup1的diapatchTouchEvent函数中,最终流入到ViewGroup1的onTouchView函数中。
对应前面的结论,这里的ViewGroup1就是控件A。
图2-2-2-2展示了在ViewGroup2的onInterceptTouchEvent函数中返回true拦截消息,在ViewGroup1的onTouchEvent函数中返回true消费消息时,ACTION_MOVE消息的传递流程:
可以看到,虽然ACTION_DOWN消息的流向与前面流程不同,但是ACTION_MOVE消息的流向却还是从Activity流到ViewGroup1的diapatchTouchEvent函数中,最终流入到ViewGroup1的onTouchView函数中。
总结
- 在dispatchTouchEvent函数中返回true拦截消息后,ACTION_MOVE消息的流向与ACTION_DOWN消息的流向完全相同,消息会直接停止传递,后面的子控件都不会接受到这个消息。
- 无论ACTION_DOWN消息的流向是怎样的,只要最终流到onTouchEvent函数中就行。假使控件A最终在onTouchEvent函数中返回true消费了ACTION_DOWN消息,那么ACTION_MOVE消息的流向就是先流到控件A的dispatchTouchEvent函数中,最终流到控件A的onTouchEvent函数中,进而消息停止传递。
2.3 在ACTION_MOVE消息到来时拦截消息
前面讲到的都是正常的拦截消息的情况,即在ACTION_DOWN消息到来时就进行拦截。但如果我们把拦截动作推后,在ACTION_MOVE消息到来时再进行拦截,情况会如何?
2.3.1 注意事项
为了保证在ACTION_MOVE消息到来时能够拦截到消息,我们必须保证ACTION_MOVE消息能够经过该控件的disPatchTouchEvent或onInterceptTouchEvent函数。
2.3.2 ACTION_MOVE消息拦截初探
图2-3-2-1展示了在dispatchTouchEvent这条线上不做拦截,仅在TextView的onTouchEvent函数中返回true消费了消息时的消息传递流程:
可以看到这里ACTION_DOWN和ACTION_MOVE消息的传递流程完全相同。
我们在ViewGroup1的onInterceptTouchEvent函数中将代码改为如下这样:
public class ViewGroup1 extends LinearLayout{
...
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch(ev.getAction()){
case ACTION_MOVE:
return true;
}
return super.onInterceptTouchEvent(ev);
}
}
即在消息ACTION_DOWN到来时不拦截,在ACTION_MOVE到来时进行返回true的拦截。这种情况的消息传递流程如图2-3-2-2所示:
- 红线: 这条线表示ACTION_DOWN消息的传递流程,也就是在TextView的OnTouchEvent函数中返回true消费消息时的传递流程。
绿线 :这条线表示ACTION_MOVE消息第一次传递时的流向情况。这里需要知道的是ACTION_MOVE消息会多次传递。这里是第一次的流向情况。
本来消息依然会从Activity的dispatchTouchEvent函数流向子控件,但是在到达ViewGroup1的onInterceptTouchEvent函数时,消息被拦截了。到这里,这次的ACTION_MOVE消息就没有了,变成了ACTION_CANCEL消息继续向子控件传递,一直传递到ACTION_MOVE消息原本要传递到的位置,通知所有被截断的子控件,它们的消息被取消了,后续不会再有消息传递过来,后续的ACTION_MOVE/ACTION_UP也不会再经过被截断的子控件了,即ViewGroup2以及View的dispatachTouchEvent和onTouchEvent函数都不会再接受到消息。
当我们收到ACTION_CANCEL消息时,就表示后续不再获得消息,一般需要像处理ACTION_UP消息一样处理该消息,执行控件归位等操作。
蓝线:这条线表示消息被截断之后的ACTION_MOVE/ACTION_UP消息的流向。可以看到,这时的ACTION_MOVE消息的流向与正常情况下ViewGroup1的onInterceptTouchEvent函数拦截ACTION_DOWN消息时ACTION_MOVE消息的流向是完全相同的。
2.3.3 在dispatchTouchEvent函数中拦截ACTION_MOVE消息
2.3.3.1 在ViewGroup2的dispatchTouchEvent函数中返回true拦截ACTION_MOVE消息:
public class ViewGroup2 extends LinearLayout{
...
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch(ev.getAction()){
case ACTION_MOVE:
return true;
}
return super.dispatchTouchEvent(ev);
}
}
这种情况下的消息传递流程如图2-3-3-1所示:
- 红线: 这条线表示ACTION_DOWN消息的传递流程,也就是在TextView的OnTouchEvent函数中返回true消费消息时的传递流程。
绿线 :这条线表示ACTION_MOVE消息的传递流程。需要注意的是这里的ACTION_MOVE消息传递流程中没有第几次的区别,每次消息都直接在ViewGroup2的dispatchTouchEvent函数中被截断。这里没有发出ACTION_CANCEL消息,而是消息直接被截断了,而且也不会向onTouchEvent函数传递。
蓝线:这条线表示消息ACTION_UP消息的传递流程。可以看到,这时的ACTION_UP消息的流向与正常情况下ACTION_UP消息的流向是完全相同的。
2.3.3.1 在ViewGroup2的dispatchTouchEvent函数中返回false拦截ACTION_MOVE消息:
public class ViewGroup2 extends LinearLayout{
...
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch(ev.getAction()){
case ACTION_MOVE:
return false;
}
return super.dispatchTouchEvent(ev);
}
}
这种情况下的消息传递流程如图2-3-3-2所示:
- 红线: 这条线表示ACTION_DOWN消息的传递流程,也就是在TextView的OnTouchEvent函数中返回true消费消息时的传递流程。
绿线 :这条线表示ACTION_MOVE消息的传递流程。需要注意的是这里的ACTION_MOVE消息传递流程中没有第几次的区别,每次消息在传递到ViewGroup2的dispatchTouchEvent函数中以后,就直接流到Activity的onTouchEvent函数中。事实上,无论你在哪个控件的dispatchTouchEvent函数中通过返回false拦截了ACTION_MOVE消息,之后消息都会直接流到Activity的onTouchEvent函数中。
蓝线:这条线表示消息ACTION_UP消息的传递流程。可以看到,这时的ACTION_UP消息的流向与正常情况下ACTION_UP消息的流向是完全相同的。
2.坐标系
2.1屏幕坐标系
如图2-1所示:
屏幕坐标系的起始位置就是屏幕的左上角,X轴向右为正,Y轴向下为正。
2.2View坐标系
如图2-2所示:
黑框表示屏幕,蓝色区块表示一级ViewGroup,它的子View是灰色区块所表示的二级ViewGroup,而二级ViewGroup又有一个子控件,这就是红色区块所表示的View控件。
View坐标系的坐标系原点在该View的父控件的左上角,即View坐标系是相对父控件而言的。
2.3 常用函数与View坐标系
getLeft、getTop、getRight、getBottom
这几个函数用于得到View控件左上角何右下角距离父View控件的距离。如图2-3所示:
需要注意的是,这4个函数获取的值是View原始状态时相对于父控件的距离,对View进行平移操作并不会改变这4个函数的返回值。
getWidth、getHeight
getWidth = getRight() - getLeft();
getHeight = getBottom() - getTop();
getX、getY
getX() = getLeft() + getTranslationX();
getY() = getTop() + getTranslationY();
getX、getY函数用于获取控件左上角点的实时位置。注意,view.getX和view.getY获取的坐标是相对于父控件而言的。
2.4 MotionEvent提供的函数
- getX:获取点击事件距离控件左边的距离,即View坐标
- getY:获取点击事件距离控件顶边的距离,即View坐标
- getRawX:获取点击事件距离整个屏幕左边的距离,即绝对坐标
- getRawY:获取点击事件距离整个屏幕顶边的距离,即绝对坐标
如图2-4所示:
判断当前触摸点是否在某个View的区域内
private boolean isPointOnViews(MotionEvent event) {
boolean result = false;
Rect rect = new Rect();
for (int i = getChildCount()-1;i >= 0;i--) {
View view = getChildAt(i);
Log.d(TAG,"isPointOnViews===>view#getX="+view.getX()+",view#getY="+view.getY());
int[] location = new int[2];
// 获取控件在屏幕中的位置,返回的数组分别为控件左顶点的 x、y 的值
view.getLocationOnScreen(location);
Log.d(TAG,"isPointOnViews===>view#getLocationX="+location[0]+",view#getLocationY="+location[1]);
Log.d(TAG,"isPointOnViews===>event#getX="+event.getX()+",view#getY="+event.getY());
rect.set(
location[0],
location[1],
location[0]+view.getMeasuredWidth(),
location[1]+view.getMeasuredHeight());
if(rect.contains((int)event.getRawX(),(int)event.getRawY())){
// 标记被拖拽的child
mDragView = view;
result = true;
break;
}
}
return result && currentState != State.DRAGGING;
}
或者使用
private boolean isPointOnViews(MotionEvent event) {
boolean result = false;
Rect rect = new Rect();
for (int i = getChildCount()-1;i >= 0;i--) {
View view = getChildAt(i);
Log.d(TAG,"isPointOnViews===>view#getX="+view.getX()+",view#getY="+view.getY());
int[] location = new int[2];
// 获取控件在屏幕中的位置,返回的数组分别为控件左顶点的 x、y 的值
view.getLocationOnScreen(location);
Log.d(TAG,"isPointOnViews===>view#getLocationX="+location[0]+",view#getLocationY="+location[1]);
Log.d(TAG,"isPointOnViews===>event#getX="+event.getX()+",view#getY="+event.getY());
rect.set((int) view.getX(), (int) view.getY(), (int) view.getX() + (int) view.getWidth()
, (int) view.getY() + view.getHeight());
if (rect.contains((int) event.getX(), (int) event.getY())) {
//标记被拖拽的child
mDragView = view;
result = true;
break;
}
}
return result && currentState != State.DRAGGING;
}
3.滑动冲突
3.1 详解requestDisallowInterceptTouchEvent(boolean disallowIntercept)函数
requestDisallowInterceptTouchEvent
是ViewGroup中的函数,当子View不想父控件拦截消息的时候会调用requestDisallowInterceptTouchEvent(true)
函数来通知父控件,让它不要拦截消息,使消息能够流向自己。
该函数有一个参数boolean disallowIntercept,表示是否禁止父控件拦截消息,参数值为true时,表示禁止父控件拦截消息;为false时,表示允许父控件拦截消息。
值得注意的是,要使
requestDisallowInterceptTouchEvent(true)
函数有效的前提是必须能够执行到它!如果父控件在获得ACTION_DOWN消息时就直接进行拦截的话,那么子控件将收不到任何消息,那么requestDisallowInterceptTouchEvent(true)
函数也将不起作用。
3.2 requestDisallowInterceptTouchEvent函数的正确使用方法
3.2.1 在onInterceptTouchEvent函数中拦截ACTION_MOVE消息
首先需要消息能够流到子控件,这就要求父控件不拦截ACTION_DOWN消息,而只拦截ACTION_MOVE消息,也就是拦截消息的流程如图3-2-1所示:
即父控件的拦截消息代码如下:
public boolean onInterceptTouchEvent(MotionEvent event){
switch(event.getAction()){
case ACTION_MOVE:
return true;
}
return super.onInterceptTouchEvent(event);
}
在这种情况下,在ACTION_DOWN消息到来时,消息就可以流到TextView的dispatchTouchEvent和onTouchEvent函数中了。而在TextView中,可以通过requestDisallowInterceptTouchEvent(true)来禁止父控件拦截消息。
public class CustomTextView extends TextView{
...
public boolean dispatchTouchevent(MotionEvent event){
getParent().requestDisallowInterceptTouchEvent(true);
return super.onInterceptTouchEvent(event);
}
public boolean onTouchEvent(MotionEvent event){
getParent().requestDisallowInterceptTouchEvent(true);
return super.onInterceptTouchEvent(event);
}
}
在这种情况下,ACTION_MOVE消息并没有被拦截,而是直接传递给了顶层的CustomTextView,这样我们就实现了通过
requestDisallowInterceptTouchEvent(true)
来禁止父控件拦截消息的功能。
需要注意的是,在CustomTextView的所有父控件中消息都没有流到onInterceptTouchEvent函数中,这说明当我们通过requestDisallowInterceptTouchEvent(true)
来禁止父控件拦截消息时,该控件的所有父控件的onInterceptTouchEvent函数都将被跳过。
3.2.2 在dispatchTouchEvent函数中拦截ACTION_MOVE消息
我们在ViewGroup2的dispatchTouchEvent函数中拦截ACTION_MOVE消息,然后在CustomTextView的dispatchTouchEvent函数中添加禁止拦截消息的代码,会发现requestDisallowInterceptTouchEvent(true)
函数无效,ACTION_MOVE消息并不会流向CustomTextView,而是在ViewGroup2的dispatchTouchEvent 函数中就停止了。
值得注意的是,requestDisallowInterceptTouchEvent 其仅能禁止onInterceptTouchEvent中的TouchEvent,因此在dispatchTouchEvent函数中拦截消息时,它是无效的。
总结
- 要想使用requestDisallowInterceptTouchEvent(true);有效,不能在父控件中拦截ACTION_DOWN消息。
- 在父控件的dispatchTouchEvent函数中拦截消息时,requestDisallowInterceptTouchEvent(true);将会失效。
- 只有在父控件的onInterceptTouchEvent函数中拦截消息时,requestDisallowInterceptTouchEvent(true);才会有效。
- 在通过requestDisallowInterceptTouchEvent(true);禁止父控件拦截消息时,所有父控件的onInterceptTouchEvent函数都将被跳过。
3.3 滑动冲突实战
3.3.1 滑动冲突类型
3.3.1.1 外层与内层的滑动方向不一致
比如外层只能横向滑动,内层只能纵向滑动。例如ViewPager内嵌ListView;
3.3.1.2 外层与内层的滑动方向一致
例如ViewPager内嵌横向的ListView。
3.3.2 解决思路
外部拦截法
点击事件都先经过父控件的拦截处理,如果父控件需要此事件就拦截,如果不需要就不拦截,让消息传递给子控件。
外部拦截法需要重写父控件的onInterceptTouchEvent函数,在内部进行相应的消息拦截即可,这种方法的伪代码如下:
public boolean onInterceptTouchEvent(MotionEvent event){
boolean intercepted = false;
switch(event.getAction){
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if(父控件需要当前点击事件){
intercepted = true;
}else{
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
return intercepted;
}
内部拦截法
父控件不拦截任何消息,所有消息都传递给子控件,如果子控件需要此消息就直接消费掉,否则就交给父控件处理。
这种方法的伪代码如下:
public boolean dispatchTouchEvent(MotionEvent event){
switch(event.getAction){
case MotionEvent.ACTION_MOVE:
if(自己需要当前点击事件){
getParent().requestDisallowInterceptTouchEvent(true);
}else{
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return super.dispatchTouchEvent(event);