View的事件分发机制
最近看到事件分发机制的讨论,可是细节都忘的差不多了,看以前的笔记写的比较简单,就趁这个机会整(shui)理(wen)吧。
事件分发机制的传递规则
时间分发机制的传递规则可以用以下伪代码表示:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = false;//事件是否被消费
if (onInterceptTouchEvent(ev)){//调用onInterceptTouchEvent判断是否拦截事件
result = onTouchEvent(ev);//如果拦截则调用自身的onTouchEvent方法
}else{
result = child.dispatchTouchEvent(ev);//不拦截调用子View的dispatchTouchEvent方法
}
return result;//返回值表示事件是否被消费,true事件终止,false调用父View的onTouchEvent方法
}
这段伪代码的总结可以说是非常精辟了,把复杂的源码逻辑给提炼出来,相信大家很从这段伪代码看出事件分发机制的传递规则。
先上总结:点击事件由Activity捕获,传递到顶层的ViewGroup,所以从顶层的ViewGroup的dispatchTouchEvent
方法开始:首先判断顶层的ViewGroup的onInterceptTouchEvent
方法,如果onInterceptTouchEvent
方法返回true,则拦截事件,调用本身的onTouchEvent
方法,否则调用子View/ViewGroup的dispatchTouchEvent
下发事件,用Word简单画了一个流程图,大致如下:
之前我也这样浅尝即止了,对具体的细节也不太清楚,解决了一个滑动冲突之后也没深究,这次就从源码入手,详细的剖析一下事件分发机制。
源码分析
从上面的总结可以看出:首先被调用的dispatchTouchEvent
是Activity中的顶层ViewGroup,于是先看ViewGroup的dispatchTouchEvent
方法
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
···
// 1.判断是否是ACTION_DOWN事件
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
final boolean intercepted;
// 2.当前ViewGroup是否拦截ACTION_DOWN以外的事件
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
···
}
首先看1:Android中的点击事件都是以ACTION_DOWN
开始,所以如果传入dispatchTouchEvent
中的事件是ACTION_DOWN
的话,说明接下来是一个新的点击事件序列,需要对之前的操作进行初始化
2:mFirstTouchTarget
在这里可以简单看做当前ViewGroup是否拦截事件的Flag,如果拦截,则mFirstTouchTarget==null
,所以当ViewGroup拦截点击事件,遇上ACTION_UP
、ACTION_MOVE
等事件时,会直接设置intercepted = true
,跳过onInterceptTouchEvent
,此后的一个事件序列都由这个ViewGroup
进行处理,一句话概括:如果ViewGroup
拦截事件,则跳过onInterceptTouchEvent
,后续事件都由它自己处理
另一种情况:当事件为ACTION_DOWN
或者mFirstTouchTarget != null
即该ViewGroup不拦截点击事件的时候会进入if,intercepted = onInterceptTouchEvent(ev)
,这里我们看onInterceptTouchEvent
方法
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
可以看到onInterceptTouchEvent
方法默认返回一个false,也就是不拦截事件,我们可以通过自定义ViewGroup
重写onInterceptTouchEvent
达到拦截的目的
- 扩展:在
dispatchTouchEvent
代码段2下面有一个标志位FLAG_DISALLOW_INTERCEPT
,它也可以禁止ViewGroup
拦截处理ACTION_DOWN
以外的事件,可以通过子View的requestDisallowInterceptTouchEvent
设置
onInterceptTouchEvent
拦截讲完了,接着看dispatchTouchEvent
代码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
···
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//1.循环遍历子View
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!child.canReceivePointerEvents()
|| !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);
//2.dispatchTransformedTouchEvent调用子元素或父类的dispatchTouchEvent
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);
}
if (preorderedList != null) preorderedList.clear();
}
···
}
我们可以看到在代码1处,倒序遍历了所有的子元素,也就是说,从最上层的元素开始遍历,如果子元素能够接受到点击事件,就交给子元素来处理。
再看2处,dispatchTransformedTouchEvent
这个方法调用了子元素或者父类的dispatchTouchEvent
方法,我们看一下dispatchTransformedTouchEvent
的代码
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
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);
return handled;
}
可以看到,如果有子View,调用子View的dispatchTouchEvent
方法,如果没有,调用super.dispatchTouchEvent
,而ViewGroup是继承自View,所以归根到底都是调用View的dispatchTouchEvent
方法:
public boolean dispatchTouchEvent(MotionEvent event) {
···
boolean result = false;
//1
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//2
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//3
if (!result && onTouchEvent(event)) {
result = true;
}
···
return result;
}
View.dispatchTouchEvent
中result == true
代表了是否消费该事件,而result = true
则只有上面三种情况:
1. 如果是通过鼠标输入拖动滚动条,则消耗这个事件,这种情况在开发中比较少见。
2. ListenerInfo
是view的一个内部类 里面有各种各样的listener,例如OnClickListener
,OnLongClickListener
,OnTouchListener
等等
在这个if中,先获取View中的ListenerInfo
对象li = mListenerInfo
,如果li!=null
且我们通过setOnTouchListener
设置了监听,即是否有实现OnTouchListener
,如果有实现就判断当前的view状态是不是ENABLED,如果实现的OnTouchListener
的onTouch
中返回true,并处理事件,设置result = true
。
其中,onTouch
方法返回的result决定了是否执行OnTouvhEvent(event)
方法,可以看出,OnTouchListener.onTouch
的优先级是高于OnTouvhEvent
的。而onTouch
方法一般是当我们调用View的setOnTouchListener
时重写的方法,利用到实际开发中,如果重写的OnTouch
方法中返回了true,View就不会调用OnTouvhEvent
,反之返回false的时候不仅会走onTouch
,还会调用调用ViewOnTouvhEvent
,最终调用我们设置的setOnClickable
、setLongClickable
等方法
3. 第三种就是OnTouchListener
未设置或者OnTouch
返回false时,调用OnTouvhEvent(event)
方法,如果OnTouvhEvent(event)
方法返回true,则把result设置为true表示事件已经被消耗。具体在OnTouvhEvent(event)
中发生了什么我们看代码:
public boolean onTouchEvent(MotionEvent event) {
···
final int action = event.getAction();
final int viewFlags = mViewFlags;
//1
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
···
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
//2
performClickInternal();
}
}
}
···
}
return true;
}
return false;
}
可以看到1处的if对viewFlags
的与CLICKABLE
、LONG_CLICKABLE
标志位进行与运算,也就是说View的CLICKABLE
、LONG_CLICKABLE
只要有一个为true,那么就会进入这个if,最终返回true,消耗这个事件。
而开发中经常使用的View的setOnClickListener
和setOnLongClickListener
就会自动把CLICKABLE
和LONG_CLICKABLE
设置为true。
然后当事件为ACTION_UP
时,也就是手势抬起,会调用performClickInternal()
方法,而performClickInternal()
方法最终调用了performClick()
方法:
public boolean performClick() {
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
//1
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
可以看到在1处,如果我们设置了OnClickListener
,会调用OnClickListener.onClick(this)
,也就是我们设置的点击事件,然后result = true
事件被消耗。自此,View的事件分发结束。