ViewGroup事件传递原理


触屏是用户和手机交互的基础,手指触屏时产生一系列事件,控制视图改变,在树形视图中,事件从顶层向下传递。

View和ViewGroup的dispatchTouchEvent方法,事件传递到视图的第一个方法,它们实现方式不同,ViewGroup容器视图,要么消费掉事件,要么派发给某个子视图。View非容器视图,自己接手处理。ViewGroup的dispatchTouchEvent方法。

public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        //第一部分,down事件初始化
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }
        //第二部分,检查拦截
        //第三部分,非打断,非取消时处理,代码段贴在后面,遍历子视图
        //第四部分,代码段贴在后面,发到touch目标
        //第五部分,最终处理。
    }
    ...
    return handled;
}

事件的初始化

down事件,表示手指第一次接触到屏幕,清除以前保存的TouchTargets链表,链表保存上一次触屏接收事件的子视图。

private void cancelAndClearTouchTargets(MotionEvent event) {
    if (mFirstTouchTarget != null) {
        boolean syntheticEvent = false;
        ...
        //发送ACTION_CANCEL事件
        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
            resetCancelNextUpFlag(target.child);
            dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
        }
        //清空TouchTarget,链表每一项元素recycle回收
        clearTouchTargets();
        if (syntheticEvent) {
            event.recycle();
        }
    }
}

清理上一次触摸遗留下来的东西。


拦截判断

下面是摘取的相关代码段。

final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        //重设action放置改变。
        ev.setAction(action);
    } else {
        intercepted = false;//设置过标志,永远不拦截
    }
} else {
    intercepted = true;
}
    ...
// 是否ACTION_CANCEL类型
final boolean canceled = resetCancelNextUpFlag(this)
        || actionMasked == MotionEvent.ACTION_CANCEL;

final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;

down事件和mFirstTouchTarget链表不空,这两种情况需要拦截判断。
down是第一个事件,需要拦截判断。
非down事件时,链表mFirstTouchTarget不空,说明前面已经存在接收事件到目标子视图,(或许不止一个),可以直接派发到目标,要经过一层拦截判断。
这两种情况进行拦截判断是合理的。

不满足以上两个条件,说明在down事件时,子视图中不存在可接收消费事件到目标,对于非down事件,不需要向子视图派发,也不需拦截判断,直接设置intercepted标志,交给容器视图onTouchEvent方法。


遍历查找满足条件子视图

经过一次拦截判断,不拦截且不取消事件类型时,优先向子视图派发,事件类型必须是down或pointer_down,才会向子视图中查找目标。
move事件不会走这一步,会直接派发给已存在的mFirstTouchTarget目标,无目标就自己处理,不需要在子视图查找,有intercepted标志,不会来这里。

...
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;

if (!canceled && !intercepted) {//非打断,非取消处理
    ...
    if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        final int actionIndex = ev.getActionIndex(); // down事件总是0
        final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;
        //删除该pointerId曾经存储在某个TouchTarget的记录。
        //因pointerId重新触摸,并确定将被哪个子视图处理。
        removePointersFromTouchTargets(idBitsToAssign);

        final int childrenCount = mChildrenCount;
        if (newTouchTarget == null && childrenCount != 0) {
            final float x = ev.getX(actionIndex);
            final float y = ev.getY(actionIndex);
            // 找到可以接收事件的View,从前向后扫描查找
            final ArrayList<View> preorderedList = buildOrderedChildList();
            final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
            final View[] children = mChildren;
            for (int i = childrenCount - 1; i >= 0; i--) {
                final int childIndex = customOrder
                      ? getChildDrawingOrder(childrenCount, i) : i;
                final View child = (preorderedList == null)
                      ? children[childIndex] : preorderedList.get(childIndex);
                ... //触摸点坐标(x,y)区域范围判断
                if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                    ev.setTargetAccessibilityFocus(false);
                    continue;
                }
                //找到newTouchTarget退出遍历
                newTouchTarget = getTouchTarget(child);
                if (newTouchTarget != null) {
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                    break;
                }
                //未找到newTouchTarget,继续
                resetCancelNextUpFlag(child);
                //看子视图是否消费。
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                    mLastTouchDownTime = ev.getDownTime();
                    if (preorderedList != null) {
                        for (int j = 0; j < childrenCount; j++) {
                            if (children[childIndex] == mChildren[j]) {
                                mLastTouchDownIndex = j;
                                break;
                            }
                        }
                    } else {
                        mLastTouchDownIndex = childIndex;
                    }
                    mLastTouchDownX = ev.getX();
                    mLastTouchDownY = ev.getY();
                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                    alreadyDispatchedToNewTouchTarget = true;
                    break;
                }
                ...
            }
            if (preorderedList != null) preorderedList.clear();
        }

        //未处理的pointerId分配给现有TouchTarget
        if (newTouchTarget == null && mFirstTouchTarget != null) {
            // 新手指未找到可接受事件的View
            // 将idBitsToAssign分配到最早手指的目标
            newTouchTarget = mFirstTouchTarget;
            while (newTouchTarget.next != null) {
                newTouchTarget = newTouchTarget.next;
            }
            newTouchTarget.pointerIdBits |= idBitsToAssign;
        }
    }
}

查找一个可以接收事件的子视图,创建一个TouchTarget对象,加入链表,如果该视图已存在TouchTarget,说明是pointer_down类型事件,已有一个手指触摸在该视图,将pointerId合并到该TouchTarget,多个手指触屏到该子视图。

子视图数组遍历顺序,如果设置Z轴值,preorderedList不是空,按照Z轴排序的列表,立体的Z轴越大,优先分发,一般情况下不设置。从子视图数组mChildren尾部开始,按照从大到小的索引遍历,针对可能重叠放置的子视图,保证最上面,也就是最后加入的先接收事件。数组索引是xml中定义的索引,其中,setChildrenDrawingOrderEnabled方法,可以控制子视图绘制顺序,getChildDrawingOrder方法,可以获取该顺序,一般情况下,绘制顺序childIndex与数组索引相同,复杂情况下,设置了setChildrenDrawingOrderEnable(boolean),并重写ViewGroup的getChildDrawingOrder方法,(默认的是按照子视图的添加顺序,即视图数组的索引顺序),改变子视图绘制顺序,则子视图索引childIndex就与遍历当前i值不同。

通过两个方法判断子视图满足事件接收的条件。

private static boolean canViewReceivePointerEvents(View child) {
    return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                || child.getAnimation() != null;
}

视图可见或有动画。

protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
    final float[] point = getTempPoint();
    point[0] = x;
    point[1] = y;
    transformPointToViewLocal(point, child);
    final boolean isInView = child.pointInView(point[0], point[1]);
    if (isInView && outLocalPoint != null) {
        outLocalPoint.set(point[0], point[1]);
    }
    return isInView;
}

判断触摸点坐标(x,y)是否位于子视图区域范围。根据MotionEvent的getX和getY方法,获取触控点坐标。

区别getX和getRawX。
getX是相对父视图坐标系的坐标值,getRawX是相对整个屏幕坐标系的坐标值。

以父容器坐标系为标准。ViewGroup的transformPointToViewLocal方法,将触控点坐标,转化成相对子视图坐标系的坐标值,减去子视图相对父视图mLeft/mTop距离就能实现转换,若父视图存在Scroll,再加上Scroll。
View的pointInView方法,判断转换后的坐标值是否在子视图(0,0,width,heigh)区域范围,在该范围内说明触摸点在该子视图内部。

计算触屏点在ViewGroup子视图位置

两个条件同时满足,找到子视图。遍历TouchTargets链表,查找与该子视图对应的TouchTarget。

查找到newTouchTarget目标

说明pointer_down类型事件,第2个甚至3、4...个手指触摸坐标均在该子视图中,将手指pointId合并到目标Target的pointerIdBits,结束遍历,下面不用再执行dispatchTransformedTouchEvent方法去查看子视图是否消费了,因为已经有TouchTarget绑定了该pointerId。

未查到newTouchTarget目标。

可能存在两种情况,down事件,已经清理过链表,pointer_down事件,新手指触摸子视图与前一手指正在触摸子视图不同。
将继续执行dispatchTransformedTouchEvent方法。
将事件传递给子视图,成功消费后,新建newTouchTarget,插入链表头部,宣告down/pointer_down事件被子视图成功消费,结束遍历,不必再查找其他子视图啦。

最后,将未处理的pointerId分配给现有TouchTarget。出现这种情况的场景。

down事件,如果分发子视图成功,会新建newTouchTarget目标,且同时赋值mFirstTouchTarget,若分发子视图失败,二者都是空,down事件不会出现这种情况。
pointer_down事件,第一触控点在该子视图的一个兄弟视图上,第二触控点在该子视图分发失败或并未触摸到任何子视图,均会导致newTouchTarget是空。说明pointer_down事件未被任何一个子视图成功消费。idBitsToAssign合并到链表最后一项元素的pointerIdBits中。
将未被消费的手指pointerId合并到另一个手指的消费目标中,之前已有多个手指触摸的话,合并到最早创建的那个TouchTarget目标,在链表尾部。合并图。

TouchTarget目标链表

图中三个TouchTarget,三个目标视图,四个触控点,pointer_down事件成功派发子视图的TouchTarget插入链表前端,pointId代表pointer_down事件产生的手指触控点Id。如图,pointerId是3的触控点未找到合适的视图,合并到TouchTarget3中(红色)。

综上所述

容器视图派发,经历事件初始化,拦截判断,子视图遍历查找。
拦截判断,down事件或目标链存在。
单个手指第一次触摸时,才会找到触摸子视图,看他能否承接消费事件,可以才为其创建TouchTarget。
系统自动为未消费的触点分配目标,前提是目标链存在。


目标处理
if (mFirstTouchTarget == null) {
    //自身处理
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
} else {
    //子视图传递
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next;
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            handled = true;//该手指第一次触屏被处理了,是新目标。
        } else {//已有目标。
            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
             //根据目标pointerIdBits匹配手指
            if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                handled = true;
            }
            if (cancelChild) {
                //若是打断,将目标target回收。
                if (predecessor == null) {
                    mFirstTouchTarget = next;
                } else {
                    predecessor.next = next;
                }
                target.recycle();
                target = next;
                continue;
            }
        }
        //设置前一个处理目标。
        predecessor = target;
        target = next;
    }
}

链表是空,可以推断出,无子视图消费down事件。未找到符合触摸坐标的子视图或者找到子视图但未消费。
调用dispatchTransformedTouchEvent方法,子视图参数传空,交给基类View的dispatchTouchEvent方法,把当前容器视图当成一个View视图,View的dispatchTouchEvent方法将触发onTouchEvent方法,触控点id传ALL_POINTER_IDS。

链表不是空,遍历,说明至少存在一个目标视图消费事件,多个节点说明多个手指触控点存在子视图消费事件。
如果有新增标志且目标是newTouchTarget,说明事件是pointer_down或down类型。在前面代码中,事件已dispatchTransformedTouchEvent被子视图消费掉,直接设置handled标志。简单情况下,只有一个手指触摸,一个目标,可以将直接handled返回。
如果非当前新增,该事件有任何类型的可能,派发事件到该目标的对应子视图,交给dispatchTransformedTouchEvent处理,根据处理结果设置handled标志。

在onInterceptTouchEvent方法被拦截,设置cancelChild标志,处理时,向子视图发送cancel事件,交给容器视图处理。子视图在move事件正常消费过程中,突然遭遇容器视图拦截,传递给子视图的事件改变为cancel事件,这次事件子视图的返回依然是消费成功。链表所有元素依次被回收。下一次move事件再次判断时,表头mFirstTouchTarget是空,代码不再执行到这里,事件也不会向下传递。
设置cancelChild标志子视图,从链表删除这个元素。前一个元素predecessor引用,遍历链表删除元素时,可以找到前面的引用,将其next指向next,删除当前。
如果未被拦截,
不设置cancelChild标志。子视图分发处理。以上情况。该手指触屏事件有可能是各种类型,链表元素也可能有多个。那么,一个手指的在某个子视图的事件都会经历整个目标链,都会派发么?在真正派发方法中再分析。


派发的最终处理

if (canceled || actionMasked == MotionEvent.ACTION_UP
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
    resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
    final int actionIndex = ev.getActionIndex();
    final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
    removePointersFromTouchTargets(idBitsToRemove);
}

cancel和up事件类型,表示事件结束,清空内部目标链表,不会再有事件传递到该视图,pointer_up事件,表示有一个手指触控点离开,仍有其他手指触控点,将该触控点对应pointerId在TouchTarget中清除。


真正的派发方法

dispatchTransformedTouchEvent方法,在前面第三和四部分都涉及过该方法,分别是新接触点消费和遍历目标链表消费。

private boolean dispatchTransformedTouchEvent(MotionEvent event, 
            boolean cancel,View child, int desiredPointerIdBits) {
    final boolean handled;
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        //派发给子视图或当前视图父类即View处理。
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    //oldPointerIdBits,所有手指pointerId集合
    //desiredPointerIdBits,目标触控点集合
    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
    //目标里面一个当前的手指pointerId都没有,说明该id离开了屏幕
    if (newPointerIdBits == 0) {
        return false;
    }

    final MotionEvent transformedEvent;
    //匹配成功
    if (newPointerIdBits == oldPointerIdBits) {
        if (child == null || child.hasIdentityMatrix()) {
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);

                handled = child.dispatchTouchEvent(event);

                event.offsetLocation(-offsetX, -offsetY);
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event);
    } else {
        transformedEvent = event.split(newPointerIdBits);
    }

    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }

        handled = child.dispatchTouchEvent(transformedEvent);
    }

    transformedEvent.recycle();
    return handled;
}

有几个参数,事件对象,取消标志,目标视图以及目标触控点,在每一个TouchTarget中,都保存pointerIdBits,代表触控点位。

如果有cancel标志,或者是cancel事件,
将MotionEvent对象设置为cancel事件派发,派发给子视图或当前视图。表示这层视图有打断,或者是更上层视图有打断,发给该层视图的cancel事件。

根据MotionEvent对象,获取触控点pointerId,
因为在上面代码中,每个事件都会经历整个目标链表,需要将该事件触控点匹配目标内部存储集合,只能向包含该pointerId目标TouchTarget的子视图派发。desiredPointerIdBits表示合并到处理目标TouchTarget的触控点集合

oldPointerIdBits是所有手指触控点集合,
将它与处理目标的触控点集合与操作,如果新值是0,没有位相等,说明该目标的触控id集合对应的手指已经不再屏幕,无法匹配。

如果newPointerIdBits与oldPointerIdBits相等,表示很可能是所有的手指均触控在目标触控点对应子视图上。
如果不相等,从所有手指触控点id中,分离出desiredPointerIdBits中存储的pointerId,然后封装成一个新的事件transformedEvent,分发到目标对应子视图。这里再看一下第四部分的那个问题。

举例,视图有两个子视图,各有1个手指触摸,目标链有两个TouchTarget,通过移动其中1个手指产生move事件,按照前面的逻辑,事件会经历整个目标链,两个TouchTarget都会处理,执行两次dispatchTransformedTouchEvent方法。从2个手指分离出每一个子视图触控点集合,封装事件,然后发向每一个子视图,只有一个手指move,两个子视图都会收到move事件。

调试图

定义了两个View,两个手指分别触摸他们,一个手指不动,滑动另外一个,产生连续move事件,结果两个视图都会接收到,而且是前后连续的,说明事件是先后发送两个视图的,就是在遍历TouchTarget时。后面的数字是打印View对象的HashCode。
如果child是空时,父类分发,child不是空时,子视图分发。
若子视图是容器,处理方法与前面一致,每层树结构节点ViewGroup的dispatchTouchEvent方法递归,若子视图是叶子节点,触发基类View的dispatchTouchEvent方法。
派发给子视图或本视图父类View#dispatchTouchEvent。关键点是入参子视图是否为空。不管是以上哪一种情况,事件派发成功返回true标志。
到这里,ViewGroup的dispatchTouchEvent方法的这六个部分分析完了。


单手指触控流程

TouchTarget链表中仅有一个节点。
down事件,触控点位于子视图,且子视图消费,创建TouchTarget,封装子视图与触控点pointerId,子视图未消费,TouchTarget一直保持空,ViewGroup自己处理。
move事件,TouchTarget不空,说明down事件已找到合适的消费目标,交给TouchTarget内部保存的子视图处理,TouchTarget是空,ViewGroup自己处理。

ViewGroup的dispatchTouchEvent流程

down,move,up事件来到ViewGroup,第一站是dispatchTouchEvent方法。

  • down先来,清理遗留TouchTarget,onInterceptTouchEvent决定是否打断,两种处理方式。子视图成功处理时给mFirstTouchTarget赋值TouchTarget对象。
  • move进来,看mFirstTouchTarget有值么,没有?子视图不给力,down未搞定,必须打断自己处理。mFirstTouchTarget有值,子视图已经搞定down,onInterceptTouchEvent决定是否打断,打断向子视图发Cancel,置空TouchTarget,继续走子视图处理,下一次move事件则走另一条TouchTarget为空的线路。
    若不断的有move事件进来则说明自己本身View#dispatchTouchEvent或者子视图一定成功处理,包括Down事件
    只有ViewGroup#dispatchTouchEvent从Down事件开始向上层返回true,才会在上层ViewGroup中为其建立一个TouchTarget,对上层视图来说,该ViewGroup消费了事件才会有源源不断的move事件进来。
  • 从上层向下看,该ViewGroup#dispatchTouchEvent返回true,说明在当前ViewGroup中事件被处消化,至于是它的子视图还是本身消化的,上层不关心,只要求结果。返回false,对上层来说该ViewGroup总归是没消化。

总结

单个手指从触摸到离开屏幕产生的完整事件流ACTION_DOWN、一系列ACTION_MOVE、ACTION_UP。
进入视图的每个事件,先交给dispatchTouchEvent方法分发,本视图或其子视图消费事件,View和ViewGroup的分发方案不同,ViewGroup重点是子视图分发,View重点是本视图消费。
拦截,一旦拦截成功,即使前期事件有处理目标,也会将目标回收,后续事件不会再触发拦截方法,View类无拦截方法。
视图处理,onTouchEvent方法,ViewGroup视图,拦截或子视图未消费时调用,View视图,无Touch监听器时调用,当前视图及子视图的最后一道防线,如果onTouchEvent未消费,上层便不会为该子视图保存目标,后续事件再无法向它传递了。
一个子视图成功处理down事件,父视图内部将为其创建目标,绑定该视图,不拦截情况下,后续的move事件将直接传递到该视图处理。未成功处理down事件,将down事件交给父视图onTouchEvent方法处理,其他事件再也不会传递到该视图。
一个子视图成功处理down事件,父视图内部将为其创建目标,绑定该视图,不拦截情况下move事件直接传递到该视图处理,如果move事件未成功处理,事件将无视图接手,包括父视图的onTouchEvent方法,最终将交给Activity的onTouchEvent处理。


任重而道远