触屏是用户和手机交互的基础,手指触屏时产生一系列事件,控制视图改变,在树形视图中,事件从顶层向下传递。
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)区域范围,在该范围内说明触摸点在该子视图内部。
两个条件同时满足,找到子视图。遍历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,三个目标视图,四个触控点,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自己处理。
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处理。
任重而道远