事件分发是Android中的一个重点也是一个难点,在自定义控件中很是常用。前后看了好多书和博客,感觉写的东西顺序都稍微有些不对,让刚接触的人看起来不是很好懂。在这里也是将我从不清楚到熟悉的过程写下来,希望对大家有所帮助,对自己也起到总结的作用。
下面介绍几个方法,只要先有个印象就好,以后会慢慢解释:
首先是
dispatchTouchEvent(MotionEvent ev)
分发事件,返回true表示事件被当前的View或其子View消耗,返回false表示事件没被消耗。
onInterceptTouchEvent(MotionEvent ev)
是否拦截某个事件,返回true表示拦截,返回false表示不拦截
onTouchEvent(MotionEvent ev)
处理点击事件,返回true表示事件被当前的ViewGroup/View消耗掉,返回false表示没消耗
任玉刚大神书中的一段代码对这个逻辑阐述的非常清楚,下面是这段伪代码:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
先简单的理解上面的代码,在源码的分析中将会对代码进行进一步分析,而后给出结论。
源代码分析:
当一个点击事件发生时,最先接收到事件的是Activity的dispatchTouchEvent方法,下面是相应源码:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
如果动作是ACTION_DOWN的话,会回调onUserIteraction方法,这个和我们关系不大。
我们先暂且认为getWindow().superDispatchTouchEvent(ev)是调用子View分发事件的方法,当子View没有消耗掉这个事件(也就是子View的dispatchTouchEvent返回false)时,Activity就会调用自身的onTouchEvent方法来处理这个事件。
进入getWindow().superDispatchTouchEvent(ev)发现这是一个抽象的方法,我们知道Window的实现类是PhoneWindow,查看PhoneWindow的相应方法可以看到其调用了DecorView的superDispatchTouchEvent,在以前的学习中我们知道在setContentView中我们设置的View就是DecorView的子View,所以点击事件一定能传递到子View,而一般的子View就是ViewGroup。根据本文开始的分析,进入时应该调用的是dispatchTouchEvent方法,我们现在来看一下ViewGroup中dispatchTouchEvent方法的源码:
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
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 {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
还有一个符号位FLAG_DISALLOW_INTERCEPT,看名字也应该可以猜出来,它的作用是不要拦截当前的事件,那和这个符号位进行与操作的mGroupFlags又是谁设置的?不难猜出,应该是子View设置的为了让当前View不去拦截事件。
现在重新审视我们刚才写过的内容,在进入dispatchTouchEvent方法时清除状态所清除的内容也就很好理解了:
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
现在我们再来看给intercepted赋值的代码块,不难得出下面这几个结论:
1.每次ACTION_DOWN发生的时候,都会回调onInterceptEvent方法(因为每次ACTION_DOWN发生的时候状态都会清除)。
2.一旦当前View将ACTION_DOWN事件拦截后,mFirstTouchTarget仍然为空,那么在一个事件序列(一个ACTION_DOWN,中间任意多个ACTION_MOVE,一个ACTION_UP)内不会再调用onInterceptTouch方法进行判断,intercepted变量始终为true,也就是所事件一直被当前View所拦截。
3.若ACTION_DOWN不拦截,那么在一个事件序列内不管拦截了什么动作,在下一个事件到来时都要回调onInterceptTouchEvent方法。
4.子View可以通过设置mGroupFLags值的方式来控制当前View拦截事件,但是ACTION_DOWN事件不被控制。
if (!canceled && !intercepted) {
final int childrenCount = mChildrenCount;
if (childrenCount != 0) {
// Find a child that can receive the event.
// Scan children from front to back.
final View[] children = mChildren;
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
final boolean customOrder = isChildrenDrawingOrderEnabled();
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder ?
getChildDrawingOrder(childrenCount, i) : i;
final View child = children[childIndex];
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
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();
mLastTouchDownIndex = childIndex;
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
在其中完成了对mFirstTouchTarget的赋值。
至此我们完成了整个ViewGroup的分析过程,如果仔细观察不难得出结论,当前View如果将事件分发给子View来进行处理,但是子View的dispatchTouchEvent方法返回false的话,那么当前View将会处理这个事件。同时不难用递归的思想来想这个问题,如果当前的View的dispatchTouchEvent也返回false呢,那么当前View的上一级View就会进行处理,而最后不难发现事件将会有Activity中的dispatchTouchEvent方法进行消耗,也就是调用了Activity中的onTouchEvent,如果忘记了可以去看最开始贴出的Activity源码。
分析过了整个ViewGroup的分发过程,现在到了整个事件分发的最底端,也就是View的分发过程,同样的,我们先来看一下dispatchTouchEvent中的代码:
public boolean dispatchTouchEvent(MotionEvent event) {
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
return true;
}
if (onTouchEvent(event)) {
return true;
}
}
return false;
}
上面的方法中调用了onTouchEvent,我们现在来看一看这个方法的默认实现:
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
继续向后来阅读:
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
而后面的代码比较多也和事件分发无关,如果感兴趣可以去读一读。值得注意的是,在当前的View的Clickable或LongClickable至少有一个为真时,onTouchEvent就会返回true,也就是说View会将这个事件消费掉,同时要注意上文中有一行代码是ViewGroup调用super的dispatchTouchEvent方法,那么最终也是会执行到这里的。