ViewGroup事件分发机制解析
最近在看View的事件分发机制,感觉比复杂的地方就是ViewGrop的dispatchTouchEvent函数,便对照着源码研究了一下。故名思意这个函数起到的作用就是分发事件,在具体分析之前还要说明几个相关的知识。
- 事件序列指的是从手指接触屏幕那一刻起,到手指离开屏幕那一刻为止产生的所有事件。
- 一旦View消耗了某个事件,那么同一事件序列内的所有事件都会交给它处理。
- ViewGroup默认不拦截任何事件。
- 事件分发过程中ViewGroup会考虑多点触控的问题,例如在一个布局中有两个子控件,如果两个手指同时对它们进行操作,控件是可以正常响应的。
现在开始分析该函数的执行过程。(省略了一些不太重要代码)
1 public boolean dispatchTouchEvent(MotionEvent ev) { 2 boolean handled = false; 3 final int action = ev.getAction(); 4 // 去除提供触点信息的pointerIndex字段,可以参考MotionEvent.ACTION_POINTER_INDEX_MASK字段的注释。 5 final int actionMasked = action & MotionEvent.ACTION_MASK; 6 if (actionMasked == MotionEvent.ACTION_DOWN) { 7 // ACTION_DOWN标志着一个事件序列的产生,此时需要进行一些复位操作。 8 cancelAndClearTouchTargets(ev); 9 resetTouchState(); 10 }
首先当收到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.
新的事件序列产生时需要对状态进行复位,因为在一些情况下系统可能会丢弃标志事件序列结束的ACTION_UP或ACTION_CANCEL事件。
1 // 标志自身是否拦截此事件。 2 final boolean intercepted; 3 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { 4 // 当子View调用requestDisallowInterceptTouchEvent函数时该变量为true。 5 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; 6 if (!disallowIntercept) { 7 // 如果子View没有禁止父View拦截事件,父View通过该函数判断是否需要拦截此事件。 8 intercepted = onInterceptTouchEvent(ev); 9 ev.setAction(action); 10 } else { 11 intercepted = false; 12 } 13 } else { 14 // 一定拦截事条件:事件类型不为ACTION_DOWN并且mFirstTouchTarget为null。 15 // 这说明ACTION_DOWN事件已经被自身消耗,那么该事件序列中的剩余事件也应该被自身消耗。 16 intercepted = true; 17 }
接下来是判断ViewGroup是否需要对事件进行拦截。判断拦截条件时需要对成员变量mFirstTouchTarget判空,它是一个链表结构,在一个事件序列中,每当有子View消耗了某个事件,那该View就会被添加到该链表中。此处会与之前的说明有一些歧义,“一旦View消耗了某个事件,那么同一事件序列内的所有事件都会交给它处理”。但是,在事件序列中存在一个比较特殊的事件类型——ACTION_POINTER_DOWN事件,代表有新手指触摸了屏幕,这时新的触点产生的事件可以被另外一个View消耗。在这段代码中还可以看出,即使有子View消耗了一个事件序列中的某些事件(mFirstTouchTarget不为空),父View还是可以拦截之后的事件。
1 // 判断该事件类型是否应该为ACTION_CANCEL。 2 final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; 3 // 判断一个事件是否可以根据触点的不同向多个View分发,只有这个条件成立时mFirstTouchTarget链表的大小才会大于1。 4 // 换句话说当条件不成立时多个手指是无法同时操作多个控件的。 5 final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; 6 TouchTarget newTouchTarget = null; 7 // 标志是否有新的子View(不在mFirstTouchTarget链表中)消耗了事件。 8 boolean alreadyDispatchedToNewTouchTarget = false; 9 // 向子View中分发事件的条件:事件类型不为ACTION_CANCEL,ViewGroup本身没有拦截, 10 // 事件类型为ACTION_DOWN或者当允许同时操作多个控件时有新手指接触屏幕。 11 if (!canceled && !intercepted) { 12 if (actionMasked == MotionEvent.ACTION_DOWN 13 || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)) {
这段代码主要是判断是否需要向子View中分发事件,可以看到当只有允许多个手指同时操作多个控件时,ACTION_POINTER_DOWN类型的事件才会寻找新的子View进行分发,相当于ACTION_POINTER_DOWN类型的事件产生了一个新的事件序列。
1 // 下边的三行代码比较难理解,在多个手指同时操作对多个控件时起作用。 2 final int actionIndex = ev.getActionIndex(); 3 // 首先是变量idBitsToAssign,它代表一个bit集合,每个位都与触点Id对应(最大为31)。 4 // 它的含义这个新的事件序列是由哪个触点产生的。如果不允许多个手指同时操作多个控件,那么 5 // 它的值为0xFFFFFFFF,即多点触控只会影响一个控件且最多可接收32个触点产生的事件。 6 final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; 7 // 如果mFirstTouchTarget链表中已有某个View消耗的事件对应的触点Id存在于该集合中, 8 // 那么该View将不会再收到此触点Id对应的事件,如果该View只接收此触点Id对应的事件,那么该View会被从链表中移除。 9 removePointersFromTouchTargets(idBitsToAssign); 10 11 final int childrenCount = mChildrenCount; 12 if (newTouchTarget == null && childrenCount != 0) { 13 //获取当前触点的坐标,多点触控时为刚按下的手指对应的触点坐标。 14 final float x = ev.getX(actionIndex); 15 final float y = ev.getY(actionIndex); 16 View[] children = mChildren; 17 //遍历子View 18 for (int i = childrenCount - 1; i >= 0; i--) { 19 int childIndex = i; 20 final View child = children[childIndex] ; 21 // 当前View能够接收事件需具备两个条件: 22 // 1、View可见或正在进行动画效果。 23 // 2、触点坐标在View内部。 24 if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { 25 continue; 26 }
这段代码主要是对子View进行遍历,直到找到满足事件接收条件的子View。
1 // 满足事件接收条件就会向子View分发事件,在该函数内部会使用函数MotionEvent.split先从原始的事件对象 2 // 中分离出此触点Id集合中触点所关联的信息。返回true说明子View已消耗了该事件。 3 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { 4 mLastTouchDownIndex = childIndex; 5 mLastTouchDownX = ev.getX(); 6 mLastTouchDownY = ev.getY(); 7 // 将该View与触点Id集合包装后添加到mFirstTouchTarget链表的队首。 8 newTouchTarget = addTouchTarget(child, idBitsToAssign); 9 alreadyDispatchedToNewTouchTarget = true; 10 // 一旦有子View消耗了事件,立即停止遍历。 11 break; 12 } 13 } 14 } 15 // 当下面的判断成立时代表此事件为ACTION_POINTER_DOWN并且没有子View消耗此事件。 16 if (newTouchTarget == null && mFirstTouchTarget != null) { 17 // 得到消耗第一个触点参数的事件的子View。 18 newTouchTarget = mFirstTouchTarget; 19 while (newTouchTarget.next != null) { 20 newTouchTarget = newTouchTarget.next; 21 } // 将新的触点Id添加到其触点Id集合中。 22 newTouchTarget.pointerIdBits |= idBitsToAssign; 23 } 24 } 25 }
当找到符合条件的View后,使用函数dispatchTransformedTouchEvent进行事件分发,实质是调用View的dispatchTouchEvent方法。如果有子View消耗了事件,那么遍历停止,也就是说其余的子View不再有机会接收到此事件序列中的事件。
1 if (mFirstTouchTarget == null) { 2 // 如果mFirstTouchTarget为null,代表没有子View可消耗该事件,事件交由本身处理。 3 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); 4 } else { 5 TouchTarget predecessor = null; 6 TouchTarget target = mFirstTouchTarget; 7 while (target != null) { 8 final TouchTarget next = target.next; 9 if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { 10 // 该节点中的View之前已消耗了此事件。 11 handled = true; 12 } else { 13 // 如果该节点中的View将要从父View中移除或父View需要拦截此事件则向该View发送 14 // 一个ACTION_CANCEL类型的事件。否则从此事件中剥离出该View对应的触点产生的信息并向其发送。 15 final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; 16 if (dispatchTransformedTouchEvent(ev, cancelChild, 17 target.child, target.pointerIdBits)) { 18 handled = true; 19 } 20 // 如果一个子View正在接收一个事件序列,里边的事件被父View拦截后,子View无法收这个序列中剩余的事件。 21 if (cancelChild) { 22 // 将此节点移出链表。 23 if (predecessor == null) { 24 mFirstTouchTarget = next; 25 } else { 26 predecessor.next = next; 27 } 28 target.recycle(); 29 target = next; 30 continue; 31 } 32 } 33 predecessor = target; 34 target = next; 35 } 36 }
本段代码主要有这么几个功能。第一,如果没有子View消耗当前事件,ViewGroup会把事件交给自身。第二,向已将消耗事件的子View继续分发该事件序列中剩余的事件。第三,在某些情况下需要禁止子View接收当前事件序列中剩余的事件。
1 if (canceled || actionMasked == MotionEvent.ACTION_UP) { 2 // 当遇到ACTION_UP类型的事件时标志着一个事件序列的结束,此时需要重置mFirstTouchTarget链表。 3 resetTouchState(); 4 } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) { 5 // 如果有手指离开屏幕(屏幕上还有其它手指),需要查看mFirstTouchTarget链表,移除该触点Id。 6 final int actionIndex = ev.getActionIndex(); 7 final int idBitsToRemove = 1 << ev.getPointerId(actionIndex); 8 removePointersFromTouchTargets(idBitsToRemove); 9 }
10}
最后,这个函数需要处理标志事件序列结束的ACTION_UP类型的事件。如果有手指离开屏幕时(还有手指在上边)并不能算是事件序列的结束,而是要从mFirstTouchTarget链表中移除接收这个手指产生的事件的View。
最后还要说明一个知识点,就是一个MotionEvent对象中会记录多个触点产生的时间,比如两个手指同时按在屏幕上,如果这两个事件分别落在了ViewGroup的两个子View上,那ViewGoup就需要对这个事件进行分解,让每个触点产生的事件信息分发到对应的View上。事件根据触点Id进行分解的代码在ViewGroup的dispatchTransformedTouchEvent函数中,有兴趣的朋友可以自己看一下。