可能你遇到过这样的情形,从github上down下来一个开源项目的demo跑到好好的,可是一用到自己项目中就出现各种问题,例如滑动冲突问题,可是不知道从何下手解决?了解View的分发机制也许可以帮助你。在自定义有交互View中,事件分发处于一个比较重的地位,也是面试的常客。
在开始之前呢先啰嗦一点题外话,我们在平时学习工作中经常会遇到一些问题,特别是作为开发人员,通常的做法是google、baidu一些资料,看看别人有没有遇到过类似的问题,借鉴他们的处理方案。这的确是一个有效的方法,可是我们都坚信一点,那就是不管在生活上还是在工作上,总有的时候没有人可以给你参考,你需要独立思考并作出选择,所以养成独立思考的习惯也很重要。
通常来说要去探究一件事,总要有一些线索才行,你比如说警察破案,他要勘察案发现场,搜集一些证据,然后沿着线索一步步侦破案件。那我们现在要分析View的事件分发机制,如何去找这个所谓的线索,事实上程序的“作案”过程会被完整地记录了,那就是方法栈。你是不是联想到了平时程序异常时打出来的异常栈,就是它,现在就案情重演一次,看看它的“作案”过程:
btn.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
//throw new RuntimeException("Touch");
Thread.dumpStack();
return false;
}
});
这里选择给一个Button设置OnTouchListener,然后在onTouch方法中通过Thread.dumpStack()打印出方法的调用栈,当然你也可以抛出一个异常,或者进入debug模式来查看。点一下这个Button打印出了下面这一段内容:
这一段内容记虽然比较长,但是不复杂。它展示了点击事件在整个Android系统中的传递过程,从ZygoteInit.main方法到OnTouchListener.onTouch方法。从严格意义上来讲,这不算是完整的过程,为啥?通常点击事件是从点击屏幕开始,当点击手机屏幕后,驱动程序是如何处理并把这个事件交给Android系统这个过程是没有体现的。
这次我并不打算分析完整个过程,因为其实在应用层,事件的输入通常是在应用的界面上,而我们编写界面基本是开始于Activity,所以从Activity开始分析往下分析,就是下面这一段:
是不是看起来内容少了很多,心理负担一下子就轻了不少。可以看到事件的传递过程是Activity->PhoneWindow->DecorView->ViewGroup->View。下面就沿着这条线索摸索摸索,首先是Activity的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//这个方法是空的实现,用法可以看看它的注释
onUserInteraction();
}
//将事件传递到Window中
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//如果子View都不消耗该事件,那Activity的onTouchEvent()方法就会被调用
return onTouchEvent(ev);
}
可以看到,Activity将事件传递给了Window,如果Window的superDispatchTouchEvent()方法返回true,那Activity的onTouchEvent就不会被调用了。
接着就到Window了,这个Window是个抽象类,我们要找的是它的实现类,通过前面的方法调用栈知道它是PhoneWindow,看看它的superDispatchTouchEvent方法:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
Window又将事件传递给了DecorView,可见这个Window充当了Activity和DecorView之间的连接纽带。
//DecorView#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView的superDispatchTouchEvent方法调用了父类的dispatchTouchEvent,而DecorView的直接父类是FrameLayout,而FrameLayout的dispatchTouchEvent方法是从ViewGroup继承下来的,所以事件就传递到了ViewGroup中,这点从方法栈也可以看出来。
ViewGroup的dispatchTouchEvent方法代码是比较多的,也是View事件分发的核心,搞清楚它基本也就搞清楚了View的事件分发过程,下面分段来看看这个方法的实现:
// 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();
}
从这段代码实现可以看到在DOWN事件的时候会重置一些状态信息。
// Check for interception.
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); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
//从这个else条件可以看出,只要不是DOWN事件或者mFirstTouchTarget为null, intercepted直接赋值为true,也就是默认拦截。这个DOWN很好理解,但是这个mFirstTouchTarget是什么现在还不知道
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
从这一段代码可以看到在DOWN事件的时候,会调用onInterceptTouchEvent()方法来询问是否要拦截该事件,并通过intercepted来标记,后面的代码会根据这个标记来选择不同的处理方式,首先看看intercepted为false,即事件会向下传递给子View:
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//遍历所有的子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;
}
//判断子View是否可以接收点击事件,点击事件的x,y坐标是否在子View内部
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
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);
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
for (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);
}
这段代码也比较简单,遍历所有的子View,首先判断子View是否能接收点击事件(View的可见性为VISIBLE或者view.getAnimation() != null),接着判断事件的x,y坐标是否在View的内部。如果能满足这两个条件,那事件就可以传给该子View处理了。其中dispatchTransformedTouchEvent()方法实际上是调用了子View的dispatchTouchEvent()方法,如下:
View#dispatchTouchEvent
// 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());
}
//这里传入的child不是null,所以走这个分支
handled = child.dispatchTouchEvent(transformedEvent);
}
这里调用dispatchTransformedTouchEvent()传入的child不是null,所以走的是else分支。注意,如果子View的dispatchTouchEvent()方法返回true, 那么 mFirstTouchTarget 就会被赋值并跳出遍历子View的循环,如下:
//addTouchTarget内部会对 mFirstTouchTarget进行赋值操作
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
addTouchTarget()内部会为mFirstTouchTarget赋值
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
还记的前面关于拦截是的条件吗?
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)
这里就解答了前面的疑问,ViewGroup不拦截DOWN事件,且子View的dispatchTouchEvent()方法返回true时mFirstTouchTarget被赋值。前面我们也提到DOWN事件时会重置一些状态信息,这个mFirstTouchTarget会被置为null。也就是说一旦子View的dispatchTouchEvent()方法返回true,那同一个事件序列中,mFirstTouchTarget != null这个条件就都成立。
如果遍历完所有子View,mFirstTouchTarget为null(有两种情况,子View的dispatchTouchEvent()方法都返回false;或者事件被拦截了,即intercepted为true),则会走下面的逻辑:
// 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);
}
注意这里的dispatchTransformedTouchEvent()方法的child是null。
// Perform any necessary transformations and dispatch.
if (child == null) {
//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);
}
可以看到当child为null是调用的super.dispatchTouchEvent()方法,这个super就是View。看到这你可能会问,前面为什么不直接往下分析View的dispatchTouchEvent()方法的实现?当然可以,只是这样方法栈就会变深,而我们记忆是有限的,一味地深入会让自己无法自拔,这点在阅读源码的时候要注意。现在大局观已经明确,再去分析它的实现就比较清晰了。
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
//点击ScrollBar
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
//li.mOnTouchListener就是通过setOnTouchListener设置的OnTouchListener
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//如果 mOnTouchListener.onTouch()返回true,onTouchEvent()就不会再被调用了
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
从View的dispatchTouchEvent()的实现可以看到,如果有调用setOnTouchListener()设置了OnTouchListener的话,就会先调用OnTouchListener的onTouch()方法,若onTouch()返回false,再调用View的onTouchEvent()方法;若返回true,则直接返回了,这将导致View的onTouchEvent()不会被调用。另外在onTouchEvent()内部会调用OnClickListener.onClick()方法:
public boolean onTouchEvent(MotionEvent event) {
...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
if (!post(mPerformClick)) {
//这个方法的实现会调用OnClickListener.onClick()方法
performClick();
}
...
}
}
}
在UP事件时调用performClick()内部会调用OnClickListener.onClick(),所以父View不能拦截UP事件,否则点击事件就不会被调用。
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
//onClick()被调用
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
从前面的分析可以看到我们平时常用的onClick()方法的优先级是最低的,调用顺序为:OnTouchListener.onTouch()->onTouchEvent()->OnClickListener.onClick(),而且onTouch()返回true会中断后续的调用。
前面的分析过程中发现,主要的逻辑是下面三个方法:
public boolean dispatchTouchEvent(MotionEvent ev)
用来进行事件分发,当事件传递到当前面View,dispatchTouchEvent()方法会被调用,它的返回值受onTouchEvent()方法的返回值和子View的dispatchTouchEvent()返回值的影响,表示是否消耗事件。
public boolean onInterceptTouchEvent(MotionEvent ev)
这个方法的返回值表示是否拦截事件,如果返回true,同一个事件序列中不会再被调用,同一个事件序列指的是down->move...move->up。
public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent()内部调用,用于处理事件,返回值表示是否消耗该事件。如果返回false,同一事件序列中,当前View(不包括ViewGroup)将无法再次接收到。
这三个方法的关系大致可以理解为下面的伪代码:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
大致的意思是当事件传递给ViewGroup时,ViewGroup的dispatchTouchEvent()方法会被调用,接着调用它的onInterceptTouchEvent()方法询问是否拦截该事件,如拦截则该事件交给这个ViewGroup处理,即它的onTouchEvent()方法被调用;如不拦截,则传递给子View,如此反复直到事件被处理掉。下面给出一些前面分析得到的结论:
(1)当一个View决定拦截某个事件,即onInterceptTouchEvent()返回true,那么同一个事件序列的所有事件都直接交给它处理而不会再调用onInterceptTouchEvent()来询问是否拦截;换句话说就是一个事件序列只能被一个View拦截并消耗。
(2)某个View一旦开始处理事件,如果它不消耗调DOWN事件,即onTouchEvent()返回false,那同一个事件序列的其他事件就不会再交给它处理,事件将重新交由它的父View处理,即父View的onTouchEvent()会被调用。
(3)如果不消耗DOWN以外的事件,同一个事件序列还是会传给当前View,没有消耗的事件会交给Activity的onTouchEvent()来处理。
(4)View的onTouchEvent()默认都消耗事件,即返回true,除非它是不可点击的,即下面的clickable为false。
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
可以看到只要是CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE这三个有一个为true它就是可点击的,而与ennable无关,换句话说就是如果View的enable为true也是会消耗事件的。
(5)事件总是先传给父View,然后在传给子View,在子View中可以通过调用父View的requestDisallowInterceptTouchEvent()来干预事件的拦截过程,在处理滑动冲突可以利用这一点。
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
在这个方法内部修改了mGroupFlags的值,现在再回头看看ViewGroup拦截事件的逻辑:
ViewGroup#dispatchTouchEvent
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//这个值会受到干预,进而影响是intercepted的值,也就是干预拦截
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
可以看到如果disallowIntercept为true,intercepted直接置为false。另外DOWN事件是不能干预的,因为在DONW事件时调用会resetTouchState()来重置状态信息,mGroupFlags会被重置。
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
这些结论看似轻描淡写,要真正理解就必须要仔细看看源码才行,如果你能为这些结论说出有力的论据,那说明你掌握的也差不多了。后面我会运用这些论据来自定义一个具体的ViewGroup,让这些结论变得有用。