再论Android的事件分发机制
Touch事件分发中只有两个主角:ViewGroup和View。Activity的Touch事件事实上是调用它内部的ViewGroup的Touch事件,可以直接当成ViewGroup处理。
View在ViewGroup内,ViewGroup也可以在其他ViewGroup内,这时候把内部的ViewGroup当成View来分析。
ViewGroup的相关事件有三个:onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent。View的相关事件只有两个:dispatchTouchEvent、onTouchEvent。
接下来我以ViewGroup的三个方法为例
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// 是否需要拦截.需要就调用onInterceptTouchEvent(flag标记位决定)
// 注意FLAG_DISALLOW_INTERCEPT这个flag,可以通过requestDisallowInterceptTouchEvent来设置
// 当标志位设置之后ViewGroup将无法拦截除了ACTION_DOWN以外的事件了
// mFirstTouchTarget 是什么?后面的代码表示,当ViewGroup的点击事件被子View消耗,那mFirstTouchTarget就会指向该子View
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
// 是否被取消
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
if (!canceled && !intercepted) {
// 如果没有取消也没有拦截,那就让子view处理
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
......
会调用dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
}
}
}
// 子view未处理
// 可能有两种情况:一、ViewGroup下面没有子View。二、子View没有消耗点击事件。
if (mFirstTouchTarget == null) {
// child=null,永远调用super.dispatchTouchEvent
// handled = super.dispatchTouchEvent
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;
// alreadyDispatchedToNewTouchTarget在循环遍历的时候如果找到,会置为true
// handled = true表示被子view消费掉
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
// handled = true表示被子view消费掉
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
}
return handled;
}
注意其中的一个很重要的参数mFirstTouchTarget。这是个链表 (找到子view,并且消费才会添加到链表中)
再来看一下dispatchTransformedTouchEvent
// 子ciew存在就让子view消费,不存在super.dispatchTouchEvent(event)
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// 循环遍历子view时,cancel=false.当child不存在时交给父view,否则交给子view
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
......
return handled;
}
总结
事件向下传递,假设被拦截(一般不会被拦截,设置了FLAG_DISALLOW_INTERCEPT表示可以拦截),此时mFirstTouchTarget == null 调用自己的onInterceptTouchEvent,处理完了return super.dispatchTouchEvent 往上抛
现在来看一般情况,没被拦截,大致情况如下图
当找到mFirstTouchTarget时.png
会一直往下找,如果找到mFirstTouchTarget,也就是上图最底下的View,那么就会在View的dispatchTouchEvent返回true表示消费。如果没有找到,说明要么没有子view,要么没有消费,都往上抛,也就是super.dispatchTouchEvent
扩展:
还记得ACTION_CANCEL么?假如点击了以后滑动到View的外面,响应了ACTION_CANCEL了呢?看dispatchTouchEvent函数,那么很明显canceled=true,也不用遍历子view,直接返回mFirstTouchTarget=null,但此时中间的ViewGroup的mFirstTouchTarget不为null啊,那么所有的滑动事件都不会再传递到最底层的View了