Andriod

触摸事件的分发(ViewGroup篇之一)

2016-10-19  本文已影响911人  张利强

本篇和触摸事件的分发(View篇)将侧重于Android源码的分析。略显枯燥,但Read the fucking code的code不就是这样吗?

本文代码基于API15分析,而不是最新的API23。因API23中多出的代码和本文无关。
为减少篇幅完整地代码注释会在文末给出链接。

关键的成员变量

触摸目标

TouchTargetViewGroup的一个静态内部类。描述一个被触摸的 childView 和他所捕获的手指的ids。

private static final class TouchTarget {
 public View child;//被触摸到得child
 public int pointerIdBits;//由该目标捕获的所有的手指的IDS 的结合位掩码
 public TouchTarget next;//用于指向链表中的下一个TouchTarget
}

记录到的全是子View的信息,在处理向子View发送事件的逻辑时使用

触摸目标链表

为了有条理得向子View分发事件,ViewGroup需要记录所有用户触摸到的触摸目标信息。在经过一系列判断逻辑后,向其中的触摸目标分发事件。
这里是通过定义一个TouchTarget类型的成员变量mFirstTouchTarget来实现的,其记录了第一个触摸目标,是触摸目标链表的头。通过它(的next),可以找到链表中所有的触摸目标。

    private TouchTarget mFirstTouchTarget;

ViewGroup.dispatchTouchEvent()

ViewGroup中关于事件的分发是通过重写dispatchTouchEvent实现的。这部分是事件分发过程中最为复杂和难的地方。充分了解了这部分代码。将再也没有难点来阻挡你理解触摸事件的分发了。

ViewGroup中的dispatchTouchEvent()方法非常复杂,不了解整体设计思路,直接阅读将会一头雾水,云里雾里。所以,这里先把ViewGroup的的dispatchTouchEvent()用以下流程图归纳如下:

事件的传递之ViewGroup.png

其中比较关键的有以下几点:

  1. 触摸事件的安全策略
  2. 处理最初的DOWN事件
  3. 检查事件的拦截情况
  4. 检查是否取消
  5. 根据需要为DOWN事件更新触摸目标链表
  6. 分发触摸事件到目标View
  7. 根据需要,为UP、CANCEL事件更新触摸目标链表

其中,除了第一条相对分发流程比较独立外,其余都标有数字序号,并在图中用蓝色标注出来了。
接下来,我们分别详细讲每一个点。

〇、触摸事件的安全策略

根据用户体验来讲,用户只会尝试去点击可以直接看到的View(或ViewGroup),所以Google据此为触摸事件的分发制定了一个安全策略:
如果某View不处于顶部,并且View设置的属性是该View不在顶部时不响应触摸事件,则不分发该事件。

不满足安全策略需要同时满足两个条件:

  1. 配置设定被遮挡时需要过滤触摸事件(mViewFlags包含FILTER_TOUCHES_WHEN_OBSCURED)
  2. 触摸事件确实被遮挡(event.getFlags()包含MotionEvent.FLAG_WINDOW_IS_OBSCURED)

若不满足安全策略,onFilterTouchEventForSecurity(MotionEvent event)方法返回false,从上文流程图可以看到,这种情况会放弃接下来的所有分发操作。

    /**
     * 依据安全策略,过滤触摸事件。
     * @param event The motion event to be filtered. 需要被过滤的触摸事件。
     * @return True if the event should be dispatched, false if the event should be dropped.
     * 该事件需要被分发,则返回true。该事件需要被丢弃,则返回false。
     */
    public boolean onFilterTouchEventForSecurity(MotionEvent event) {
        //noinspection RedundantIfStatement
        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;
    }

一、处理最初的DOWN事件,重置ViewGroup状态

Android触摸事件--MotionEvent一文我们可以知道:DOWN类型的事件是一系列触摸事件的开始。

当上一次触摸结束后,ViewGroup的某些状态可能已经发生了变化,比如ViewGroup的成员变量mFirstTouchTarget已经记录了一些值。
这时,做了两个动作:

  1. 取消并清空触摸目标链表。 cancelAndClearTouchTargets(MotionEvent event)
     /**
     * 取消并清空所有的的触摸目标
     * Cancels and clears all touch targets.
     */
    private void cancelAndClearTouchTargets(MotionEvent event) {
        if (mFirstTouchTarget != null) { //如果mFirstTouchTarget链表不为空,则清空该链表
            boolean syntheticEvent = false; //是否是我们人为合成的事件。
            if (event == null) {
                final long now = SystemClock.uptimeMillis();
                event = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);//人为合成一个 ACTION_CANCEL 事件
                event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
                syntheticEvent = true;
            }

            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                resetCancelNextUpFlag(target.child);
                //从cancelAndClearTouchTargets的调用关系,我们可以发现,这里发送出去的事件只可能是两种:
                // 1、处理 DOWN 事件时的DOWN事件
                // 2、当该ViewGroup离开屏幕(Window)时,发送上面人为合成的ACTION_CANCEL 消息。
                // 但因为第二个参数为true,无论是哪种事件,最终都会被转化为取消事件。
                dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
            }
            clearTouchTargets();

            if (syntheticEvent) {
                event.recycle();
            }
        }
    }
2.png

根据Android中ViewGroup源码,该方法除了在这里之外,还会在ViewGroup的dispatchDetachedFromWindow()方法中被调用。这部分逻辑在流程图中特别标示出来了。在分发触摸事件的情况下地逻辑就简单了不少。
1)遍历触摸目标列表,将链表中的所有子View的 CANCEL_NEXT_UP_EVENT 标志全部重置
2)将取消事件传递给链表中所有的子View。(这里事件虽未DOWN事件,但dispatchTransformedTouchEvent方法第二个参数为true,最终都会被转化为ACTION_CANCEL事件)
3)清空触摸链表 clearTouchTargets()

  1. 重置所有触摸状态来为一个新的循环做准备。 resetTouchState();
    private void resetTouchState() {
        clearTouchTargets();//清空触摸链表
        resetCancelNextUpFlag(this);//重置CANCEL_NEXT_UP_EVENT 标志
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;//重置 FLAG_DISALLOW_INTERCEPT 标志
    }

这里代码非常简单,就是清空触摸链表、重置本ViewGroup实例的 CANCEL_NEXT_UP_EVENT 标志、重置 FLAG_DISALLOW_INTERCEPT标志。
和cancelAndClearTouchTargets()方法主要处理触摸链表中的子View不同,该方法主要是针对ViewGroup实例自身的一些处理。

二、检查事件的拦截情况

APP开发工程师在开发程序时,可以对ViewGroup是否拦截事件做的限定有:
1、是否允许拦截触摸事件;由 requestDisallowInterceptTouchEvent(boolean disallowIntercept) 来设定
2、是否要拦截某个触摸事件;由onInterceptTouchEvent(MotionEvent ev)返回值来决定

而ViewGroup内部是如何处理这些限定呢?
我们结合代码来看一下:

 @Override
  public boolean dispatchTouchEvent(MotionEvent ev) {
    ****代码从略   ****
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {//允许拦截
                    intercepted = onInterceptTouchEvent(ev);//根据onInterceptTouchEvent(dev)决定是否拦截
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;//不允许拦截
                }
            } else {//不是down事件并且mFirstTouchTarget==null也没用与之对应的触摸目标
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                // 不存在触摸目标,并且该事件不是down事件,那么就继续拦截触摸事件。
                //(比如一个空的ViewGroup,则拦截触摸事件,通过自己的touchEvent处理)。
                // (又比如虽ViewGroup不为空,但触摸事件并没发生在任何子View上)。
                intercepted = true;
            }
    ****代码从略   ****
  }

在两种情况下,ViewGroup需要根据APP工程师的限定来决定事件是否被拦截:

  1. 当发生down事件时
    down事件是一个完整事件序列的的起点,当发生down事件时,这时还不知道是否有子View可消费该事件(此时mFirstTouchTarget已经被重置为null),必须根据APP工程师对ViewGroup的限定,方知是否需要拦截该事件。
  2. mFirstTouchTarget 不为null时
    这意味已经(靠该序列的起点down事件)找到要消费触摸事件的目标了,那么肯定不会是down事件了,而是move、up等类型的事件。这时,我们也需要根据APP工程师对ViewGroup的限定来决定是否拦截这种类型(move、up等)的事件。

从代码可知:只有在允许拦截并且onInterceptTouchEvent(MotionEvent ev)返回true时,才会对触摸事件拦截。

在以下情况下,触摸事件默认需要交给ViewGroup自己来处理,这时,我们可以当做事件被拦截了来处理:

比如说一个空的Layout布局;Layout布局不为空,但用户从未点击到任何子View上。

三、检查事件是否被取消

            final boolean canceled = resetCancelNextUpFlag(this)
                  || actionMasked == MotionEvent.ACTION_CANCEL;

很简单两种情况:

  1. 当前的View或ViewGroup要被从父View中detach时,PFLAG_CANCEL_NEXT_UP_EVENT就会被设为true;此时,resetCancelNextUpFlag(this)返回true,canceled被赋值为true,它就不再接受触摸事情。
  2. 触摸事件本身就是MotionEvent.ACTION_CANCEL类型的事件。

四、根据需要,为down事件更新触摸目标链表

  1. 获取到该触摸事件所对应手指ID,从触摸目标链表中清空与之对应的所有触摸目标。
  2. 遍历子View,直到找到即可接收触摸事件,触摸事件的坐标又坐落在其坐标范围内的childView;找到了就继续,找不到结束循环。
  3. 尝试从已有的触摸目标链表中找到与该childView对应的触摸目标实例,找到即结束。
  4. 没有在已有链表中找到对应的触摸目标实例,就把该事件发送给childView去处理,并生成一个触摸目标实例加入到触摸目标链表中。
  5. 找不到newTouchTarget,并且触摸目标链表不为空时,将newTouchTarget指向触摸目标链表的最初的target去处理

�1:第一个手指按下(单点触摸)时mFirstTouchTarget链表被清空,肯定找不到
2:多点触摸的后续手指按下时,从前面手指按下时产生的mFirstTouchTarget链表中寻找

五、分发触摸事件到目标。

如果触摸目标链表为空,直接把该ViewGroup当成一个普通的View来处理,

如果触摸目标链表不为空,则遍历链表,将事件分发给触摸目标对应的childView中去。若某childView事件接收到了CANCEL事件,就从链表中移出该触摸目标。

六、处理CANCEL事件和手指抬起事件

http://blog.csdn.net/ns_code/article/details/49848801
http://wangkuiwu.github.io/2015/01/04/TouchEvent-ViewGroup/
http://blog.csdn.net/yanbober/article/details/45912661
http://blog.csdn.net/lfdfhl/article/details/42241253
http://www.cnblogs.com/hi0xcc/p/5583791.html

触摸事件的安全策略

    /**
     * Filter the touch event to apply security policies.
     * 依据安全策略,过滤触摸事件。
     * 安全策略:
     * ①:配置设定被遮挡时需要过滤触摸事件(mViewFlags包含FILTER_TOUCHES_WHEN_OBSCURED)
     * ②:触摸事件确实被遮挡(event.getFlags()包含MotionEvent.FLAG_WINDOW_IS_OBSCURED)
     * @param event The motion event to be filtered. 需要被过滤的触摸事件。
     * @return True if the event should be dispatched, false if the event should be dropped.
     * 该事件需要被分发,则返回true。该事件需要被丢弃,则返回false。
     *
     * @see #getFilterTouchesWhenObscured
     */
    public boolean onFilterTouchEventForSecurity(MotionEvent event) {
        //noinspection RedundantIfStatement
        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;
    }

触发ACTION_CANCEL事件

运行上面两个例子,如果没什么差错的话,你是不会看到ACTION_CANCEL事件的,为什么呢?

要触发ACTION_CANCEL,就先得了解一个类ViewGroup,ViewGroup是一个放置其他views(子view)的特殊view,它是布局类(*Layout)、视图容器(ListView、GridView、HorizontalScrollView、TabHost等等很多)的基类。

也就是说ViewGroup一般是做为父视图来容纳、管理其他子视图的。既然管理,在用户手势操作过程中,就会存在父视图不希望子视图响应用户手势操作的情况。Android提供了一个函数public boolean onInterceptTouchEvent (MotionEvent ev),在用户手势操作时,系统先调用父视图(一个继承自ViewGroup的类)的这个函数,来决定当前手势操作是由父视图还是子视图来响应、处理。我们仔细看看这个函数名,函数名中有一个单词intercept,经过查词典,这个单词的中文意思是拦截。在用户的一个完整手势操作过程中(起自ACTION_DOWN,终于ACTION_UP),对于每一次的MotionEvent``Android都会调用该函数,向父视图查询是否拦截当前MotionEvent,如果父视图返回false:不拦截,则系统会调用子视图的onTouchEvent函数;如果父视图返回true:拦截,则系统调用父视图的onTouchEvent。等等,有人不禁要问了,如果在这个完整手势操作过程中,父视图初期返回false、后期返回true会是一个什么样的情况呢(捣乱的来了)?这个嘛,是这个样子的,一开始返回false,毫无疑问,子视图会被调用onTouchEvent,但凡父视图在函数onInterceptTouch中有一次返回了true,那这一完整手势操作内所有后续的MotionEvent都会调用父视图的onTouchEvent,即使父视图后期反悔而改成返回false也不行(没有后悔药)。在这种父视图先返回false,后返回true的情况下,子视图收不到后续的事件,而只是在父视图由返回false改成返回true(拦截)的时候收到ACTION_CANCEL事件。

我们可以得到的结论

疑问

  1. 在处理最初子View事件时,是否会对触摸链表中的子View都传递DOWN事件。

其他

本篇在将来的某天会有更新。

上一篇下一篇

猜你喜欢

热点阅读