1.事件在DecorView中的分发过程
ViewRootImpl在接收到输入事件后最终会调用DecorView的dispatchPointerEvent,由于DecorView没有重写dispatchPointerEvent,所以调用的是View的dispatchPointerEvent,而View 的dispatchPointerEvent又中会调用dispatchTouchEvent,这个方法在DecorView中被重写了,代码如下:
// DecorView
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
上述代码中的Callback即为Activity。也就是说事件分发最开始是传递给 DecorView 的,DecorView 的 dispatchTouchEvent 是传给 Window.Callback接口方法 dispatchTouchEvent,而 Activity 实现了 Window.Callback 接口。
紧接着Activity 的 dispatchTouchEvent方法里,是调到 Window 的 superDispatchTouchEvent,
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
Window 的唯一实现类 PhoneWindow 又会把这个事件回传给 DecorView,DecorView 在它的 superDispatchTouchEvent 把事件转交给了 ViewGroup。
// PhoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
// DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
至此,触摸事件正式进入ViewGroup中,开启事件的分发流程。总结一下事件分发的流程是:
DecorView -> Activity -> PhoneWindow -> DecorView -> ViewGroup -> View
不难看出,事件的传递过程是一个典型的责任链模式。
2.事件分发涉及到的三个方法
dispatchTouchEvent 事件从Activity经Window,最终会调用到DecorView(即ViewGroup)的dispatchTouchEvent。这个方法的作用是进行事件分发,只要事件能到达ViewGroup那么dispatchTouchEvent方法必定会被调用。dispatchTouchEvent方法返回一个boolean值,代表事件是否被消费。
onInterceptTouchEvent 该方法是ViewGroup独有的方法,在ViewGroup的dispatchTouchEvent中被调用,返回一个boolean值,用来判断当前ViewGroup是否要拦截事件。如果返回true,则表示该ViewGroup要拦截事件。事件交由ViewGrroup处理。
onTouchEvent onTouchEvent方法会在dispatchTouchEvent中调用,作用是处理事件。返回结果表示是否消费当前事件。
View的dispatchTouchEvent事件
以上三个方法的关系可以用以下伪代码表示:
// ViewGroup.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false;
if (onInterceptTouchEvent(event)) {
consume = onTouchEvent(event);
} else {
consume = child.dispatchTouchEvent(event);
}
return consume;
}
通过上边的伪代码想要完整的了解事件分发是不现实的。想要完整的理解事件分发就必须深入到View跟ViewGroup内部一探究竟。
3.View中对事件的处理
View中dispatchTouchEvent方法实现逻辑比较简单,简化后代码如下:
public boolean dispatchTouchEvent(MotionEvent event) {
//... 省略了对滑动等部分的处理逻辑代码
boolean result = false;
// 如果View是enable状态,并且设置了TouchListener,则调用TouchListener的onTouch,
// 如果onTouchEvent方法返回true,则将true作为dispatchTouchEvent的返回值,表示事件被当前View消费掉了
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 如果View是disable状态或者没有设置TouchListener或者TouchListener的onTouch方法反回了false
// 那么就调用View自身的onTouchEvent来处理事件,onTouchEvent返回true表示当前View消费了事件。
if (!result && onTouchEvent(event)) {
result = true;
}
return result;
}
View中的onTouchEvent方法会对事件进行兜底处理,比如在ACTION_UP中调用performClick方法,进而执行View的OnClick方法
public boolean onTouchEvent(MotionEvent event) {
// 判断View是否是可以点击状态,即View设置了clickable或者longClickable或者contextClickable
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:
// 伪代码,这里省略了所有其他逻辑
performClick();
break;
}
}
}
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
// 如果设置了OnClickListener,则调用OnClickListener的onClick方法
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
return result;
}
4.ViewGroup中对事件的分发与处理
首先由于ViewGroup是继承View的,那么它的dispatchTouchEvent有两条路可以走,
1)可以调用super.dispatchTouchEvent
如果调用super.dispatchTouchEvent意味着调用了View的dispatchTouchEvent中的逻辑,即事件交由自身处理。
2)调用自身的dispatchTouchEvent。
如果调用自身的dispatchTouchEvent,即走事件的分发流程,向下分发事件。
至于这两条路是如何选择的,就要详细分析ViewGroup中的dispatchTouchEvent方法了。下面贴一下dispatchTouchEvent简化后的代码:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
// ACTION_DOWN事件认为是时间序列分发的开始
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 重置mFirstTouchTarget状态为null
cancelAndClearTouchTargets(ev);
// 重置是否允许该ViewGroup拦截事件的标记,意味着disallowIntercept对DOWN事件无效
resetTouchState();
}
// (1)如果是DOWN事件,条件一定成立,先询问自身是否要拦截事件
// (2)mFirstTouchTarget不为null,说明有子View消费事件
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
// 是有有子View禁止当前ViewGroup拦截事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 如果没有子View禁止当前ViewGroup拦截事件,则询问自身是否拦截事件
intercepted = onInterceptTouchEvent(ev);
} else {
// 子View禁止了ViewGroup拦截事件,当前ViewGroup不拦截
intercepted = false;
}
} else {
// 能走到此处说明一定不是DOWN事件。
// 且mFirstTouchTarget为null,即没有子View消费事件
intercepted = true;
}
TouchTarget newTouchTarget = null;
// 事件是否已经分发过了的标记,用来避免后边重复分发
boolean alreadyDispatchedToNewTouchTarget = false;
// 注意此处的条件!!!!,此处的intercepted是理解事件分发的关键因素,根据上边分析的条件思考一下,
// intercepted什么时候是false,什么时候是true? 可以来分情况讨论:
// (1)如果是ACTION_DOWN事件,并且拦截了ACTION_DOWN事件,则intercepted为true,否则为false
// (2)如果非ACTION_DOWN事件,且mFirstTouchTarget不为null,拦截了事件则为true,否则为false
// (3)如果不是ACTION_DOWN事件,且mFirstTouchTarget为null,那么intercepted一定为true。
// 此时会跳过此处的if语句,直接将事件交给自身处理。之所以会出现这种情况是因为前面拦截了
// ACTION_DOWN事件,致使mFirstTouchTarget无法被赋值。
// 对上边的三种情况以此执行下边的代码,分析会有什么样的结果
if (!intercepted) {
// 别管前面的条件如何,能走到这里来,意味着当前ViewGroup不拦截事件,即走上述的第二条路,向子View分发事件
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 注意此处,只有ACTION_DOWN事件才能进来
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final View[] children = mChildren;
// 遍历子View,查找消费事件的View/ViewGroup
for (int i = childrenCount - 1; i >= 0; i--) {
// 按顺序查找子View
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// 检查子View是否能接收事件,即事件的坐标在子View内,并且View没有在执行动画
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
// 不满足条件,则跳过该子View,继续下一个子View
continue;
}
// 代码能执行到此处说明已经查找到了符合条件的子View
// 查找到目标View之后,通过dispatchTransformedTouchEvent开始向子View分发事件,
// 即可以理解为调用child的dispatchTouchEvent方法,并将child的dispatchTouchEvent返回值返回
// 那么dispatchTransformedTouchEvent的返回值就意味着子View是否消费了事件。
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 走到这里说明子View中消费了事件,那么就将该View保存到mFirstTouchTarget中
// 该保存操作是在addTouchTarget方法中完成
newTouchTarget = addTouchTarget(child, idBitsToAssign);
// 将alreadyDispatchedToNewTouchTarget置为true,标记已经分发了事件
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}
// mFirstTouchTarget为null,说明没有子View消费事件
if (mFirstTouchTarget == null) {
// 调用dispatchTransformedTouchEvent方法,注意这里child的参数是null,
// 即走上述中的第一条路,调用super.dispatchTouchEvent方法把事件交给自身处理
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 分析什么情况下能走到此处?该ViewGroup没有拦截ACTION_DOWN事件,并且DOWN事件中遍历子View找到了消费事件的子View,
// 那么此时会为mFirstTouchTarget赋值,并且结束ACTION_DOWN事件的分发流程(注意,此时DOWN事件不会执行到此处的代码)
// 接下来会有一系列的MOVE事件,如果此时没有拦截ACTION_MOVE事件,那么ACTION_MOVE事件就会跳过上边的if语句,即跳过事件分发的流程
// 直接将MOVE事件分发给mFirstTouchTarget中的child,即跳过了遍历子View查找消费事件View的流程,节省了性能。
TouchTarget target = mFirstTouchTarget;
// 注意到此处是一个While循环,因为TouchTarget是一个链表,由于多指触控的支持,会形成了一个TouchTarget链表
// 多个TouchTarget代表就有多个触控点,因此,此处会循环遍历链表,然后将多个触控点分发到child View.
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// TouchTarget中的child成员才是真正消费事件的View,调用dispatchTransformedTouchEvent将事件交给child.
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
target = next;
}
}
// 如果子View或者自身消费了事件则返回true,否则返回false
return handled;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
// dispatchTransformedTouchEvent的大致逻辑是这样的,这里简化了代码,理解就好。
if (child == null) {
// 调用自身的dispatchTouchEvent方法
handled = super.dispatchTouchEvent(transformedEvent);
} else {
// 调用子View的dispatchTouchEvent方法
handled = child.dispatchTouchEvent(transformedEvent);
}
}
5. ACTION_CANCEL事件的处理
上述对于ViewGroup的dispatchTouchEvent方法的分析忽略了ACTION_CANCEL的处理。ACTION_CANCEL的调用时机是什么时候?看一个具体的例子。
在一个自定义的ViewGroup里边嵌套一个Button。自定义的ViewGroup重写onInterceptTouchEvent,并拦截一次ACTION_MOVE事件。
class CustomViewGroup @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
var hasInterceptMoveEvent = false
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
// 只拦截一次ACTION_MOVE事件
if (!hasInterceptMoveEvent && ev?.action == MotionEvent.ACTION_MOVE) {
hasInterceptMoveEvent = true
return true
}
return false
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
// 打印事件
Log.d("TAG", "${getAction(ev?.action)}:CustomViewGroup dispatchTouchEvent")
return super.dispatchTouchEvent(ev)
}
}
接下来自定义一个Button,并在Button中的dispatchTouchEvent方法中打印事件,如下:
class CusButton @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : Button(context, attrs, defStyleAttr) {
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
Log.d("TAG", "${getAction(event?.action)}:CusButton dispatchTouchEvent")
return true
}
}
在布局文件中用CustomViewGroup嵌套一个CusButton,接下来,手指按住Button,然后移动,直到手指移除Button外,然后再松开。
可以看到Button收到了一个ACTION_CANCEL事件。在收到这个ACTION_CANCEL事件之后,Button就再也没有收到其它事件。
接下来修改一下上边的例子,CustomViewGroup中不再拦截ACTION_MOVE事件,其它保持不变。
即Button的ACTION_CANCEL事件没有被回调,且抬起手指时CusButton收到了ACTION_UP。
可以总结一下,如果一个子View处理了DOWN事件,那么随之而来的MOVE事件及UP事件也会交给这个View处理。但是在交给它处理之前父View是可以拦截这个事件的。如果父View拦截了这个事件,那么这个子View就会收到一个CANCEL事件。并且后续大MOVE与UP事件都不会再传递给这个View。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null
) {
// 此处,如果子View没有禁止该View拦截事件,则调用onInterceptTouchEvent方法看自身是否要拦截。
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 如果拦截了MOVE事件,那么intercepted为true
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
// intercepted为true,则这个if语句不会被执行,直接跳过
if (!canceled && !intercepted) {
// ...
}
if (mFirstTouchTarget == null) {
// mFirstTouchTarget不为空,所以不会走到这里来
} else {
// intercepted为true时会执行到这里,这里显示是对mFirstTouchTarget链表的遍历
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
// 事件已经分发到了新的TouchTarget
handled = true;
} else {
// 正常情况这个MOVE事件应该走到这里来,此时由于intercepted为true,那么cancelChild就一定为true
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 看dispatchTransformedTouchEvent源码,此时cancelChild为true
if (dispatchTransformedTouchEvent(
ev, cancelChild,
target.child, target.pointerIdBits
)
) {
handled = true;
}
// CANCEl事件交给子View之后,这里判断如果子View被cancel了,那么就将这个View从mFirstTouchTarget链表中移除
// 意味着后续的MOVE事件与UP事件这个View就再也不会收到
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
return handled;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
// cancel为true
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
// 将事件设置为ACTION_CANCEL
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
// 子View为null,则将这个事件交给自身
handled = super.dispatchTouchEvent(event);
} else {
// 将这个事件分发给子View,那么子View就会收到一个ACTION_CANCEL事件
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// 。。。
}