美文网首页
触屏事件派发源码分析

触屏事件派发源码分析

作者: gczxbb | 来源:发表于2018-05-29 18:07 被阅读8次

触屏是用户交互的基础,当手指触屏时,将产生一系列事件,我们通过事件来控制视图的改变,实现用户与手机交互。本文从源码角度分析事件在树形结构,从视图顶层向下派发的过程。事件派发流程图。

事件派发流程图.jpg

从顶层视图DecorView的dispatchPointerEvent方法开始分析,它调用父类基类View的dispatchPointerEvent方法,判断触屏类型。

public final boolean dispatchPointerEvent(MotionEvent event) {
    if (event.isTouchEvent()) {//判断属于Touch类型
        return dispatchTouchEvent(event);
    } else {
        return dispatchGenericMotionEvent(event);
    }
}

然后,调用dispatchTouchEvent方法,DecorView#重写了基类View的此方法,下面是DecorView的dispatchTouchEvent方法。

public boolean dispatchTouchEvent(MotionEvent ev) {
    final Callback cb = getCallback();
    return cb != null && !isDestroyed() && mFeatureId < 0 
            ? cb.dispatchTouchEvent(ev): super.dispatchTouchEvent(ev);
}

在Activity的attach方法中,我们会创建PhoneWindow对象,初始化Callback,调用PhoneWindow的setCallback(this)方法,Activity实现Window的Callback接口。下面是实现的Window#Callback的接口dispatchTouchEvent方法。

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    //窗体视图成功消费
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    //窗体视图未成功时,由Activity处理。
    return onTouchEvent(ev);
}

从顶层视图派发事件时,优先到Activity,然后,再交给窗体处理,由窗体向视图派发。如果在视图中派发失败,未消费,则交给Activity的onTouchEvent方法处理。窗体派发superDispatchTouchEvent方法,交给顶层视图。

@Override
public boolean superDispatchTouchEvent(MotionEvent event){
    return mDecor.superDispatchTouchEvent(event);
}

调用DecorView的superDispatchTouchEvent方法。

public boolean superDispatchTouchEvent(MotionEvent event) {
    //正式进入顶层视图派发。
    return super.dispatchTouchEvent(event);
}

最终,进入DecorView父类ViewGroup的dispatchTouchEvent方法,开始在树形视图结构中传递。

事件传递到Activity流程.jpg
综上所述

每个视图触摸事件入口是dispatchTouchEvent方法。顶层视图重写了该方法,将事件转移到Activity。基类View和ViewGroup都实现了该方法,它们的处理逻辑不同,其他视图根据类型调用基类方法。
Activity希望控制事件传递,实现了Window暴露的一个接口,并设置成窗体内部CallBack对象,顶层视图是窗体内部类,因此,它可以获取这个对象,在派发时拦截消息,优先到Activity。
事件到达Activity,还会进入窗体和视图。Activity增加了一层所有视图未消费事件,自己亲自处理的逻辑。这就是视图事件传递时,当所有视图不接收事件,最后交给Activity处理的原因。


视图派发原理

针从down开始的一系列触摸事件,到达视图的处理方案是,要么处理掉它,要么派发给它的某个子视图。如果是容器视图,它包含子视图,当事件来了,需要选择派送给视图或自己接手处理。如果是非容器视图,它不包含子视图,当事件来了,只能自己接手处理。
所有的处理分别由他们的dispatchTouchEvent方法完成。决定一个事件到来后,是去是留。我们先看一下容器视图的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();
        }
    }
}

cancelAndClearTouchTargets方法,清空回收该视图内部所有的TouchTargets对象。

第二部分,拦截判断

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

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;

该代码段是dispatchTouchEvent方法中拦截判断部分。其中,在两种情况下需要拦截判断,down事件和mFirstTouchTarget目标存在。
首先,down事件是触屏系列事件中的第一个事件,此时,前面的触屏相关内容已经被清理干净。对与第一个,需要拦截判断。然后,当是非down事件时,如果链表mFirstTouchTarget不空,说明前面的事件已存在处理目标(或许还不止一个),这时,可以直接派发到处理目标对应的子视图。当然,需要在容器视图经过一层拦截判断。总之,这两种情况下进行拦截判断是合理的。
如果不满足以上两个条件,说明在down事件时,未能在子视图中找到一个可以处理事件的目标。此时,作为非down事件,也就不需要再向子视图派发啦。言外之意就是,子视图们既然down事件都没搞定,后续一的系列事件也不会再传给你处理了。这种情况下,不用拦截判断,直接设置intercepted拦截标志,拦截后交给容器视图onTouchEvent处理。

第三部分,子视图遍历相关代码

子视图遍历这部分的代码稍微有点多,下面一点点分析一下。

...
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;
        }
    }
}

首先,经过拦截判断后,当视图不拦截且不是取消的事件类型时,优先向子视图派发事件。当然,事件类型必须是down或pointer_down,才会向子视图中查找目标。为什么move事件不会走这一步呢?因为move类型会直接派发给已存在的mFirstTouchTarget目标,(无目标就自己处理),不需要在子视图查找。
查找一个子视图可以接收事件,为该视图创建一个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子视图位置.jpg

当两个条件同时满足,成功找到派发子视图,接下来查找该子视图是否存在TouchTarget。
遍历TouchTargets链表每一项元素,查找与该子视图对应的目标TouchTarget。
如果查找到newTouchTarget目标,说明是pointer_down类型的事件,第2个甚至3、4...个手指触摸坐标均在该子视图中,将手指pointId合并到目标Target的pointerIdBits,结束子视图遍历。此时,下面不需要再执行dispatchTransformedTouchEvent方法去查看子视图是否消费了,因为已经有TouchTarget绑定了该pointerId。
如果未查到newTouchTarget目标,可能存在两种情况,down事件时,前面已经清理过链表,无法查到newTouchTarget。pointer_down事件时,新手指触摸的子视图与前一手指正在触摸的子视图不同,也无法查到newTouchTarget。
继续执行dispatchTransformedTouchEvent方法,将事件传递给子视图,成功消费后,新建newTouchTarget,插入链表头部,宣告down/pointer_down事件成功被子视图消费,结束遍历(不必再查找其他子视图啦)。
最后,将未处理的pointerId分配给现有TouchTarget。
遍历结束后,如果newTouchTarget目标是空,当链表存在时,将该pointerId分配给其中的TouchTarget。看一下出现这种情况的场景。
down事件,如果分发子视图成功,会新建newTouchTarget目标,且mFirstTouchTarget同时赋值,若分发子视图失败,二者都是空。因此,down事件不会出现这种情况。
pointer_down事件,第一触控点在该子视图的一个兄弟视图上,第二触控点在该子视图分发失败,或并未触摸到任何子视图,均会导致newTouchTarget是空。说明pointer_down事件未被任何一个子视图成功消费。idBitsToAssign合并到链表最后一项元素的pointerIdBits中。
将未被消费的手指pointerId合并到另一个手指的消费目标中,之前已有多个手指触摸的话,合并到最早创建的那个TouchTarget目标,在链表尾部。合并图。

TouchTarget目标链表图.jpg 上图中,包括三个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方法处理,即容器视图亲自处理。触控点id传ALL_POINTER_IDS。
如果链表不是空,说明至少存在一个目标视图消费事件,多个节点则说明有多个手指触控点存在子视图消费事件。
如果遍历的到目标是新增,并且目标是newTouchTarget,说明事件是pointer_down或者down类型。在前面代码中,事件已被子视图消费掉,直接设置handled标志。简单情况下,如果此时只有一个手指触摸,一个目标,那就可以将直接handled返回啦。
如果遍历到的目标非当前新增,该事件有任何可能类型,需要派发事件到该目标的对应子视图。交给dispatchTransformedTouchEvent方法处理,根据处理结果,设置handled标志。
如果在onInterceptTouchEvent方法被拦截,设置cancelChild标志,在处理时,向子视图发送cancel事件,交给容器视图处理。子视图在move事件正常消费过程中,突然遭遇容器视图拦截,传递给子视图的事件改变为cancel事件,这次事件子视图的返回依然是true。所有链表元素将依次被回收。下一次move事件再次判断时,mFirstTouchTarget将是空,代码将不再经过这里,事件也不会向下传递。
如果是某个单独的Target元素,resetCancelNextUpFlag方法,同样设置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;
    }
    //desiredPointerIdBits是目标绑定是触控点
    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
    //未匹配成功
    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方法,根据所有手指,分离出目标中每一个子视图触控点集合,封装事件,然后发向每一个子视图,也就是说,只有一个手指move,两个子视图都会收到move事件。调试图。

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

手指触控点分析

先看一段代码。

final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);

有几个方法一定要知道。

getActionIndex方法,获取触控点索引,索引值与触控手指个数相关,从0开始,例如,三个手指触控索引是(0,1,2)。
getActionMasked方法,获取事件类型。down,up,move,cancel..事件。
getAction方法,存储事件类型和触控点信息,低8位是事件类型,其中前一个getActionMasked方法,相当于getAction & 0xff,高8位存储触控点信息,getActionIndex方法,相当于getAction & 0xff00。
getPointerId(int pointerIndex)方法,获取触控点id

触控点id不会改变,从手指触摸到离开的时间段,id都不会改变
触控点索引动态变化:actionIndex是触摸点索引,根据手指触控个数动态变化
假如有一个触控点,索引是0,id是0
新增一个触控点,索引是为0,1,id是0,1
此时,up第一个触控点,剩余触控点索引是0,id是1

总之:索引值与跟随触控手指个数变化,Id不变。
在上面代码中,首先,通过getActionIndex方法,获取触控点索引,然后,通过getPointerId方法,根据索引,获取触控点id,idBitsToRemove二进制值,当前触控点id值代表的那一位是1,其他位都是0,该值是不变的。
看一下前面删除触控点的removePointersFromTouchTargets方法。该方法主要针对多手指触摸的一些情况处理,删除早期的标志位。

private void removePointersFromTouchTargets(int pointerIdBits) {
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next;
        if ((target.pointerIdBits & pointerIdBits) != 0) {
            target.pointerIdBits &= ~pointerIdBits;
            //若不存在任何一位,释放TouchTarget。
            if (target.pointerIdBits == 0) {
                if (predecessor == null) {
                    mFirstTouchTarget = next;
                } else {
                    predecessor.next = next;
                }
                target.recycle();
                target = next;
                continue;
            }
        }
        predecessor = target;
        target = next;
    }
}

首先,该方法入参pointerIdBits代表二进制触控点id,id对应位是1。在TouchTargets链表中,根据TouchTarget中存储的pointerIdBits集合,查找触控点id的标志位,目的是将pointerIdBits中的触控点id标志位删除。
如果触控点pointerId依附在某一项TouchTarget,说明该pointId曾经被TouchTarget的视图处理,从而被加入到TouchTarget位集合pointerIdBits中。将TouchTarget的pointerIdBits中触控点pointerId位置清除。该位置0后,如果整个pointerIdBits变成0,说明在位集合中,仅存在该位,TouchTarget已无用,从链表中删除,recycle释放。
然后,该方法在dispatchTouchEvent中触发的位置。当判断不cancel不intercepted,事件类型down,pointer_down或hover_move时触发。这类情况是第一个手指触屏或新增一个手指触屏,要为该手指触控点pointerId重新分配TouchTarget,如果前面保存过TouchTarget存储着该触控点pointerId,将其删除,防止影响此次触摸。
在第五部分的最终处理时,判断pointer_up事件时触发。触控点pointerId的手指离开屏幕,pointer_up事件,此时仍有其他触控点,这种情况也需要删除触控点pointerId在TouchTarget的存储标志位,触控点pointerId离开,后续将不再影响视图。


叶子节点视图派发

如果视图是叶子节点,dispatchTouchEvent方法派发过程比较简单。下面看一下该方法。

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    ...
    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        stopNestedScroll();
    }
    if (onFilterTouchEventForSecurity(event)) {
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    if (actionMasked == MotionEvent.ACTION_UP ||
            actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
        stopNestedScroll();
    }
    return result;
}

普通叶子视图节点在事件处理时,OnTouchListener监听的优先级较高,优先调用监听的onTouch方法,如果未消费掉,将调用视图自己onTouchEvent方法。
注意,叶子节点不一定就是View类型,也有可能是没有子节点的ViewGroup类型。另外,如果一个ViewGroup的子视图未消费事件,也会调用它的基类View的dispatchTouchEvent方法,自己处理。
View的onTouchEvent方法,如果viewFlags标志支持CLICKABLE,LONG_CLICKABLE标志位,视图可点击,此时,该方法会消耗掉事件,例如,Button控件支持CLICKABLE标志,但是,TextView控件不支持,触摸它时不会消费事件,这和我们在开发中遇到的现象是一致的。
总之:一个视图自身是否消费事件,由监听器OnTouchListener和View的onTouchEvent方法共同决定。


总结

根据单个手指触控点,总结一下触摸行为流程。TouchTarget链表中仅有一个节点。
下面待修改TAG。

Down事件:触控点位于子视图且并子视图消费,创建一个TouchTarget,封装子视图与触控点pointerId。触控点未获得子视图消费,TouchTarget一直保持空,ViewGroup自己处理。
Move事件:TouchTarget是不空,说明Down已找到合适的处理目标,交给TouchTarget存储的子视图处理。若是空,则ViewGroup自己处理。

Down与Move事件流程图如下所示:

ViewGroup的dispatchTouchEvent流程_Move和Down.png

流程分析:

  • 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总归是没消化。

从上层看该ViewGroup和该ViewGroup看它的下层子视图,逻辑都是一样的。

  • 单个手指从触摸到离开屏幕产生的完整事件流ACTION_DOWN、一系列ACTION_MOVE、ACTION_UP。
  • 事件会经过Activity,然后进入Window,传入视图,当所有视图均不处理时,最后交给Activity的onTouchEvent处理。
  • 事件进入每个视图,均先交给dispatchTouchEvent方法分发,本视图或子视图消费事件,该方法将返回true。
  • 拦截,onInterceptTouchEvent方法,一旦拦截成功,即使前期事件有处理目标,也会将目标回收,后续事件将不会再触发拦截方法。普通视图没有拦截方法,直接进入onTouchEvent方法
  • 视图处理,onTouchEvent方法,拦截或子视图未消费事件,将调用它。它是当前视图及子视图的最后一道防线,若onTouchEvent未成功消费,便不会在上层为其保存目标。因此再无法向它传递了。
  • 一个视图Down事件成功处理,父视图为其创建目标,绑定视图,Move事件将直接传递给该视图处理(不拦截情况下)。Down事件未成功处理,将交给父视图onTouchEvent处理,其他事件再也不会传递到该视图。
  • 一个视图Down事件成功处理,父视图为其创建目标,绑定视图,不拦截情况下Move事件将直接传递给该视图,若Move事件未成功处理,此时,事件将无视图接手,包括视图的onTouchEvent方法,最终将交给Activity的onTouchEvent处理。

任重而道远

相关文章

网友评论

      本文标题:触屏事件派发源码分析

      本文链接:https://www.haomeiwen.com/subject/qbenrftx.html