前面我们讲到:一个触控点的序列一般情况下只给一个view处理,当一个view消费了一个触控点的down事件后,该触控点的事件序列后续事件都会交给他处理。对于viewGroup来说,他有很多个子view,如果不同的子view接受了不同的触控点的down事件,那么ViewGroup如何记录这些信息并精准把事件发送给对应的子view呢?答案就是:TouchTarget。
TouchTarget中维护了每个子view以及所对应的触控点id,这里的id可以不止一个。TouchTarget本身是个链表,每个节点记录了子view所对应的触控点id。在viewGroup中,该链表的链表头是mFirstTouchTarget,如果他为null,表示没有任何子view接收了down事件。
TouchTarget有个非常神奇的设计,他只使用一个整型变量来记录所有的触控id。整型变量中哪一个二进制位为1,则对应绑定该id的触控点。
例如 00000000 00000000 00000000 10001000,则表示绑定了id为3和id为7的两个触控点,因为第3位和第7位的二进制位是1。这里可以间接说明系统支持的最大多点触控数是32,当然实际上一般是8比较多。当要判断一个TouchTarget绑定了哪些id时,只需要通过一定的位操作即可,既提高了速度,也优化了空间占用。
当一个down事件来临时,viewGroup会为这个down事件寻找适合的子view,并为他们创建一个TouchTarget加入到链表中。而当一个up事件来临时,viewGroup会把对应的TouchTarget节点信息删除。那接下来,就直接看到viewGroup中的dispatchTouchEvent 是如何分发事件的。首先看到源码中的第一部分:事件拦截。
事件拦截这里的拦截分为两部分:安全拦截和逻辑拦截。
安全拦截是一直被忽略的一种情况。当一个控件a被另一个非全屏控件b遮挡住的时候,那么有可能被恶意软件操作发生危险。例如我们看到的界面是这样的:
但实际上,我们看到的这个按钮时不可点击的,实际上触摸事件会被分发到这个按钮后面的真正接收事件的按钮:
然后我们就白给了。这个安全拦截行为由两个标志控制:
FILTER_TOUCHES_WHEN_OBSCURED:这个标志可以手动给控件设置,表示被非全屏控件覆盖时,直接过滤掉所有触摸事件。
FLAG_WINDOW_IS_OBSCURED:这个标志表示当前窗口被一个非全屏控件覆盖。
具体的源码如下:
View.java api29 public boolean onFilterTouchEventForSecurity(MotionEvent event) { // 两个标志,前者表示当被覆盖时不处理;后者表示当前窗口是否被非全屏窗口覆盖 if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0 && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) { // Window is obscured, drop this touch. return false; } return true; }第二种拦截是逻辑拦截。如果当前viewGroup中没有TouchTarget,而且这个事件不是down事件,这就意味着viewGroup自己消费了先前的down事件,那么这个事件就无须分发到子view必须自己消费,也就不需要拦截这种情况的事件。除此之外的事件都是需要分发到子view,那么viewGroup就可以对他们进行判断是否进行拦截。简单来说,只有需要分发到子view的事件才需要拦截 。
判断是否拦截主要依靠两个因素:FLAG_DISALLOW_INTERCEPT标志和 onInterceptTouchEvent() 方法。
子view可以通过requestDisallowInterupt方法强制要求viewGroup不要拦截事件,viewGroup中会设置一个FLAG_DISALLOW_INTERCEPT标志表示不拦截事件。但是当前事件序列结束后,这个标志会被清除。如果需要的话需要再次调用requestDisallowInterupt方法进行设置。
如果子view没有强制要求不拦截,那么会调用onInterceptTouchEvent() 方法判断是否需要拦截。onInterceptTouchEvent方法默认只对一种特殊情况作了拦截。一般情况下我们会重写这个方法来拦截事件:
// 只对一种特殊情况做了拦截 // 鼠标左键点击了滑动块 public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.isFromSource(InputDevice.SOURCE_MOUSE) && ev.getAction() == MotionEvent.ACTION_DOWN && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY) && isOnScrollbarThumb(ev.getX(), ev.getY())) { return true; } return false; }