在学习事件的分发机制前,我们要先了解下什么是触摸事件。触摸事件就是捕获触摸屏幕后产生的事件。比如当点击一个button的时候,通常就会产生两个或者三个事件——按钮按下,这是事件一;如果不小心滑动一下,这是事件二;当手抬起,这是事件三。Android为触摸事件封装了一个类——MotionEvent。只要是重写触摸相关的方法,参数一般都含有MotionEvent,这在接下来实例演示的时候可以看到。
MotionEvent里面封装了一些常用的东西,比如触摸点的坐标,可以通过event.getX()和event.getRaw()方法取出坐标点,也可以通过不同的Action来获取点击事件的类型(如MotionEvent.ACTION_DOWN、MotionEvent.ACTION_MOVE、MotionEvent.ACTION_UP等),进而实现不同的逻辑。因此,触摸事件其实就是一个动作类型加一个坐标而已。
我们知道,Android的View结构是树形结构,View可以放在一个ViewGroup里面,ViewGroup又可以放在其他ViewGroup里面,甚至还可能继续嵌套,这样布局嵌套可能会复杂,而我们的触摸事件就只有一个,到底该分给谁呢,子View和父ViewGroup可能都有可能需要对触摸事件进行处理,这就需要用到事件传递、拦截、处理机制了。
Android的事件产生是从我们触摸屏幕开始,经过WindowManagerService到达应用程序。
上面DecorView是经过了两次,第一次是调用DecorView的dispatchTouchEvent,它的源码是:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final Callback cb = getCallback();
return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev)
: super.dispatchTouchEvent(ev);
}
Callback就是Window.Callback,Activity实现了这个接口。
在Activity的attach函数中,会调用window的setCallback,将Activity设置给Window。所以这里getCallback返回的就是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);
}
在ACTION_DOWN的时候会调用onUserInteraction方法,然后调用Window(实际上是PhoneWindow)的superDispatchTouchEvent。
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
而DecorView的superDispatchTouchEvent为:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
最终还是调用DecorView的父类的dispatchTouchEvent,DecorView的父类是FrameLayout,它没实现该方法,最终会调用ViewGroup的dispatchTouchEvent方法。从这里开始就进入view树的事件派发流程了。
由以上的分析可以知道,当一个点击事件产生后,它的传递可以概括为如下顺序:Activity->Window->View树,即事件总是先传给Activity,Activity再传递给Window,Window再传递给顶级View,之后进入View的事件分发机制。
在了解了事件分发机制后,下面我们通过具体实例来直观感受下。
这里我自定义了一个View命名为MyView和一个ViewGroup命名为MyViewGroup,MyViewGroup包含了MyView。
代码非常简单,只是重写了事件拦截和处理的几个方法,为了方便看结果,加上一些log而已。
对于ViewGroup,重写了如下三个方法:
class MyViewGroup(context:Context,attrs:AttributeSet):RelativeLayout(context,attrs) {
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
when(event?.action){
0-> Log.d("Tag","MyViewGroup dispatchTouchEvent down")
1-> Log.d("Tag","MyViewGroup dispatchTouchEvent up")
2-> Log.d("Tag","MyViewGroup dispatchTouchEvent move")
}
return super.dispatchTouchEvent(event)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
when(event?.action){
0-> Log.d("Tag","MyViewGroup onTouchEvent down")
1-> Log.d("Tag","MyViewGroup onTouchEvent up")
2-> Log.d("Tag","MyViewGroup onTouchEvent move")
}
return super.onTouchEvent(event)
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
when(ev?.action){
0-> Log.d("Tag","MyViewGroup onInterceptTouchEvent down")
1-> Log.d("Tag","MyViewGroup onInterceptTouchEvent up")
2-> Log.d("Tag","MyViewGroup onInterceptTouchEvent move")
}
return super.onInterceptTouchEvent(ev)
}
}
对于View,重写了如下两个方法,MyView代码如下。
class MyView(context: Context, attrs: AttributeSet) : AppCompatButton(context, attrs) {
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
when(event?.action){
0->Log.d("Tag","MyView dispatchTouchEvent down")
1->Log.d("Tag","MyView dispatchTouchEvent up")
2->Log.d("Tag","MyView dispatchTouchEvent move")
}
return super.dispatchTouchEvent(event)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
when(event?.action){
0-> Log.d("Tag","MyView onTouchEvent down")
1->Log.d("Tag","MyView onTouchEvent up")
2->Log.d("Tag","MyView onTouchEvent move")
}
return super.onTouchEvent(event)
}
}
可以发现,View比ViewGroup少了一个onInterceptTouchEvent()方法,这个方法从名字就可以看出来是事件拦截的核心方法。View是不包含onInterceptTouchEvent()这个方法的,这也很容易理解,毕竟View下不能像ViewGroup一样包含View了,也就没有拦截这一说法。这里补充一点,Activity也没有onInterceptTouchEvent()方法。
点击MyView,我们来看看打出的log,如下:
可以看见,事件的传递顺序是MyViewGroup->MyView。事件传递的时候,先执行dispatchTouchEvent()方法,再执行onInterceptTouchEvent()方法。而事件的处理顺序则相反,是MyView-MyViewGroup,事件处理执行的是onTouchEvent()方法。
一句话概括,传递是从上往下,处理是从下往上冒泡。下面谈谈事件传递和处理的返回值,很好理解。
事件传递的返回值:True,拦截,不继续;False:不拦截,继续流程。
事件处理的返回值:True,处理完了,不用给上级ViewGroup处理了;False:不处理,给上级ViewGroup处理。
初始情况下,默认返回值都是false。上面的演示默认返回false,所以流程完整走完了。
这里给出一个流程图,方便我们理解这一过程:
注意流程图里省略去了dispatchTouchEvent()方法,虽然dispatchTouchEvent()方法是事件分发的第一步,但一般情况下,我们不太会去改写这个方法,所以这里暂时忽略。后面第五部分会再次提及dispatchTouchEvent()方法。
接下里我们试着改写下代码,让事件拦截。我们把MyViewGroup中的onInterceptTouchEvent()返回值改为true。
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
when(ev?.action){
0-> Log.d("Tag","MyViewGroup onInterceptTouchEvent down")
1-> Log.d("Tag","MyViewGroup onInterceptTouchEvent up")
2-> Log.d("Tag","MyViewGroup onInterceptTouchEvent move")
}
//return super.onInterceptTouchEvent(ev)
return true
}
同样点击MyView,看看打印出的log,如下
可以看到,结果和我们前面分析的一样,事件已经被外层MyViewGroup拦截了,MyView接收不到触摸事件。我们也可以看到,MyViewGroup只处理了down事件,move和up事件并没有处理。因为我们在MyViewGroup的onTouchEvent()返回默认的false,表示不处理此事件。那么这个事件就会往上级传递,这里MyViewGroup已经顶级View了,所以事件传递到了Activity去处理。
下面我们来看看事件的处理。我们再来改写一下代码,这次只修改MyView中的onTouchEvent()方法的返回值为true。
override fun onTouchEvent(event: MotionEvent?): Boolean {
when(event?.action){
0-> Log.d("Tag","MyView onTouchEvent down")
1->Log.d("Tag","MyView onTouchEvent up")
2->Log.d("Tag","MyView onTouchEvent move")
}
//return super.onTouchEvent(event)
return true
}
同样点击MyView,看看打印出的log,如下
可以看到,结果和我们前面分析的一样,事件已经被MyView处理了,不用传递给上级ViewGroup了,所以MyViewGroup的onTouchEvent()方法不会执行。由于事件被MyView处理了,所以MyView整个事件序列都会执行,所以可以看到down、move、up事件都有。这里可能大家会有个疑问,为什么MyViewGroup的dispatchTouchEvent()和onInterceptTouchEvent()也会执行完整时间序列呢?这里需要注意一点,应该把ViewGroup以及它所包含的子View都看作是这个ViewGroup的一部分,对于一个ViewGroup是否会处理一次事件,应该是包含了它的子View是否也处理。
(1)修改button的onTouchEvent返回值为false,事件是否会继续派发给button?
不会,button的onTouchEvent返回值为false的话,表示button不处理这个事件,那么button的dispatchTouchEvent()和onTouchEvent()只有down事件会相应,然后将此事件向上级View传递。
(2)派发DOWN事件给button时,onTouchEvent返回true。然后派发MOVE事件给button,onTouchEvent返回false。那么button可以接收到后续MOVE和UP事件吗?
可以,button的onTouchEvent返回true,表明button想处理此事件,那么整个事件序列都会派发给button,所以button可以接收到后续MOVE和UP事件,不关心move事件返回什么。
(3)修改button的dispatchTouchEvent返回值为false,事件是否会继续派发给button?
不会,这里需要注意一下dispatchTouchEvent()方法的返回值:
return true | 表示该View内部消化掉了所有事件 |
---|---|
return false | 事件在本层不再继续进行分发,并交由上层控件的onTouchEvent方法进行消费(如果本层控件已经是Activity,那么事件将被系统消费或处理) |
return super.dispatchTouchEvent(ev) | 事件将分发给本层的事件拦截onInterceptTouchEvent 方法进行处理 |
(4)修改button的dispatchTouchEvent返回值为true,onTouchEvent返回值为false,事件是否会继续派发给button?
会,可参考第四点,dispatchTouchEvent()方法return true的话,表示内部消化掉事件,所以事件会派发给button,程序运行结果如下,可以看到,事件序列都被处理了。
(5)纵向的listview中包含一个横向的pageview,如何处理滑动冲突?
当上下滑时,我们需要让外部的listview拦截点击事件,当左右滑时,需要让内部pageview拦截点击事件。这里提供一种解决方法,我们可以通过水平方向和竖直方向的滑动距离差来判断,比如竖直方向滑动的距离差大就判定为竖直滑动,否则为水平滑动,进而执行相应的拦截策略。
(1)如果ViewGroup找到了能够处理该事件的View,则直接交给子View处理,自己的onTouchEvent不会被触发。
(2)可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法。
(3)一个点击事件产生后,它的传递过程如下:Activity->Window->View。顶级View接收到事件之后,就会按相应规则去分发事件。如果一个View的onTouchEvent方法返回false,那么将会交给父容器的onTouchEvent方法进行处理,逐级往上,如果所有的View都不处理该事件,则交由Activity的onTouchEvent进行处理。
(4)如果某一个View开始处理事件,如果他不消耗ACTION_DOWN事件(也就是onTouchEvent返回false),则同一事件序列比如接下来进行ACTION_MOVE,ACTION_UP,则不会再交给该View处理。
(5)ViewGroup默认不拦截任何事件,Android源码中Viewgroup的onInterceptTouchEvent方法默认返回false。
(6)诸如TextView、ImageView这些不作为容器的View,一旦接受到事件,就调用onTouchEvent方法,它们本身没有onInterceptTouchEvent方法。正常情况下,它们都会消耗事件(返回true),除非它们是不可点击的(clickable和longClickable都为false),那么就会交由父容器的onTouchEvent处理。
(7)点击事件分发过程如下 dispatchTouchEvent—->OnTouchListener的onTouch方法—->onTouchEvent–>OnClickListener的onClick方法。也就是说,我们平时调用的setOnClickListener,优先级是最低的。