Android事件处理(一)——ViewGroup的dispatchTouchEvent 函数源码详解
作者: 林子木
博客地址:http://blog.youkuaiyun.com/wolinxuebin
文章意图:
主要是想一自己阅读代码后的一些小收获分享给大家。让大家更加深入的了解Android的事件分发这块的内容。
文章主要内容:
本文将在第一章通过自己的语言,简单介绍dispatchTouchEvent,并将其中的一些关键点直接提炼出来,方便那些不想阅读源代码的同学把握住其中的关键点。
在第二章将放上源码,其中包含了我阅读过程中的26处注释。
第一章、dispatchTouchEvent关键点提炼
ViewGroup的dispatchTouchEvent是对View的dispatchTouchEvent函数的重新,两者具有很大的区别。如下表:
View的dispatchTouchEvent函数 ViewGroup的dispatchTouchEvent函数 1)是将Event派发给自己的onTouchEvent函数处理。 1)将Event派发给子View
2)只有在子View没有相应该事件或ViewGroup拦截了该事件的时候,才通过View的dispatchTouchEvent将事件派发给自身。
有上面两个表,应该大致了解了ViewGroup的dispatchTouchEvent大致讲上面了吧。那现在具体讲讲做了哪些事:Step 1: 如果收到的ACTION_DOWN 那么清除所有和时间相关的状态Step 2: 判断是否拦截事件
1) 如果在子view中调用了getParent().requestDisallowInterceptTouchEvent(true)那么将不会拦截该事件。问:有什么用呢?答:在listView或其他srcollView都会在onInterceptTouchEvent进行拦截判断,只要距离或者速度达到一定要求,就会拦截该事件。那就做不到一view在listView中上下滑动。那么如何才能实现这个需求?那就需要使用getParent().requestDisallowInterceptTouchEvent(true)不要拦截该事件,将事件传递下去。2)onInterceptTouchEvent() 函数这是一个很重要的函数,是在当前的事件达到一定要求之后才进行拦截处理。有了这个,listView才能实现item可点击,而item上下滑动。Step 3: 如果事件是ACTION_DOWN ,并且没有被拦截,将会执行向子View的派发处理。
1)派发顺序
5.0之前,基本按照子View被添加的顺序
5.0之后,还需要考虑Z轴、Drawing顺序、添加的顺序
重点关注Z轴顺序,值最大的将会最优先,这是得到所有的子view,那么如何判断是否点击到子View区域内呢?
2)判断子View是否可见或者存在动画,否则不处理
3)事件是否点击到子View的区域内
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempPoint();
point[0] = x;
point[1] = y;
//[lxb] 如果view做了相应的变换,那么将point也做相应的变换
transformPointToViewLocal(point, child);
//[lxb] 直接将pointInView函数拿出
// return LocalX >= 0 && localX < (mRight - mLeft)
// && localY >= 0 && localY < (mBottom - mTop);
// 经过变换之后的point,则已经将左上角作为原点,所以只要判断是否在这个矩形中就可以了
// 所以这就是为什么如TranslateAnimation的原始点击点还是在最开始的地方
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
这也就是为什么补间动画尽管显示的View已经移动了另一个地方,但是点击区域还是在最原始的地方。4)派发该事件给子View,看子View是否要处理
注意,parent将event该子view的时候做了相应的偏移处理,所以子view中的event.getX() 和 parent中的不一样。
具体如下:
<span style="font-size:12px;"><span style="white-space: pre;"> </span>//[lxb] 将event做相应的偏移之后,然后传递给相应的子view
<span style="white-space: pre;"> </span>final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);</span>
5)将相应了该事件的子view加入到mFirstTouchTarget的单链表中(接下来的事件都会直接派发给其中的view)
Step 4: 如果没有一个子View相应Action_DOWN 怎么办?
那么直接将事件交给父类View的dispatchTouchEvent,再根据条件派发给自己。
Step 5: 其他事件如 ACTION_MOVE, ACTION_UP, ACTION_CANCEL则直接照着mFirstTouchTarget链表派发就行。
最后:
讲讲拦截,如果ViewGrop 拦截该事件,那么将立马给之前相应事件的子View派发一个ACTION_CANCEL事件。
另外,还需要注意的是,如果ViewGrop 拦截该事件,并不会立马将该事件给自己的onTouchEvent,只有等下次事件过来才可能,所以在onInterceptTouchEvent中需要对ACTION_UP和ACTION_CANCEL也做相应处理。
第二章、dispatchTouchEvent源码及分析
<span style="font-family:Microsoft YaHei;"> public boolean dispatchTouchEvent(MotionEvent ev) {
//[lxb #1] 调试使用,请忽略
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
//[lxb #2] [辅助功能] 事件将会第一个派发给开启了accessibility focused的view
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
//[lxb #3] 表示窗口是否为模糊窗口(FILTER_TOUCHES_WHEN_OBSCURED),如果是窗口,则表示不希望处理改事件。(如dialog后的窗口)
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
//[lxb #4] 过滤字段的最后8bit,也就是指只关心是ACTION_DOWN、ACTION_UP等事件,而不关心是哪个手指引起的。
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
//[lxb #5] 初始化相关状态
//[lxb #5] (01) 清空mFirstTouchTarget链表,并设置mFirstTouchTarget为null。mFirstTouchTarget是"接受触摸事件的View"所组成的单链表
//[lxb #5] (02) 清空mGroupFlags的FLAG_DISALLOW_INTERCEPT标记,如果设置了FLAG_DISALLOW_INTERCEPT,ViewGroup对触摸事件进行拦截。
//[lxb #5] (03) 清空mPrivateFlags的PFLAG_CANCEL_NEXT_UP_EVEN标记,作用是将下一个时间变我Cancel
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// Check for interception.
final boolean intercepted;
//[lxb #6] 如果为DOWN事件,或者mFirstTouchTarget为null(那么事件直接给到自己),就没必要执行拦截。
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//[lxb #7] 查看是否设置了,禁止拦截的标记
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//[lxb #8] ViewGroup的onInterceptTouchEvent不执行拦截,除非子类重写了该方法(如listview)
intercepted = onInterceptTouchEvent(ev);
//[lxb #9] 仅仅是避免action被篡改过。
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// Check for cancelation.
//[lxb #10] 查看时候被标记了PFLAG_CANCEL_NEXT_UP_EVENT 或者 当前是一个Cancel事件
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
//[lxb #11] 比如我们多个手指放到了屏幕上,是否要将第二个手指的事件下面下去
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// If the event is targeting accessiiblity focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
//[lxb #12] 清除Targets中相应的pointer ids
removePointersFromTouchTargets(idBitsToAssign);
//[lxb #13] 遍历所有的child,将事件派发下去
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.
//[lxb #13] 以 1)Z轴(5.0系统引入) 2)draw的顺序 进行排序
final ArrayList<View> preorderedList = buildOrderedChildList();
//[lxb #14] 可以理解为,是否按照draw的顺序(因为,buildOrderedChildList在都没有设置Z的情况下返回null)
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
//[lxb #15] 这里两端代码,简单的理解根据不同的排列选项(1、view添加到 2、view的draw顺序 3、viewZ 轴顺序)
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
//[lxb #16] 如果存在开启了AccessibilityFocus 的view
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
//[lxb #17] 如果正是当前的childView开启了AccessibilityFocus,直接将i指向最后一个元素
//[lxb #17] 和 break的区别是,还将执行后面的代码,但是不会再进行循环了
i = childrenCount - 1;
}
//[lxb #18] canViewReceivePointerEvents 判断child是否为visiable 或者 是否有动画
//[lxb #18] isTransformedTouchPointInView 判断x, y是否在view的区域内(如果是执行了补间动画 则x,y会通过获取的matrix变换值
//[lxb #18] 换算当相应的区域,这也是为什么补间动画的触发区域不随着动画而改变)
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//[lxb #19] getTouchTarget 查找child是否已经记录在mFirstTouchTarget这个单链表中
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//[lxb #20] 简单的理解,dispatchTransformedTouchEvent就是将相应的事件传递下去
//[lxb #20] 不过需要注意一点的就是,event被传递给child的时候将会做相应偏移,如下
//[lxb #20] final float offsetX = mScrollX - child.mLeft;
//[lxb #20] final float offsetY = mScrollY - child.mTop;
//[lxb #20] event.offsetLocation(offsetX, offsetY);
//[lxb #20] 为什么要做偏移呢? 因为event的getX得到的值是,childView到parentView边境的距离,是一个相对值
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
//[lxb #21] 找到childIndex所代表的child的最原始的index【?】看代码,children和mChildren指向同一链表
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
//[lxb #22] 将相应该事件的child包装成一个Target,添加到mFirstTouchTarget的链表中
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
//[lxb #22] 如果没有child相应该事件,则将此事件交给最近加入的target
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
//[lxb #23] mFirstTouchTarget == null 表示,没有能相应该事件的child,那么就调用父类(也就是View)的dispatchTouchEvent
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
//[lxb #24] 表示在Down事件处理中,已经将这个事件交给newTouchTarget处理过了,就不重复处理了
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
//[lxb #25] 再次判定是否需要cancel(被标记为cancel 或者 事件被拦截)
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
//[lxb #26] 问:难道看到这里你们会不会产生一个疑问,如果parent在ACTION_MOVE过程中拦截了该事件,哪里hi处理呢?
//[lxb #26] 答:如果拦截了该事件,还是需要自身 dispatchTransformedTouchEvent 函数将事件交个自己的onTouchEvent
//[lxb #26] 此外dispatchTransformedTouchEvent完成上述操作需要一个条件,也就是child形参数为null
//[lxb #26] 问:那么怎么为null呢?
//[lxb #26] 答:往上面看10几行,不就是了吗?
//[lxb #26] 那么如何达到,其实条件就是 mFirstTouchTarget == null, 请看下面的分析
//[lxb #26] 如果intercepted == true的情况下, cancelChild == true, predecessor == null
//[lxb #26] 从而使得mFirstTouchTarget 一直 -> next,当target遍历到最后的时候,next == null,从而使得mFirstTouchTarget == null。
//[lxb #26] 问: 稍等,这里仅仅做了将mFirstTouchTarget 设置了为null,那么如何派发给自己的onTouchEvent呢?
//[lxb #26] 这个只能等下一个事件过来了
//[lxb #26] 结论【事件拦截,拦截了该事件,并没有将本次这个事件传递给自身的onTouchEvent,而需要等到下次】
//[lxb #26] 问:如何验证
//[lxb #26] 答:重新FrameLayout的 onInterceptTouchEvent 和 onTouchEvent 将相应的event.getEventTime打印出来,
//[lxb #26] 将会发现拦截的事件和传递到onTouchEvent的时间不是一个时间。
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
// Update list of touch targets for pointer up or cancel, if needed.
//[lxb #27] cancel ACTION_UP ACTION_HOVER_MOVE(表示鼠标滑动)等,清理状态
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);
}
}
//[lxb #28] 调试使用,可以忽略
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}</span>