前言
在前面一篇文章我们说了如何把点击事件从Activity传递给ViewGroup的,以及它的2个方法重要方法dispatchTouchEvent以及onTouchEvent的调用时机和逻辑,那本章就接着说点击事件在ViewGroup中是如何处理的。
该系列文章链接:
Android事件分发 | Activity分发事件 - 掘金 (juejin.cn)
Android事件分发 | View分发事件 - 掘金 (juejin.cn)
正文
从前面Activity的思想来看,ViewGroup的传递思路也是责任链模式,一层一层进行向下传递,和Activity不同的是这里由3个方法配合完成。
三个方法
对于基本的事件处理Android开发者应该都比较熟悉了,所以我直接画个流程图来示意:
这里其实当ViewGroup自己处理事件时,就是View的事件分发流程了,因为很简单,ViewGroup的父类就是View,所以这里可以直接调用super.dispatchTouchEvent方法,关于这部分内容,下篇文章View的事件分发细说。
dispatchTouchEvent
这个方法的代码很多,我们来截取主要的一段:
//拦截标志位
final boolean intercepted;
//只有当这俩种情况才考虑要不要拦截事件
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) { //见分析1
//见分析2
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
在上一篇文章中我们知道在dispatchTouchEvent方法中会调用onInterceptTouchEvent方法来判断是否要拦截事件,但是这个逻辑没有那么简单,上面代码就是其中的主要一段逻辑。
这里注意有个至关重要的点,就是当事件类型是 ACTION_DOWN 或者 mFirstTouchTarget != null 时才会去判断是否要拦截的主要逻辑。
所以当接收到 ACTION_DOWN事件时,不管什么情况都进行判断是否要拦截(只是判断是否要拦截,而不是要不要拦截)。
那另一个判断条件是啥呢?
分析1:mFirstTouchTarget != null
这里理解起来比较绕,主要就是为了实现一个功能。
当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值,并指向子元素。反过来说也就是当ViewGroup拦截事件时,mFirstTouchTarget就为null,那为什么要搞这一出呢?
假设现在有个滑动操作,我在GroupView A的View B上进行滑动,如下所示:
假如View B设计的需求是不能点击且无法滑动,所以要求在点击事件传递到GroupView A的时候就给拦截掉,这里一系类事件可以归纳为:
DOWN -> MOVE .... MOVE -> UP这个事件流,
所以看下面流程图:
这里会发现当DOWN事件被onInterceptTouchEvent给拦截后,后面的MOVE和UP事件都会被它处理,因为由前面可知事件被拦截后mFirstTarget为null,所以这里的onInterceptTouchEvent不会再次被调用。
这里的代码就会产生一条真理,当ViewGroup拦截某个事件流的DOWN事件时,那这整个事件流都会给这个ViewGroup来处理,不会再往下进行分发,而且该ViewGroup的onInterceptTouchEvent将不会再被调用,这个很关键,后面有细说。
当DOWN事件没有被ViewGroup的onInterceptTouchEvent给拦截时,也就是返回false,这时后面的MOVE和UP事件还会进行判断,这时就有可能再次调用ViewGroup的onInterceptTouchEvent方法来看是否拦截事件了,至于如何调用以及时机,我们继续说。
分析2:disallowIntercept标志位
上面代码其实很符合一般的逻辑,不过也有一些问题需要处理,上面代码无法处理,就比如下面这个例子:
这里GroupView A是一个竖直方向可以滚动的View,同时View B是一个竖直方向可以滚动显示内容的View,这里就可以把View B看成RecyclerView,这时我向上滑动B,这时内容进行位移:
当已经到底的时候,也就是条目12是最后一条,这时再向上滑动,应该让整个View B向上滑动,即下面效果:
这时MOVE事件将由ViewGroup A进行拦截处理。
这里就涉及到了Android事件分发的重要知识点:滑动冲突。
滑动冲突
其实这里就涉及到了滑动冲突的问题,不过我想说的是凡是看问题我们要看其本质。
比如这里的问题,如果我们按照前面流程图来说的话,我们是可以解决的,也就是我们常说的外部拦截法。
下面来看解决办法
外部拦截法
- 首先是DOWN事件传递到A时,这时是不能进行拦截的,按照前面所说,如果DOWN事件被A拦截,它后面的事件流将都会被A处理。
- 接着是MOVE事件传递到A时,按照前面代码流程,这时mFirstTouchTarget不为空,将进入判断,这时再判断B是否滑动到了顶部或者底部,如果滑动到了顶部或者顶部,则由A把MOVE事件给拦截,A进行处理,否则不拦截,MOVE事件传递给B,由B继续处理。
这里的外部拦截法其实比较好理解,注意点就是不能拦截DOWN和获取B的状态,所以这里有个局限性,也就是A的事件分发必须处理,B的也要处理,有点麻烦。
内部拦截法
那有没有一个更方便的做法呢,比如我这里的A就是一个竖向的ScrollView,B是我自定义的View,我只需要修改B就可以达到这个效果,当然有,这就是内部拦截法。
内部拦截法的关键就是这个disallowIntercept标志位,所以我们先来看看这个标志位是啥。
//ViewGroup中的flag
protected int mGroupFlags;
给它赋值的方法通常由它的子View来赋值:
//通常由子类来调用
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
//true -> FLAG_DISALLOW_INTERCEPT
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
//递归调用
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
然后是使用的地方,也就是之前说的dispatchTouchEvent方法中:
//dispatchTouchEvent关键逻辑代码
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//这里默认为false,也就是上面方法为false
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
//当调用requestDisallowInterceptTouchEvent为false时
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
关于这个flag有很多叫法,我个人喜欢叫做 是否禁用事件拦截功能 ,默认是false,也就是不禁用。
-
当requestDisallowInterceptTouchEvent方法设置为false时,也就是不禁用事件拦截功能,这时包括DOWN、MOVE事件都会在ViewGroup中进行判断是否拦截,比如例子中的ViewGroup A它会默认拦截竖直方向的MOVE事件,如果不禁用事件拦截功能,它将会拦截MOVE事件。
-
当requestDisallowInterceptTouchEvent方法设置为true时,说明禁用事件拦截功能,这时不论什么事件都不进行拦截,传递给其子View处理。
内部拦截法原理总结
所以这里的disallowIntercept这个标志位 是否禁用事件拦截功能 非常重要。下面来小小总结一下:
-
当你自定义View时,无法修改其包裹View的事件拦截代码时,需要使用内部拦截法。
-
对于DOWN事件,其包裹View不能进行拦截,前面也是说了,假如把DOWN拦截了,后面子View将永远不会得到剩下事件流。
-
对于MOVE事件,当需要自己处理时,则禁用包裹View的事件拦截功能。当不需要自己处理时,则不禁用包裹View的事件拦截功能。
兜底处理
当然和前面的Activity一样,当ViewGroup的子View没有成功消费掉事件,或者点击的ViewGroup的空白地方,这时就会有兜底策略,会调用super.dispatchTouchEvent方法,也就是View的事件分发机制来处理,至于一直没说的onTouchEvent方法,我们会放在View分发事件中来说。
总结
这篇内容大多数作为Android开发都非常熟悉了,不过温故而知新,还是总结几点:
-
根据源码mFirstTouchTarget的逻辑,当ViewGroup拦截DOWN事件时,后续事件流都由它处理。
-
ViewGroup的onInterceptTouchEvent方法默认不拦截DOWN事件,视需求情况拦截MOVE事件,一般情况所有事件都会在这里判断是否拦截。
-
当无法修改包裹View的事件拦截时,可以通过是否 禁用ViewGroup的事件拦截功能 来实现内部拦截法,来让ViewGroup根据子View来分发事件。
-
当ViewGroup的子View没有消费事件或者点击空白处,由ViewGroup的父类View自己处理。