彻底弄清事件分发流程之ViewGroup源码详细分析
背景:
在android开发中,经常会遇到触摸事件的分发处理,事件冲突,事件消费,如果界面比较复杂,一旦出现问题,如果对事件的分发处理机制不了解的话,这将使得我们难以处理,不知道从何处着手处理,更不知道该怎么去修改。但是如果我们对事件的分发处理机制非常熟悉,那么处理这些冲突或者由于事件消费问题引起的bug,我们就能很好的处理,并且毫不费力。
举个栗子:比如你在一个scrollView里面嵌套了一个RecyclerView然后在RecyclerView里面,你又放了一个自己定义的可以伸缩并且滑动展开折叠的一个View,这个时候就很容易出现问题,而不能达到你所期望的效果。还有等等的一些滑动冲突的问题…
由于上面所述的背景,如果我们想要在开发过程中游刃有余,并且满足ui的各种无理要求,那么事件的分发流程就是我们必须要弄清楚的一件事。好了废话不多说下面开始事件流程分析。
在Activity.java这个类中有个dispatchTouchEven() 方法,从字面意思来看是:分发触摸事件,我们来看看源码。
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* 翻译:这个方法用来处理触摸屏幕事件,你可以重写这个方法去拦截所有的触摸屏幕事件
* 在这些事件被分发到window之前。确保这个方法实现能够正常的处理屏幕的触摸事件。
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
从这个方法描述我们可以得到几个信息:
- 这个方法时处理屏幕触摸事件的。
- 我们可以重写这个方法来拦截所有的屏幕触摸事件,不让这个事件再向下分发了,这个事件让我自己来处理。
- 重写的话,一定要确保这个方法能正常处理这个事件。
往这个方法里面看
- 如果这个事件是【按下】事件,那么先执行onUserInteraction()方法,进入这个方法看到,这个方法是一个空的实现,里面什么都没写,而且这个方法申明是public,这就意味着:我们可以重写这个方法,在用户刚开始【按下】的时候,就会执行到我们重写的这个方法里面,可以做一些事件预处理。
- 把事件传递给window,如果 window 的 superDispatchTouchEvent(ev) 方法执行结果为true 代表事件被消费掉了,那么dispatchTouchEvent(MotionEvent ev)方法返回true;如果window执行结果为false 代表传下去的事件没有被消费掉,那么activity将执行自己的onTouchEvent(ev)方法,并把结果继续往上返回给调用者。
我们来看下window是如何处理的:
public Window getWindow() {
return mWindow;
}
我们可以看到这个getWindow()方法,返回的是一个Window对象,进入Window类查看:
/**
* Abstract base class for a top-level window look and behavior policy. An
* instance of this class should be used as the top-level view added to the
* window manager. It provides standard UI policies such as a background, title
* area, default key processing, etc.
*
* <p>The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*
* 翻译:这个抽象类,仅存的实现类是android.view.PhoneWindow。
*/
public abstract class Window {
我们看到了关键的一个描述:The only existing implementation of this abstract class is
android.view.PhoneWindow。意思是这个抽象类,有且只有一个实现类,就是PhoneWindow。那么我们去找PhoneWindow查看:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
在PhoneWindow里面,我们找到了superDispatchTouchEvent(MotionEvent event)这个方法,这个方法实际上是调用了mDecor的superDispatchTouchEvent(event)方法。那我们又去找mDecor是什么:
// This is the top-level view of the window, containing the window decor.
// 翻译:这个是window最顶层view,包含window的装饰。
private DecorView mDecor;
在PhoneWindow中找到mDecor的申明,实际是DecorView,是window的顶层view,如果了解activity的启动流程,DecorView并不陌生。好了继续进入DecorView这个类里面去找superDispatchTouchEvent(event)方法:
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
首先我们可以看到DecorView的申明,DecorView其实他的本质是一个继承自FrameLayout的View。继续找到superDispatchTouchEvent()方法:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
这个方法又调用了父亲的dispatchTouchEvent(event)方法,那我们点进去看父亲的实现,点进去之后发现到了ViewGroup的dispatchTouchEvent()方法,终于找到我们今天的重点了,咋一看这个方法里面的代码非常多,但是其实逻辑很简单,让我们来分析分析,由于代码片段比较长,我把这块方法逻辑大致分成三个大块
- 判断是否拦截代码块。
- 找寻消费事件的子view。
- 分发事件。
/** 这个是ViewGroup的分发事件方法,主要是向下传递事件,然后返回结果给调用者 */
@Override
public 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);
}
/** 是否处理事件的局部变量,最后返回 */
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {...}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
/** 返回定义的局部变量 */
return handled;
这是整个方法的概览,我把中间if (onFilterTouchEventForSecurity(ev)) {…}这里的代码逻辑折叠起来了,这样我们先窥整个方法的全貌。这里定义了一个是否处理事件的局部变量,根据逻辑给变量赋值,最后返回给上层调用者。最关键的逻辑就在中间被折叠起来的那里。按照我们上面分的3大块,我们把中间那部分代码依次拆开:
#第一块,是否拦截逻辑:
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();
}
// Check for interception.
/** 是否拦截事件的标志 */
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
/** 如果事件是【按下】事件,或者触摸对象 mFirstTouchTarget 不为空,进入这里 */
/**
* disallowIntercept, 是否被禁止拦截的标志,通过 FLAG_DISALLOW_INTERCEPT 这个标志位判断。
* */
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
/**
* 如果没有禁止拦截即 disallowIntercept = false,
* 调用ViewGroup的onInterceptTouchEvent(ev),并把返回值给 intercepted 这个局部变量。
* */
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
/** 如果禁止拦截,intercepted 这个局部变量置为false */
intercepted = false;
}
} else {
/**
* 要进入这个判断,必须同时满足两个条件
* 1、事件不是Down【按下】。
* 2、mFirstTouchTarget 当前触摸目标为null,即没有触摸目标。
* 这个触摸目标的概念:消费事件的view就是触摸目标。
* */
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
有些概念我做一个解释:
- 一个完整的事件,是从【Down】到【Up】的过程。【Dow】表示事件的开始,【Up】表示事件结束。
- 触摸目标:表示如果有子view消费了【Down】事件,那么我们就把这个view保存在一个叫TouchTarget的链表数据结构中,我们姑且把它称为触摸目标。也就是说,如果没有子view消费【Down】事件,那么触摸目标对象就是null。
源码中我已附上了解释,我这里在连贯的描述一下,我们只分析重点的地方:
- 事件传递进来 ==》如果是【Down】事件 或者 触摸目标对象不为NULL ==》先检查自己禁止拦截事件的标志FLAG_DISALLOW_INTERCEPT,如果disallowIntercept=false,那么当前ViewGroup会去调用自身的onInterceptTouchEvent方法,询问是否要拦截这个事件,返回值保存在局部变量中;如果禁止拦截,那么局部变量恒等于false。这个局部变量intercepted 将影响后面第三块逻辑,是否分发结束事件给子view,所以等我等会儿回过来再连起来看。
- 事件传递进来 ==》如果既不是【Down】事件,而且 触摸目标对象 为NULL, 那么这就表示,在一个完整的事件中,没有消费这个完整事件的子view,那么intercepted 这个拦截标志将一直为true。
第一块代码分析完毕,上面主要讲是intercepted 的赋值逻辑。
#第二块:寻找消费事件的子View
/** 定义一个触摸目标 局部变量 */
TouchTarget newTouchTarget = null;
/** 是否已经分发给新的触摸目标 局部变量 */
boolean alreadyDispatchedToNewTouchTarget = false;
/**
* 如果canceled = false 并且 intercepted = false
* 即:
* 1、不是【取消】事件
* 2、不拦截(不拦截可以理解为:不拦截就意味着可以向下传递这个事件,拦截意味着就不向下传递事件了)
*
* 执行以下逻辑
* */
if (!canceled && !intercepted) {
// If the event is targeting accessibility 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.
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.
/**
* buildTouchDispatchChildList这个方法得到一个由Z从小到大排序的子view列表
* Z:可以参照Z轴,距离用户越近Z值越大。
* */
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
/** 列表从后往前遍历,即Z从大到小遍历,先从离用户最近的view找起以此类推 */
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, 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;
}
/**
* 这两有两个判断
* 1、子view是否可以接收触摸事件
* 2、触摸点是否在子view区域上
* 如果任一一个条件不满足,直接continue跳过,寻找一个子view
* */
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
/**
* 执行到这一步,表示子view能接收触摸事件,并且触摸点在子view的区域上
* 把子view所在的触摸目标,赋值给newTouchTarget这个局部变量,
* 如果mFirstTouchTarget=null,那么newTouchTarget=null
*
* 这个方法表示意思是:如果这个子view之前已经被保存过了,那么直接把他给newTouchTarget赋值
* */
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);
/** dispatchTransformedTouchEvent 这个方法里面根据条件,是否向下分发事件 */
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
/** 进到这个执行逻辑里面表示: 有子view消费掉了这个事件 */
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
/**
* addTouchTarget() 这个方法里面,把子view封装成一个触摸目标对象,
* 并且mFirstTouchTarget这个全局变量被赋值为当前这个触摸目标对象
* */
newTouchTarget = addTouchTarget(child, idBitsToAssign);
/** 这个标志表示:已经分发给新的触摸对象了 */
alreadyDispatchedToNewTouchTarget = true;
/** 这里如果找到了处理事件的子View,那么退出循环 */
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
/** 遍历循环走完后,将子view列表清空 */
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;
}
}
}
这个代码块里面主要是在找寻消费事件的子view:如果当前不是【Cancel】事件并且intercepted = false,会进入这个逻辑里面寻找消费事件的子view。但是,如果我们的ViewGroup拦截这个事件,那么就不会进到这个逻辑里面,即:不会去找消费事件的子view,那么这就会导致mFirstTouchTarget永远为NULL,触摸目标为NULL。
在这个条件里面做了几件事:
- 把所有的子view按照Z值大小,从小到大放入一个list中。
- 从后往前遍历这个list,即:先从离用户最近的view开始找,以此类推。
- 每个子view必须满足两个条件,才能将这个事件分发给子view。
3.1. 子view是否可以接收触摸事件。
3.2. 触摸点在子view的区域内。 - 将【Down】事件分发给满足条件的这个子view。
- 如果满足条件的这个子view,消费了这个事件,那么将这个子view保存到触摸目标这个链表结构内并且退出遍历,即找到一个消费事件的子view后,就不会继续找了。但是如果满足条件的子view,不消费事件,那么就继续寻找。直到完全遍历完。
第二块内容大致就是这样,主要是在【Down】事件,找寻消费事件的子view,如果没有找到子view,那么触摸目标将无法赋值,也即:mFirstTouchTarget == NULL
#第三块:分发事件
// Dispatch to touch targets.
/**
* mFirstTouchTarget=null
* 其实实际的意思是:这个事件没有一个子view来消费,所以这个对象没有地方被赋值。
* 只有当子view消费了【按下】事件后,mFirstTouchTarget这个对象才会被赋值,否则一直都是null。
* 那么就有一个结论:如果没有子view消费【按下】事件,那么当前ViewGroup对象会把事件交给自己处理。
* 再将处理结果返回给上层调用者。
* */
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.
/** 进入到这里表示:有子view消费了【按下】事件 */
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
/**
* 如果
* 1、局部变量表示已经分发给新的触摸目标
* 并且
* 2、局部变量newTouchTarget = mFirstTouchTarget
*
* 这两个条件要满足,那必须是刚刚上面循环子view的时候找到了消费【按下】事件的子view
* 在上面逻辑中,已经分发事件给子view处理了,所以不能再分发一次,否则就会分发两次
* 【按下】事件给子view。只有除了【按下】事件的其他事件,才可以在这里分发给子view
*
* 那么这次事件就不在分发给子view处理
* */
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
/**
* 局部变量 cancelChild :表示是否给子view分发一个【cancel】事件,结束掉子view的事件处理
* 有这种情况:如果子view消费了【按下】事件,但是如果在【MOVE】事件的时候,当前ViewGroup
* 拦截了事件即:intercepted = true,那么dispatchTransformedTouchEvent()这个方法
* 会分发一个【Cancel】事件给子view,并且将这个子view从触摸目标链表中移除。表示这个子view
* 不会在接收到任何事件消息了, mFirstTouchTarget 对象重置为触摸目标链表中的第一个。
* */
final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
第三部分分发事件:
- mFirstTouchTarget = null 表示没有子view消费事件,那么事件将交给ViewGroup自己来处理。
- 如果【Down】事件的时候,找到了消费事件的子view,即mFirstTouchTarget != null
分发给子view。
这里有有两个关键的判断:
第一个关键:如果是【Down】那么不会重复向子view分发事件
/**
* 如果
* 1、局部变量表示已经分发给新的触摸目标
* 并且
* 2、局部变量newTouchTarget = mFirstTouchTarget
*
* 这两个条件要满足,那必须是刚刚上面循环子view的时候找到了消费【按下】事件的子view
* 在上面逻辑中,已经分发事件给子view处理了,所以不能再分发一次,否则就会分发两次
* 【按下】事件给子view。只有除了【按下】事件的其他事件,才可以在这里分发给子view
*
* 那么这次事件就不在分发给子view处理
* */
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
}
我们根据第二块,知道alreadyDispatchedToNewTouchTarget这个局部变量只有在【Down】事件,并且找到子view消费的情况下,才会被赋值为true,否则都是false。如果是【Down】事件传递到这里,这两个条件都为true,所以就不会再分发一次,要不然的话,就会出现分发两个【Down】事件给子view的情况,因为在第二块寻找子view的时候已经分发过一次【Down】事件了。
第二个关键:是否主动取消子view消费
/**
* 局部变量 cancelChild :表示是否给子view分发一个【cancel】事件,结束掉子view的事件处理
* 有这种情况:如果子view消费了【按下】事件,但是如果在【MOVE】事件的时候,当前ViewGroup
* 拦截了事件即:intercepted = true,那么dispatchTransformedTouchEvent()这个方法
* 会分发一个【Cancel】事件给子view,并且将这个子view从触摸目标链表中移除。表示这个子view
* 不会在接收到任何事件消息了, mFirstTouchTarget 对象重置为触摸目标链表中的第一个。
* */
final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
如果intercepted = true,即表示:拦截事件,cancelChild = true,在dispatchTransformedTouchEvent() 这个方法中,就会给子view,分发一个【Cancel】事件,子view接收到【Cancel】事件,然后ViewGroup将子view触摸目标从链表中移除,后续将不在给这个子view分发事件了。
结论:可以看出是否取消子view,关键在于 intercepted 这个局部变量。这就跟我们第一块是否拦截的逻辑那里联系起来了。如果当前ViewGroup拦截事件,那么直接给子view发一个【Cancel】事件,结束子view接收事件。
到此我们把ViewGroup里面的骨干逻辑就全部了解清楚了,至于没有细小的逻辑我们就不一个个说了,比如:怎么得到的按Z排好序的列表的,怎么判断子view是否能接收事件的,触摸点是否在子view上的,这些都是具体的逻辑了,如果要一个个放到文章里面,那就实在太多了。
综合上述:我们可以以一个故事场景来描述这个分发事件流程。我们就以一个警察局破案这么一个场景来描述一下吧。我们知道一个完整的事件:以【Down】开始,以【Up】或者【Cancel】结束。现实中我们以【报案】开始,以【结案】结束。
场景描述:
1. 有一天【市*】接到【报警】,【市*】根据这个案子的严重情况,来判断是否由【市*】自己亲自处理。如果【市*】自己觉得这是个小案子,自己不想处理,那么就找【市*】下属的【*机构】去处理这个案子。
2. 【市*】根据报案人的【事发地区】,把下属【*机构】按照离【事发地区】距离由远到近以此排序,然后从最近的开始找,如果【*机构】目前有条件可以空出来处理,那么【市*】就把这个案子交给找到的这个下属【*机构】去处理,并且在【市*】这里做好记录信息,如果下属【*机构】没有条件处理,那么就继续找直到找完下属【*机构】为止,如果一个都没法处理那么就由【市*】自己来处理。如果已经有下属【*机构】来处理了,那么之后,所有关于这个案子的所有信息全部都交给这个【*机构】处理。
3. 但是【市*】一直会跟踪这个事情的严重性来判断是否需要自己来处理,如果有一天这个【案子】变严重了,【市*】说,这个案子不用你处理了,我们自己会亲自处理,然后会给下属【*机构】通知这个案子你们不用处理了。然后后续的这个【案子】信息全权由【市*】自己处理。
4. 如果下属【*机构】明确告诉【市*】说,这个事情请你不要插手,那么【市*】说,好我们不插手,然后整个【案子】在【结案】之前都全权由下属【*机构】处理。
结尾
这就是ViewGroup里面的事件分发流程,下一篇我们一起看View的分发!
本文地址:https://blog.csdn.net/u011326269/article/details/108810184