话说在很多时候,特别是在自定义控件时,需要对事件传递机制要有所了解,当然,网上这方面的资料一搜一大堆,然而如果不自己整理一遍的话,还是感觉不踏实。了解Android事件分发机制并不难,难的是能一步一步搞清楚原理,结合Android源码可以帮助我们理解,所以这篇文章就诞生了。
PS: 吐槽一下,看网上很多分析的源码都很简单的,和自己看的代码感觉完全是两个风格,表示很头疼。后来发现是因为我看的是API 23的代码,新版本有很多之前没有的方法,很坑爹。写完整个文章后才知道这一点,真是醉了。Orz
现在让我们创建一个简单的Activity,创建一个TestLinearLayout继承自LinearLayout,创建一个Test继承自Button。
在TestLineaLayout类中,重写了和事件相关的代码,整个代码如下:
1 | public class TestLinearLayout extends LinearLayout { |
在TestButton中,同样也重写的和事件分发相关的代码,整改代码如下:
1 | public class TestButton extends Button { |
然后是MainActivity的XML布局:
1 | <com.wl.com.testview.TestLinearLayout |
最后是MainActivity的代码:
1 | public class MainActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener { |
然后我们运行项目,点击button按钮,会得到如下的信息:
点击button以外的地方,会得到如下的信息:
建议使用模拟器来运行点击事件。如果用真机的话,会触发大量的
ACTION_MOVE
事件。
事件分发的流程就出来了,那么为什么会是这样的呢?注意到事件都是从MainActivity
的dispatchTouchEvent()
方法开始调用的,那我们就从这个方法开始着手看代码吧。
Activity 的事件分发
事件传递到Activity后,第一个触发的方法是dispatchTouchEvent()
,我们看一看这个方法的源码:
1 | public boolean dispatchTouchEvent(MotionEvent ev) { |
好简单的代码,最喜欢看到这种“小清新”风格的代码了。
首先判断MotionEvent
是不是ACTION_DOWN
,如果是的话,执行onUserInteraction()
方法,然后判断getWindow().superDispatchTouchEvent(ev)
是否为true,如果为true,就不会执行onTouchEvent()
方法,如果为false
则执行onTouchEvent()
方法。
onUserInteraction()
这个方法只有当ACTION_DOWN
时才会触发, 那么这个方法是干嘛的?我们点进去看,会发现啥也没有。这个方法是空方法:
1 | /** |
还好有注释,不然都不知道这个方法是用来干嘛的了。当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法。下拉statubar、旋转屏幕、锁屏不会触发此方法。所以它会用在屏保应用上,因为当你触屏机器 就会立马触发一个事件,而这个事件又不太明确是什么,正好屏保满足此需求。
getWindow().superDispatchTouchEvent(ev)
这个方法是干嘛的呢?我们点进superDispatchTouchEvent()
看看:
1 | /** |
意思大概是说,这个方法会在Dialog等界面中使用到,开发者不需要实现或者调用它。
纳尼?这样就把我们打发了?不带这样玩的啊…
我们注意到superDispatchTouchEvent()
方法是getWindow()
调用的,getWindow()
方法返回的是一个Window
对象。我们在Window
类的说明中可以看到一下内容:
1 | /** |
相信大家的英语应该比我好,我就不翻译了,注意到这句话:The only existing implementation of this abstract class is android.view.PhoneWindow
,这是说PhoneWindow
是Window
的唯一实现类。那么我们就可以在PhoneWindow
类中看一下superDispatchTouchEvent()
方法究竟干了什么。
在PhoneWindow
类中,我们可以看到如下代码:
1 |
|
很简单,mDecor
对象调用了superDispatchTouchEvent()
方法。那么mDecor
对象又是什么?
跳转到mDecor
的声明,代码如下:
1 | // This is the top-level view of the window, containing the window decor. |
发现原来mDecor
是DecorView
的实例。等等,注释里面说,DecorView
是视图的顶层view?
再跳转到DecorView
类的定义处,发现这么一行代码:
1 | private final class DecorView extends FrameLayout implements RootViewSurfaceTaker { |
现在我们清楚了,DecorView
继承自FrameLayout
,是我们编写所有的界面代码的父类。
然后我们看看mDecor.superDispatchTouchEvent()
这个方法干了什么,也就是在DecorView
类中,superDispatchTouchEvent()
方法的内容:
1 | public boolean superDispatchTouchEvent(MotionEvent event) { |
嗯? 看不懂?注意,刚才我们已经知道了,DecorView
是继承自FrameLayout
,那么它的父类就应该是ViewGroup
了,而super.dispatchTouchEvent(event)
方法,其实就应该是ViewGroup
的dispatchTouchEvent()
方法,不信?你可以点进去看看。
现在回到最开始的地方:在Activity 中:
1 | public boolean dispatchTouchEvent(MotionEvent ev) { |
其中getWindow().superDispatchTouchEvent(ev)
的意义大概就清楚了,就是说,如果视图顶层的ViewGroup
-DecorView类-的dispatchTouchEvnent()
方法返回true
的话,就不会执行onTouchEvent()
方法了。
那么,问题来了,ViewGroup
中的dispatchTouchEvent()
方法什么时候返回true
,什么时候返回false
呢?
ViewGroup的事件分发
先看一下源码:
1 | public boolean dispatchTouchEvent(MotionEvent ev) { |
是不是瞬间感觉头大了?和Activity中dispatchTouchEvent()
的简洁优美
的代码完全是两个风格,面对这堆庞然大物
,我们要有目的性的,策略性的解决它!
回顾一下我们目标:ViewGroup
中的dispatchTouchEvent()
方法什么时候返回true
,什么时候返回false
。
然后看一下ViewGroup
方法中dispatchTouchEvent()
方法返回的是handled
的值,也就是我们要特别关注该方法中会改变handled
值的语句。
第12行代码初始化了handled
的值,默认为false
。
第13行的if (onFilterTouchEventForSecurity(ev))
判断囊括了dispathcTouchEvent()
的几乎所有的方法。也就是说,如果onFilterTouchEventForSecurity(ev)
返回为true
的话,就表示可以分发该触摸事件,如果返回为false
,则不分发事件。查看一下onFilterTouchEventForSecurity(ev)
的方法:
1 | public boolean onFilterTouchEventForSecurity(MotionEvent event) { |
- FILTER_TOUCHES_WHEN_OBSCURED 是
android:filterTouchesWhenObscured
属性所对应的位。android:filterTouchesWhenObscured
是true的话,则表示其他视图在该视图之上,导致该视图被隐藏时,该视图就不再响应触摸事件。 - MotionEvent.FLAG_WINDOW_IS_OBSCURED 为
true
的话,则表示该视图的窗口是被隐藏的。
然后在第18行开始判断,如果是ACTION_DOWN
事件的话,就会触发cancelAndClearTouchTargets(ev);
和resetTouchState();
方法,在resetTouchState()
方法中,有一个clearTouchTargets();
方法,这个方法将mFirstTouchTarget
设置为null
。为什么要关心这个呢?很快你就知道答案了。
第27行创建了一个intercepted
的布尔变量,看名字就知道是记录是否拦截的标志了。第28行有一个判断:actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null
,刚才我们知道了mFirstTouchTarget
是为null的,也就是说,如果是ACTION_DOWN
的话,就会直接进入这个方法块中。
然后第30行判断是否设置了FLAG_DISALLOW_INTERCEPT
标志,如果设置了,则disallowIntercept
为true
(禁止拦截判断), intercepted
直接设置为false。
对于这个FLAG_DISALLOW_INTERCEPT
标志,其实有两种方法可以设置,第一种自然是通过ViewGroup
设置Flag来,第二种就是通过代码,调用ViewGroup
中的requestDisallowInterceptTouchEvent()
方法设置:
1 | public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { |
可以看到,其实通过代码的方式最后还是将ViewGroup
设置FLAG_DISALLOW_INTERCEPT
flag。在我们的代码中,使用getParent().requestDisallowInterceptTouchEvent(true);
可以剥夺父view 对除了ACTION_DOWN
以外的事件的处理权。
为什么是
ACTION_DOWN
以外的呢?
其实在第23行代码resetTouchState()
的方法中,有一行代码:mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
也就是每次来到ViewGroup
的dispatchTouchEvent()
方法里面时,都会重置这个flag。因此,设置FLAG_DISALLOW_INTERCEPT
flag并不能影响ViewGroup
对ACTION_DOWN
的处理。
好了,扯远了,调用onInterceptTouchEvent()
方法,然后将结果赋值给intercepted。那就来看下ViewGroup与众不同与View特有的onInterceptTouchEvent方法:
1 | public boolean onInterceptTouchEvent(MotionEvent ev) { |
很简单,直接返回false
。但是我们可以重写这个方法,也就是说,如果我们在ViewGroup中重写了onInterceptTouchEvent()
方法,并且让它返回true。那么intercepted也为true,表示拦截事件。
第57行,如果既不是ACTION_CANCEL
,也不拦截事件,那么就会进行下面的判断:
1 | if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { |
在第79行判断了childrenCount != null
,然后从子View的最后一个元素开始往前遍历。
第106行判断判断子view是否能接受点击事件。
判断完了之后在137行有一个newTouchTarget = addTouchTarget(child, idBitsToAssign);
语句,查看addTouchTarget()
方法,会发现里面有一行代码是这样的:mFirstTouchTarget = target;
这里就是将mFirstTouchTarget
赋值的地方。
也就是说,当子View存在,并将事件传递给了子View后,
mFirstTouchTarget
就不为null了,
这个时候,如果在传入ACTION_MOVE
或ACTION_UP
事件进入ViewGoup的dispatchTouchEvent()方法后,由于不会调用cancelAndClearTouchTargets(ev)
方法,mFirstTouchTarget
也就不为null!!!这种情况需要特别注意,将直接影响代码的分析。
如何派发事件呢?注意到第121行有一个判断:if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
,这个dispatchTransformedTouchEvent()
在后面还会出现,可以看一下里面的代码。由于代码比较长,而且我们关心的是和child
有关的代码,抽象出来,就如同下面的判断:
1 | if (child == null) { |
也就是说,如果子view为空,则调用ViewGroup
自身的dispatchTouchEvent()
方法,如果不为空,则调用子View的dispatchTouchEvent()
方法。这里就是派发事件的地方。
回到我们分析的地方。在第121行,由于判断了子View非空,则会调用子View的dispatchTouchEvent()
方法,该子View既可能是View,也可能是ViewGroup。而如果没有子View,就会在164行调用super.dispatchTouchEvent()
方法(其实也是View的dispatchTouchEvent()
方法,因为ViewGroup的父类就是View,然而View的dispatchTouchEvent()
方法和ViewGroup的那个方法是有区别的)。
现在,让我们总结一下:
- ViewGroup执行
dispatchTouchEvent()
方法后,会执行onInterceptTouchEvent()
方法。 - 如果我们重写
onInterceptTouchEvent()
方法,并且返回true,那么就会调用父类的dispatchTouchEvent()
方法。 - 如果我们没有重写
onInterceptTouchEvent()
方法,或者重写了该方法但是返回了false。如果有子View,则会调用子View的dispatchTouchEvent()
方法,如果没有子View,则调用父类(View)的dispatchTouchEvent()
方法。
看来无论怎么样都会触发到View中的dispatchTouchEvent()
方法,而且对于我们最开始的问题,也是和View中的dispatchTouchEvent()
方法的返回值有直接的关系。那么,该方法里面又是怎样一番美景呢?
View的事件分发
那么我们看一看View里面的dispatchTouchEvent()
的代码:
1 | /** |
第26行判断是否为ACTION_DOWN
,如果是,则停止滚动。
第31行通过onFilterTouchEventForSecurity()
方法判断当前View是否被覆盖。
第34行开始有一个判断语句:if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))
。
这个判断语句比较重要,下面逐个分析:
- li 对象肯定不为空。因为li对象由mListenerInfo赋值,而关于mListenerInfo有下面的代码存在:
1 | ListenerInfo getListenerInfo() { |
- li.mOnTouchListener 是否为空呢?这个就要看我们是否为View设置了OnTouchListener了,如果设置了,就不为空;没设置就为空。
(mViewFlags & ENABLED_MASK) == ENABLED
这个判断当前的View是否为ENABLE
,默认都是enable。li.mOnTouchListener.onTouch(this, event))
这个就是判断OnTouchLinstener
的返回了。
如果上面4点都为true,则34行的判断语句也为true。这时候View中的dispatchTouchEvent()
返回值也为true。
到这里我们就能推导出一些结论性的东西了:View的dispatchTouchEvent()
方法触发后,如果该View为ENABLE,并设置了OnTouchLisener且返回为true。那么该View的dispatchTouchEvent()
返回true。
如果该View没有设置OnTouchListener()
或者OnTouchListener()
返回false。那么就会执行到第40行做一个判断onTouchEvent(event)
。那么onTouchEvent()
什么时候返回true?什么时候返回false呢?我们可以看一下该方法的源码:
1 | public boolean onTouchEvent(MotionEvent event) { |
这个方法的代码和ViewGroup的dispatchTouchEvent()
源码的长度有得一拼了。
第7行有一个判断,如果该View为DISABLE
,返回值就由13行的判断决定:如果CLICKABLE
、LONG_CLICKABLE
或CONTEXT_CLICKABLE
有一个为true,则返回true。(默认情况下,LONG_CLICKABLE
为false)
其实我们可以发现,只要进了第7行判断的代码,该onTouchEvent()
方法就返回true,否则就返回false。这一点很重要。
如果View是ENABLE的,那么就会进入第24行的判断。在ACTION_UP
事件里面,有一个方法performClick()
我们看一下它的源码:
1 | public boolean performClick() { |
在第4行有一个判断。之前已经知道了li不为空了。那么li.mOnClickListener
呢?我们看一下li.mOnClickListener
赋值的地方:
1 | public void setOnClickListener(@Nullable OnClickListener l) { |
setOnClickLisnter()
!!!多么熟悉的方法!!!也就是说,如果我们为View重写了setOnClickLinstener()
方法的话,performClick()就返回true,否则返回false。
好了,结合上面的分析,我们可以得出关于View事件分发的一些结论:
- 如果设置了
OnTouchListener()
且返回true。View的事件流程就完成了,不会执行到OnClickLinstener()方法。 - 如果没有设置
OnTouchLinstener()
或者设置了OnTouchListener()
且返回false。则View会执行OnClickLinster()方法。
如果View中的dispatchTouchEvent()
返回了false
,结合我们之前对于Activity的相关分析,就会调用到Activity的onTouchEvent()
方法。
终于结束了一个事件的轮回,不容易啊。
让我喝口茉莉花茶,再聊聊剩下的。
小结
结合之前对Activity
和ViewGroup
的相关分析,我们可以得出事件分发的大致流程:
- 事件分发是从
Activity
开始,传递到Window
,再传递给视图的顶级View
(一般是View的子类ViewGroup)。然后顶级View再派发事件。 - 事件传递到ViewGroup之后,会首先调用
dispatchTouchEvent()
方法,然后执行onInterceptTouchEvent()
方法。由于ViewGroup中的该方法默认返回false,所以ViewGroup默认不拦截事件。如果我们重写了onInterceptTouchEvent()
方法并返回了true,那么该事件就被消耗掉了,不会往下派发了。否则传递到子View(如果有),或者调用View的onInterceptTouchEvent()
方法。 - 当事件传递到View之后(这里的View包含ViewGroup),会首先调用
dispatchTouchEvent()
方法。在该方法内会调用OnTouchListener()
,如果设置了OnTouchListener()
且返回true。View的事件流程就完成了,不会执行到OnClickLinstener()方法。如果没有设置OnTouchLinstener()
或者设置了OnTouchListener()
且返回false。则View会执行OnClickLinster()
方法。至此,一个事件派发结束。 - 如果事件在ViewGroup中就被拦截了(onInterceptTouchEvent()方法被重写并返回了true),或者事件在View中的
dispatchTouchEvent()
方法返回了false。(有好多种可能的情况使得该方法返回false,详见上面的分析),就会调用到Activity中的onTouchEvent()
方法。
也就是说,在整个流程中会涉及到以下三个方法:
- dispatchTouchEvent() 用来分发事件。如果事件传递到了某个view,则该view的
dispatchTouchEvent()
方法肯定会调用到。而其返回值受到下面两个方法的影响。 - onInterceptTouchEvent() 只有ViewGroup中存在该方法,用来决定是否拦截某个事件。
- onTouchEvent() 在dispatchTouchEvent()方法中会调用到这个方法。用来处理点击事件。
当然,在分析的过程中我们也得出了一些比较有意思的结论,比如:
- View的
ENABLE
属性是否为true并不会对onTouchEvent()
的默认返回值有影响。只要clickable
、longclickable
或者contextclickable
有一个为true,那么onTouchEvent()
就返回true。 - 我们可以通过
requestDisallowInterceptTouchEvent()
方法在子View中控制父View的事件是否传递,但是对ACTION_DOWN事件除外。 - View没有
onInterceptTouchEvent()
方法,而ViewGroup中有该方法。这是二者的区别之一。
大概就是这些了,欢迎交流。
以后再看源码一定尽量选低版本的源码看了,嗯。