Android ViewGroup 触摸屏事件派发机制和源码分析
Android 中不管是View 还是ViewGoup,触摸事件来的时候都是从dispatchTouchEvent开始的.其中dispatchTouchEvent()是View.java 的方法,ViewGroup 只是重写了这个方法.
看ViewGroup的dispatchTouchEvent() 之前最好先看View的dispatchTouchEvent().View 的逻辑简单些,而且ViewGroup的dispatchTouchEvent()最终也会调用View的dispatchTouchEvent().
以下分析基于Android L.
我们现在从ViewGroup的dispatchTouchEvent()开始分析.相关代码如下:
@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {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.if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {ev.setTargetAccessibilityFocus(false);}if (DBG_MOTION || DBG_TOUCH) {Xlog.d(TAG, "(ViewGroup)dispatchTouchEvent 1: ev = " + ev + ",mFirstTouchTarget = "+ mFirstTouchTarget + ",this = " + this);}boolean handled = false;if (onFilterTouchEventForSecurity(ev)) {//调用View.java判断当前View 没有被遮蔽final int action = ev.getAction();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.cancelAndClearTouchTargets(ev);resetTouchState();//mFirstTouchTarget设置为null;mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;}// Check for interception.final boolean intercepted;if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//true:不拦截;false:拦截if (!disallowIntercept) {//拦截intercepted = onInterceptTouchEvent(ev);// 默认返回false/// M : add log to help debuggingif (intercepted == true && DBG_TOUCH) {Xlog.d(TAG, "Touch event was intercepted event = " + ev+ ",this = " + this);}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.final boolean canceled = resetCancelNextUpFlag(this)|| actionMasked == MotionEvent.ACTION_CANCEL;//是否cancle事件// Update list of touch targets for pointer down, if needed.final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;//默认true,用于判断分发事件,作用是判断可以把事件分发到多个子View.if (DBG_MOTION) {Xlog.d(TAG, "(ViewGroup)dispatchTouchEvent 2: actionMasked = " + actionMasked+ ",intercepted = " + intercepted + ",canceled = " + canceled + ",split = "+ split + ",mChildrenCount = " + mChildrenCount + ",mFirstTouchTarget = "+ mFirstTouchTarget + ",this = " + this);}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 downfinal 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.removePointersFromTouchTargets(idBitsToAssign);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 = 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);// 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.if (childWithAccessibilityFocus != null) {if (childWithAccessibilityFocus != child) {continue;}childWithAccessibilityFocus = null;i = childrenCount - 1;}if (!canViewReceivePointerEvents(child)|| !isTransformedTouchPointInView(x, y, child, null)) {//! View.VISIBLE || ! is in viewev.setTargetAccessibilityFocus(false);if (DBG_MOTION) {Xlog.d(TAG, "(ViewGroup)dispatchTouchEvent continue 6: i = "+ i + ",count = " + childrenCount + ",child = " + child+ ",this = " + this);}continue;}newTouchTarget = getTouchTarget(child);//Down事件:由于之前的mFirstTouchTarget已经被设置为null,这里newTouchTarget 赋值结果也是nullif (DBG_MOTION) {Xlog.d(TAG, "(ViewGroup)dispatchTouchEvent to child 3: child = "+ child + ",childrenCount = " + childrenCount + ",i = " + i+ ",newTouchTarget = " + newTouchTarget+ ",idBitsToAssign = " + idBitsToAssign+ ",mFirstTouchTarget = " + mFirstTouchTarget+ ",this = " + this);}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);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//将事件传递给child view// Child wants to receive touch within its bounds.mLastTouchDownTime = ev.getDownTime();if (preorderedList != null) {// childIndex points into presorted list, find original indexfor (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;}// 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();}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;}}}// Dispatch to touch targets.if (mFirstTouchTarget == null) {//事件没有被消耗掉或者被拦截了if (DBG_MOTION) {Xlog.d(TAG, "(ViewGroup)dispatchTouchEvent mFirstTouchTarget = null,"+ " canceled = " + canceled + ",this = " + this);}// No touch targets so treat this as an ordinary view.handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);- //内部会转调super.dispatchTouchEvent也就是View的onTouchEvent(),
- //可以理解为:如果如果视图的child 都没有消耗掉这个事件,那么视图自己来调用onTouchEvent.
- //还有一种情况是被拦截了,也是会调用自己的onTouchEvent
} 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;if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {//ACTION_DWON执行这里handled = true;} else {//其他事件move/up执行这里final boolean cancelChild = resetCancelNextUpFlag(target.child)|| intercepted;if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {handled = true;}if (DBG_MOTION) {Xlog.d(TAG, "dispatchTouchEvent middle 5: cancelChild = " + cancelChild+ ",mFirstTouchTarget = " + mFirstTouchTarget + ",target = "+ target + ",predecessor = " + predecessor + ",next = " + next+ ",this = " + this);}if (cancelChild) {//child 通知父元素拦截后续事件if (predecessor == null) {mFirstTouchTarget = next;//此时next=null,所以mFirstTouchTarget=null,后续再由其他事件来就是执行的上面的if分支而不是现在的else分支} 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.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);}}if (DBG_MOTION) {Xlog.d(TAG, "(ViewGroup)dispatchTouchEvent end 4: handled = " + handled+ ",mFirstTouchTarget = " + mFirstTouchTarget + ",this = " + this);}if (!handled && mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);}return handled;}
上面这个方法有点长,我们先从ACTION_DWON事件开始分析.
在ACTION_DWON 事件处理之前,是一些关于手手势操作的处理,不是我们目前要关注的重点,接这再调用View.java判断当前View 没有被遮蔽,这些和View.java的dispatchTouchEvent()相似.
从24行就是ACTION_DWON的处理逻辑的开始.
其中cancelAndClearTouchTargets()主要是执行一个ACTION_CANCEL和设置mFirstTouchTarget为null.看注释就可以知道,执行ACTION_CANCEL是为了防止ANR和其他原因导致上一个触摸动作的ACTION_CANCEL或者ACTION_UP被丢失了.
resetTouchState():mFirstTouchTarget设置为null同时执行mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT.
总之ACTION_DWON 来了之后mFirstTouchTarget会被设置为null,而且mGroupFlags 已经被设置了不容许拦截的标志.
接着看34-48行代码,为了方便将这一小段重新贴出来.
//......................if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {//action=down的时候就可以通过了final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//true:不拦截;false:拦截if (!disallowIntercept) {intercepted = onInterceptTouchEvent(ev);// 默认返回falseev.setAction(action); // restore action in case it was changed} else {intercepted = false;}}//....................
由于之前执行了cancelAndClearTouchTargets()和resetTouchState(),mGroupFlags
& FLAG_DISALLOW_INTERCEPT就等于0,所以booelan disallowIntercept现在就是false,也就是说接下来会执行onInterceptTouchEvent().这个方法在ViewGroup里面默认是直接返回false的.所以变量intercepted的值由onInterceptTouchEvent()的返回值来决定.
上面又说到mGroupFlags 设置是否容许拦截标志位的问题,我们在源码中搜索会发现有requestDisallowInterceptTouchEvent(),这个方法是公开用来调用,决定后续事件来的时候父层的view是否可以调用它的onInterceptTouchEvent().
对于底层的View来说,有一种方法可以阻止父层的View截获touch事件,就是调用getParent().requestDisallowInterceptTouchEvent(true);方法。一旦底层View收到touch的action后调用这个方法那么父层View就不会再调用onInterceptTouchEvent了,也无法截获以后的action.
61行是判断是否ACTION_CANCLE事件的,并保持到boolean canceled变量中.
65行是判断是否可以分发事件,并保存到split变量中,作用是判断可以把事件分发到多个子View.这个同样在ViewGroup中提供了public的方法:public
void
setMotionEventSplittingEnabled(boolean
split)来设置.
75行if (!canceled && !intercepted) {,就是开始事件的分发处理.ACTION_DOWN事件执行到这里canceled = false,intercepted的值由
onInterceptTouchEvent(ev)来决定.
85行 if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE)
这个条件满足了才会将touch事件传递到子View,当然也要child view存在也就是mChildrenCount >0. 也就是133行的if (newTouchTarget == null && childrenCount != 0)是否可以通过,由于newTouchTarget 在ACTION_DOWN时是null,所以这个条件pass.接着会调用buildOrderedChildList()方法得到一个子View
的ArrayList 集合,然后从childrenCount - 1 到0开始循环查找那个View是View.VISIBIE状态或者在View视图的范围之内,然后调用dispatchTransformedTouchEvent来让child view处理事件.
135行调用getTouchTarget(child)来赋值newTouchTarget.当是Down事件时:由于之前的mFirstTouchTarget已经被设置为null,这里newTouchTarget 赋值结果也是null,所以181行的break跳出for循环暂时不会执行.
152行开始事件传递到child View,if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))这个if判断的结果由dispatchTransformedTouchEvent()来决定,这个方法代码很长就不贴出来,但是流程不是很复杂.
dispatchTransformedTouchEvent在这里它最终会调用child.dispatchTouchEvent(),并且将child.dispatchTouchEvent()的返回值作为dispatchTransformedTouchEvent()的返回值,也就是if()判断的条件值.显然dispatchTransformedTouchEvent就是递归调用dispatchTouchEvent()方法,如果递归传递到了一个View哪里,一般都会执行onTouchEvent(),并且onTouchEvent()的返回值就是dispatchTouchEvent()的返回值.(为什么是一般会执行需要查看View.java)
在这个if()通过之后会做如下3件事情:
a.调用addTouchTarget()来赋值newTouchTarget,同时mFirstTouchTarget也会被赋值,也就是说mFirstTouchTarget和newTouchTarget都不会是null了.
b.设置alreadyDispatchedToNewTouchTarget= true,这个变量和他的名字一样就是说已经将touch时间传递给新的目标view处理了,也就是消耗掉了.
c.跳出上面的for循环,因为一个touch事件只能被一个目标View消耗掉,也就是一个View的dispatchTouchEvent()返回true.
以上代码的分析其实都还是只有ACTION_DOWN才能执行的,后面192-247就是一个if-else,而且具体怎么走是有mFirstTouchTarget== null来判断.而mFirstTouchTarget 由前面调用dispatchTransformedTouchEvent
()返回true再来调用addTouchTarget()来设置值的.而前面的dispatchTransformedTouchEvent()返回值就是有递归调用的子View 的dispatchTouchEvent()返回boolean
参数来决定. dispatchTouchEvent()返回值一般和onTouchEvent()是一致的,也是由他决定的.所以dispatchTransformedTouchEvent()的返回值由onTouchEvent()决定.这也决定了后面其他move和up事件来的时候192-247行里面的具体怎么执行.
private TouchTarget addTouchTarget(View child, int pointerIdBits) {TouchTarget target = TouchTarget.obtain(child, pointerIdBits);target.next = mFirstTouchTarget;mFirstTouchTarget = target;return target;}
注意上面提到的addTouchTarget()在第一次执行的时候mFirstTouchTarget还是null,所以target.next 也会是null,但是target.child不会是null.这个在子视图调用getParent().requestDisallowInterceptTouchEvent(false)来让父元素拦截后续事件的时候很重要.
192-247行:
如果mFirstTouchTarget ==null,说明ACTION_DOWN没有被消耗掉或者被拦截掉了.接着执行dispatchTransformedTouchEvent(),这个方法的第3个参数是null,最终结果就是在方法内部执行的时候是会调用super.dispatchTouchEvent(),也就是说不会继续传递给子View了.其中super.dispatchTouchEvent()也就是View的dispatchTouchEvent(),这个方法内部会执行onTouchEvent()方法.(可以参考View的触摸屏事件派发机制和分析.)
如果mFirstTouchTarget !=null,执行后面的while循环.
206行:
这个while循环内部有一个if-else来处理,
其中if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) 只有Down事件才会执行这里,因为alreadyDispatchedToNewTouchTarget 在这个方法的内部开头是false,只有在169行才能设置为true,169行只有是Down相关的事件才能走的流程,168行的addTouchTarget会使 target
== newTouchTarget为true.这个分支就是表示事件已经被子View消耗掉了,ViewGoup自己接下来什么都没有了,直接return true,等后面的move/up事件
而else分支就是其他事件move/up才会走的流程.其实也是执行dispatchTransformedTouchEvent()方法,只是参数不一样.
上面反复提到dispatchTransformedTouchEvent(),这里单独分析一下.
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {final boolean handled;// Canceling motions is a special case. We don't need to perform any transformations// or filtering. The important part is the action, not the contents.final int oldAction = event.getAction();if (DBG_MOTION) {Xlog.d(TAG, "dispatchTransformedTouchEvent 1: event = " + event + ",cancel = "+ cancel + ",oldAction = " + oldAction + ",desiredPointerIdBits = "+ desiredPointerIdBits + ",mFirstTouchTarget = " + mFirstTouchTarget+ ",child = " + child + ",this = " + this);}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);if (DBG_MOTION) {Xlog.d(TAG, "Dispatch cancel action end: handled = " + handled + ",oldAction = "+ oldAction + ",child = " + child + ",this = " + this);}return handled;}// Calculate the number of pointers to deliver.final int oldPointerIdBits = event.getPointerIdBits();final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;// If for some reason we ended up in an inconsistent state where it looks like we// might produce a motion event with no pointers in it, then drop the event.if (newPointerIdBits == 0) {Xlog.i(TAG, "Dispatch transformed touch event without pointers in " + this);return false;}// If the number of pointers is the same and we don't need to perform any fancy// irreversible transformations, then we can reuse the motion event for this// dispatch as long as we are careful to revert any changes we make.// Otherwise we need to make a copy.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);}if (DBG_MOTION) {Xlog.d(TAG, "dispatchTransformedTouchEvent 2 to child " + child+ ",handled = " + handled + ",mScrollX = " + mScrollX + ",mScrollY = "+ mScrollY + ",mFirstTouchTarget = " + mFirstTouchTarget + ",event = "+ event + ",this = " + this);}return handled;}transformedEvent = MotionEvent.obtain(event);} else {transformedEvent = event.split(newPointerIdBits);}// Perform any necessary transformations and dispatch.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);}if (DBG_MOTION) {Xlog.d(TAG, "dispatchTransformedTouchEvent 3 to child " + child + ",handled = "+ handled + ",mScrollX = " + mScrollX + ",mScrollY = " + mScrollY+ ",mFirstTouchTarget = " + mFirstTouchTarget + ",transformedEvent = "+ transformedEvent + ",this = " + this);}// Done.transformedEvent.recycle();return handled;}
其实逻辑很简单,如果第2个参数cancel为true,就是表示这个事件是ACTION_CANCEL,起码意见被当成是ACTION_CANCEL,接着判断第3个参数child是否为null,child为null,就调用父类的dispatchTouchEvent,也就是执行super. dispatchTouchEvent().如果3个参数child不是为null,就调用child. dispatchTouchEvent(),并且这个dispatchTouchEvent()的返回值就是dispatchTransformedTouchEvent()的返回值.如果第2个参数cancel不是null,还是会一样判断3个参数child是否为null,后面的流程都是和前面一样.
到这里,ViewGroup 触摸屏事件派发流程就算是结束了.
总结一下:
1.触摸屏事件是先传递到最顶层的ViewGroup(也就是最parent)的dispatchTouchEvent()开始处理的,再传递到子View(child view)的dispatchTouchEvent().
2.如果ViewGroup的onInterceptTouchEvent()拦截了事件也就是返回true,那么后续的move, up等事件将不再传递给onInterceptTouchEvent(),而是和down事件一样传递给该ViewGroup的onTouchEvent()处理.不会再传递给子View.
如果ViewGroup的onInterceptTouchEvent()返回false,就是不拦截,后续的move,up和down事件一样传递给最终的目标View的onTouchEvent()来处理.
3.如果子View将事件消耗掉了,ViewGroup自己就不能处理了,只能等后续的move/up事件了.
4.某个view 一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一个事件序列中的其他事件就不会再交给他来处理,并将事件提交给父元素去处理(关键是mFirstTouchTarget).这里可以理解为如果ViewGroup的child 都没有消耗掉事件就是执行ViewGroup 的super()也就是View来执行.
5.如果View不消耗除ACTION_Down以外的其他事件(up/move),那么这个点击事件就会消失,此时父元素的onTouchEvent不会执行,并且当前view可以继续接受后续事件,最终这些消失的点击事件会传递给activity处理.
6.子View想要禁止父View的拦截事件只要调用getParent().requestDisallowInterceptTouchEvent(true)方法.一旦底层View收到touch的action后调用这个方法那么父层View就不会再调用onInterceptTouchEvent了,也无法截获以后的action.
本文详细解析了Android中ViewGroup的触摸事件分发机制,包括dispatchTouchEvent方法的执行流程、ACTION_DOWN事件的处理逻辑、onInterceptTouchEvent方法的作用及如何通过requestDisallowInterceptTouchEvent方法阻止父View拦截事件。
560

被折叠的 条评论
为什么被折叠?



