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

从顶层视图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方法,开始在树形视图结构中传递。

综上所述
每个视图触摸事件入口是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)区域范围,在该范围内说明触摸点在该子视图内部。

当两个条件同时满足,成功找到派发子视图,接下来查找该子视图是否存在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目标,在链表尾部。合并图。

综上所述
容器视图派发,经历事件初始化,拦截判断,子视图遍历查找。
拦截判断,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事件。调试图。

如果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事件流程图如下所示:

流程分析:
- 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处理。
任重而道远
网友评论