ViewGroup 的事件分发核心
我们知道,View
是 Android 的最基本控件,不能再细分。而ViewGroup
继承于 View,可以包含多个 View。手指触碰屏幕时,触摸事件可能由 ViewGroup 拦截处理了,也可能传递给 ViewGroup 内部的 Child View 去处理。
ViewGroup 的事件分发核心就是方法 dispatchTouchEvent(MotionEvent)
,主要分为几步:
@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) { // 1. 如果是 Down 事件,处理初始化,重置各种状态; // 2. 检查是否拦截了事件 // 3. 检查是否取消了事件 // 4. 根据 intercepted 和 canceled 决定是否分发事件给 Child View // 5. 根据 mFirstTouchTarget 再次分发事件}
步骤 1
// 处理初始化的 Down 事件。if (actionMasked == MotionEvent.ACTION_DOWN) { /* * 当开始新的手势时,放弃所有之前的状态。 * 框架可能由于应用程序切换,ANR 或其他一些状态更改而丢失了上一个手势的抬起或取消事件。 */ cancelAndClearTouchTargets(ev); resetTouchState();}
步骤 1 说明事件分发的起始就是 Down 事件。当接收到 Down 事件,说明开始了一次新的触摸事件分发。
步骤 2
// 拦截检查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); // 重新恢复 action 以防发生了改变 } else { intercepted = false; }} else { // 没有触摸目标并且不是初始化的 Down 事件,当前 ViewGroup 继续拦截触摸事件。 intercepted = true;}
步骤 2 决定了 ViewGroup 是否需要拦截事件,如果拦截那么 Child View 不会接收到此次事件或者接收到 Cancel 事件。
主动判断是否拦截的条件有两个:
- 当前事件是否为 Down 事件。如果是 Down 事件则进行第一次拦截判断;
mFirstTouchTarget
是否为空。不为空说明上一次事件有 Child View 捕获,再一次进行拦截判断。
被动判断是否拦截的条件有一个:
disallowIntercept
是否为true
。Child View 可以通过调用 ViewGroup
的方法requestDisallowInterceptTouchEvent(boolean)
来控制 ViewGroup 是否拦截 Child View 的事件。当该变量为true
,ViewGroup 不拦截 Child View 的事件。
步骤 3
// 取消检查final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
步骤 3 判断当前事件是否为取消,影响后面对 Child View 事件的分发。
步骤 4
TouchTarget newTouchTarget = null;boolean alreadyDispatchedToNewTouchTarget = false;if (!canceled && !intercepted) { ··· if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { ··· final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { ··· 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); ··· if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, chi ev.setTargetAccessibilityFocus(false); continue; } ··· if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { ··· newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } ··· } ··· } ··· }}
步骤 4 通过canceled
和intercepted
判断到如果不取消、不拦截当前事件,并且当前事件为 Down 事件(这里还有多指等其他情形,暂不讨论),对所有 Child View 进行遍历,找到位置处于事件范围内并且不处于动画状态的 Child View,调用dispatchTransformedTouchEvent(MotionEvent, boolean, View, int)
方法将事件分发给 Child View,如果该方法返回true
说明 Child View 处理了事件,在addTouchTarget(View, int)
方法内部赋值mFirstTouchTarget
。
步骤 5
// Dispatch to touch targets.if (mFirstTouchTarget == null) { // 没有触摸目标,把当前 ViewGroup 当做一个 View handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);} else { // 分发事件给触摸目标,如果已经分发过给 newTouchTarget,则排除它。必要时则取消触摸目标。 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; if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } ··· } ··· }}
步骤 5 里判断到如果mFirstTouchTarget
为空,说明没有 Child View 处理事件,则把 ViewGroup 当做一个普通的 View,把事件分发给自身。否则将当前以及接下来的事件分发给mFirstTouchTarget
指向的 Child View 处理。handled
是dispatchTouchEvent(MotionEvent)
的返回值,说明当前 View 处理了该事件。
更多相关文章
- Android四种菜单的使用
- Android(安卓)VelocityTracker简介
- Android学习笔记,Notification通知事件
- Android(安卓)Widget ListView添加点击事件
- 【Android】 onClick与onTouch并存触发的问题
- 【android学习】getevent和sendevent
- Android(安卓)studio 任意修改项目包名(含com)
- android RecyclerView响应点击事件
- Android使用ListView构造复杂界面,响应点击事件,通过Intent跳转act