转载一篇文章,对这篇文章受益匪浅,建议看文章的时候也打开你的AndroidStudio,并且找到对应的代码,跟着文章去看,看完之后,找个例子实际一下
事件分发机制解析
1.介绍相关基础
Android中与事件分发机制相关的主要方法有三个:dispatchTouchEvent、onInterecptTouchEvent、onTouchEvent。而事件分发一般会经过视图的三个层级:Activity、ViewGroup、View。下表会对视图不同的三个层级拥有的事件分发的相关方法进行整理:
事件分发相关方法 方法功能 Activity ViewGroup View public
boolean dispatchTouchEvent 事件分发 + + + public boolean onInterceptTouchEvent 事件拦截 + public
boolean onTouchEvent 事件消费 + + +
"+"号表示拥有该方法
这几个相关方法返回值说明:
public boolean dispatchTouchEvent(MotionEvent ev)
- return true ----- 表示事件会分发给当前View并由
diapatchTouchEvent
方法进行消费,同时事件会停止向下传递
- return false ----- 表示事件停止往下面的视图层级进行传递,同时开始往上面的视图层级的
onTouchEvent
传递,也称为回溯。
public boolean onInterceptTouchEvent(MotionEvent ev)
- return true ------ 表示将事件进行拦截,并将拦截到的事件交由当前视图层级(一般为ViewGroup及其子类)的
onTouchEvent
进行处理
- return false ------ 表示将事件放行,当前视图层级上的事件会被传递到下一级的视图层级。再由下一级的视图层级的dispatchTouchEvent来开始这个事件的分发
public boolean onTouchEvent(MotionEvent ev)
- return true ----- 表示该视图层级(一般为View类及其子类)接收并消费该事件
- return false ----- 表示传递的事件会从当前视图层级向上传递,并且都是由上层视图层级的
onTouchEvent
来接收,如果上级的视图层级的onTouchEvent
也返回false,那么这个事件就会消失,并且后续的事件也将会接收不到
2.从源码[1]介绍事件分发机制
Activity视图层级
整个事件分发流程的起点就是Activity的dispatchTouchEvent
,那接下来我们看看它的源码:
Activity.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev){
if (ev.getAction==MotionEvent.ACTION_DOWN){
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)){
return true;
}
return onTouchEvent(ev);
}
哈哈,大家觉得这是不是很简单呢。其实秘密都藏在了两个判断语句当中。首先我们来看看第一个判断语句。当如果触摸事件为MotionEvent.ACTION_DOWN
就会回调onUserInteraction
方法。那我们现在来看看onUserInteraction
的源码
Activity.onUserInteraction
public void onUserInteraction(){
}
大家是不是懵逼了。咦!这怎么是个空方法。不过还好关于这个方法还有注释,不然就真的是懵逼了。原来这个方法会在此Activity在栈顶时触摸Android手机三大触摸键HOME、BACK、MENU键都会回调此方法。而下拉状态栏和旋转屏幕、锁屏都会回调此方法。所以此方法可以用在屏保应用上,例如Window电脑处于屏保模式时动一下鼠标就可以退出屏保模式一样。好,这个方法领会了。让我们再来看看第二个判断语句。
getWindow()
获取的是一个Window对象,而我们知道PhoneWindow
是Window
类的唯一实现类。那我们看看PhoneWindow.superDispatchTouchEvent
的源码
PhoneWindow.superDispatchTouchEvent
@Override
public boolean superDispatchTouchEvent(MotionEvent event){
return mDecor.superDispatchTouchEvent(event);
}
mDecor是不是感觉很熟悉。没错!他就是DecorView
的实例对象。那我们再看看mDeocr.superDispatchTouchEvent
的源码
DecorView.superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event){
return super.dispatchTouchEvent(event);
}
super
关键字我们都知道是调用父类的方法。而DecorView
实例对象mDecor
是继承自FrameLayout
,而FrameLayout
是继承自ViewGroup
,所以super.dispatchTouchEvent
其实就是ViewGroup
的dispatchTouchEvent
方法。
ViewGroup视图层级
上面我们刚刚谈论到了ViewGroup.dispatchTouchEvent
方法,按照惯例我可能又会带大家去看一下他的源码。但是这次我不这么做了(嘿嘿,来打我呀),想知道原因的同学可以自己去看源码(其实就是该方法块源码太长)。特别是里面有太多的变量以及方法,想一一掌握也是不现实的事。所以我就把关键部分挑出来做个说明就好,方便大家学习。
ViewGroup.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev){
......
if(action==MotionEvent.ACTION_DOWN){
if(......){
......
}
if(disallowIntercept||!onInterceptTouchEvent(ev)){
......
final View[] children=mChildren;
final int count=mChildrenCount;
for(int i=count-1;i>=o;i--){
final View child=children[i];
if((child.mViewFlags & VISIBILITY_MASK)==VISIBLE
||child.getAnimation()!=null){
child.getHitRect(frame);
if(frame.contains(scrolledXInt,scrolledYInt)){
final float xc=scrolledXInt-child.mLeft;
final float yc=scrolledYInt-child.mTop;
ev.setLocation(xc,yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
if(child.dispatchTouchEvent(ev)){
mMotionTarget=child;
return true;
}
}
}
}
}
}
......
if(target==null){
ev.setLocation(xf,yf);
if((mPrivateFlags & CANCEL_NEXT_UP_EVENT)!=0){
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
}
......
......
}
关于该方法源码挺长的,现在我就用浓缩成这41行跟大家说明(相信我,浓缩的都是精华)。首先我们来看看第7行,这里有一个条件判断语句,如果disallowIntercept
和onInterceptTouchEvent
两者有一个为true,就会进入到这个条件判断中。disallowIntercept
是指是否禁用掉事件拦截的功能,默认是false
,也可以通过调用requestDisallowInterceptTouchEvent
方法对这个值进行修改,那么当一个值为false
的时候就会完全看第二个值来决定是否可以进入到条件判断的内部。那我们来看看第二个值,它是对onInterceptTouchEventTouch
的返回值取反!也就是说如果我们在onInterceptTouchEvent
方法中返回false
,就会让第二个值为true
,从而进入到条件判断的内部,否则情况相反。我时候我要告诉你一个“惊天秘诀”,就是事件被传递到View视图层级,其实就是在判断语句的内部进行的。
我们这时候来看看判断语句的内部,第11行通过一个for循环,遍历了当前ViewGroup下的所有子View,然后在第16行判断当前遍历的子View是不是正在点击的控件,如果是的话就会进入到另一个条件判断的内部,然后在第21行回调了该控件的dispatchTouchEvent
方法,之后的过程就可以承接到下一个部分的内容了。从上面的源码我们还可以看出如果ViewGroup下的子View的dispatchTouchEvent
方法返回true
的话,那么ViewGroup的dispatchTouchEvent
也是返回true
。后面的代码就不在执行了。
如果第16行判断当前遍历的子View不是正在点击的控件,而是ViewGroup本身的话,不会进入刚刚那个条件判断的内部,而是会转而执行后面的代码。我们再看看第31行,如果target为null,就会进入到下面的判断语句的内部。该判断语句的内部最后回调了父类的dispatchTouchEvent
方法。而ViewGroup的父类就是最大的基类View了,因此ViewGroup也是View类,此时ViewGroup会被看做一个普通的控件,去执行View.dispatchTouchEvent
方法,至于后面的过程怎么样也是可以承接到下一个部分我们要讲的内容了。
ViewGroup.onInterceptTouchEvent
public boolean onInterceptTouchEvent(MotionEvent ev){
return false;
}
如上面所示,默认的onInterceptTouchEvent
方法是返回false
,就是不拦截事件。然而我们可以根据实际项目中所需的业务逻辑去重写这个方法来达到实际需求(比如使用滑动类Scroller使布局可以滑动,其中需要判断是否滑动,滑动时阻止事件向下分发,交由布局处理,这是典型运用)。然后会遍历该ViewGroup中的所有子组件,如果子组件也是ViewGroup也会遍历其内部的子View,这是一个递归的过程。如果是子组件是View类,则会调用View的dispatchTouchEvent
,如果其返回true
,则说明事件被方法内部消费了,立即停止继续遍历,即可以看做事件停止传递。而如果返回false
,则会回调ViewGroup的onTouchEvent
方法。
View视图层级
上面我们谈到了View的dispatchTouchEvent
方法,那么趁热打铁我们来看看其源码是怎么样的。
View.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event){
if(mOnTouchListener!=null&&
(mViewFlags&ENABLED_MASK)==ENABLED&&mOnTouchListener.onTouch)){
return ture;
}
return onTouchEvent(event);
}
这个方法还是挺简洁的,只有短短几行代码。我们可以看到在这个方法内,首先会进行一个判断,如果判断的三个条件都为真,那么就返回true
,否则就会执行onTouchEvent
方法并返回。我们现在一个个看成立的三个条件分别是什么。
mOnTouchListener
不为空,那么这个对象是在哪赋值的呢,我们看看下面的方法。
public void setOnTouchListener(OnTouchListener listener){
mOnTouchListener=listener;
}
原来我们给控件注册touch事件就会给mOnTouchListener
就一定会被赋值。而第二个条件是判断当前的控件是否是可点击的,如Button默认是可点击的,而ImageView、TextView默认不可点击(我们可以通过布局文件中设置android:clickable=true/false
来改变控件的可点击属性)。第三个条件就是看mOnTouchListener.onTouch(this,event)
的返回值,如果onTouch
返回true
,那么说明只要给可按控件注册了touch事件,同时onTouch
返回true
的话就会使View.dispatchTouchEvent
返回true
,而不会去回调View.onTouchEvent
方法。如果返回的是false
,就会回调View.onTouchEvent
方法并返回布尔值。那我们再去看看View.onTouchEvent
的源码。
View.onTouchEvent
public boolean onTouchEvent(MotionEvent event){
final int viewFlags=mViewFlags;
......
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch(event.getAction()){
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed){
boolean focusTaken = false;
if(isFocusable() && isFocusableInTouchMode() && !isFocused()){
focusTaken = requestFocus();
}
if(!mHasPerformedLongPress && !mIgnoreNextUpEvent){
removeLongPressCallback();
if(!focusTaken){
if(mPerformClick == null){
mPerformClick = new PerformClick();
}
if(!post(mPerformClick)){
performClick();
}
}
}
}
break;
......
}
......
......
return true;
}
return false;
}
同样该方法实际源码也挺长的,我就把重点挑出来。我们可以看见第4行我们会进入一个判断,如果该控件可点击就会进入到第7行的switch判断中去,而如果当前的事件是抬起手指,则会进入到MotionEvent.ACTION_UP
这个case当中。在经过种种判断之后,会执行到第22行的performClick()
方法。我们进去里面看看这个performClick()
方法。
public boolean performClick(){
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if(mOnClickListener!=null){
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
可以看到只要mOnClickListener
通过setOnClickListener
方法给控件注册一个点击事件的话,mOnClickListener
得到赋值从而不为空值的话都会回调被点击控件onClick()
方法。这也就可以解释给同一个控件同时setOnClickListener
和 setOnTouchListener
的话,只要onTouch
返回true就会阻止onClick
的回调,说明onTouch
的优先级要高于onClick
。我们再来看看上面View.onTouchEvent
,得知对于不可点击的控件来说该方法肯定返回false,说明事件还没被消费,由此又从下至上传递该事件。
3.具体案例
上面讲了这么多理论性的知识,文字一大堆,估计会遭到一批批读者的怒斥(求轻喷,么么哒),同时文字太多对于初学者拿来学习可能会不太友好,同时也容易失去耐心和继续看下去的兴趣,这里我建议如果一下子看不完全部内容,可以分部分来看,毕竟罗马也不是一天建成的。按照自己的情况决定一次阅读多少,一次阅读的量少,就可以多看几次。我觉得这也是极好的。
当然,关于这部分,我也不会这么简单把大家忽悠过去。但是也没必要重负造轮子,在简书我也发现了一篇挺不错的技术博文,里面讲到了ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
各自的传递情况是不一样的,而且有图有真相!希望大家可以去看看图解 Android 事件分发机制。
总结
整个事件分发的传递过程我总结出了一张图,图中super代表默认回调,true、false表示方法的返回值。

这里要提几点值得注意的地方:
1.dispatchTouchEvent
和onTouchEvent
一旦return true
,事件就停止传递了(到达终点,没有谁能再收到这个事件)。对于return true
我们经常说事件被消费了,消费了的意思就是事件走到这里就是终点,不会往下传,没有谁能再收到这个事件了。
2.对于dispatchTouchEvent
返回false
的含义应该是:事件停止往子View传递和分发同时开始往父控件回溯(父控件的onTouchEvent
开始从下往上回传直到某个onTouchEvent
),事件分发机制就像递归,return false
的意义就是递归停止然后开始回溯。
3.ViewGroup 想把自己分发给自己的onTouchEvent
,需要拦截器onInterceptTouchEvent
方法return true
把事件拦截下来。
4.大体的分发过程为:首先传递到Activity
,然后传给了Activity
依附的Window
,接着由Window
传给视图的顶层 View 也就是DecorView
,最后由DecorView
向整个 ViewTree 分发。分发还会有回溯的过程。
作者:BarryLiu1995
链接:http://www.jianshu.com/p/3d4e425d6ca7
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。