从源码角度解析Android事件分发机制
Posted 加冰雪碧
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从源码角度解析Android事件分发机制相关的知识,希望对你有一定的参考价值。
事件分发是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;
出现了一个mFirstTouchTarget变量,它代表的是什么意思呢?在后文中会有对mFirstTouchTarget的说明,但是这里为了解释清楚,不妨先透露一下,如果当前View将事件分发到子View,那么mFirstTouchTarget将被赋值。
还有一个符号位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;
很清楚,将mGroupFlags的状态清除掉了,同时将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语句块,可以发现,当 dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法返回true的时候for循环即可退出,下面是这个方法代码中有用的片段:
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;
可以看出当前的child不为空,那么其调用了child的dispatchTouchEvent方法。如果当前所有的子View的dispatchTouchEvent都返回的是false,或者当前没有子View,那么for循环就会跳出,执行如下的代码:
if (mFirstTouchTarget == null)
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
这次传入dispatchTransformedTouchEvent方法的参数是null,由上面的代码可以看出将调用父View的dispatchTouchEvent方法。这里的返回值是handled,也就是子View的dispatchTouchEvent返回值或者当前View的处理,也说明了开篇时伪代码的正确性。
前面提到了mFirstTouchTarget,那么这个参数是在哪赋值的呢?仔细看dispatchTransformedTouchEvent返回true时进入的if代码块,调用了一个函数是addTouchTarget(child, idBitsToAssign),其中的代码如下:
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中的代码:
<span style="font-size:18px;">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;
</span>
整个方法很简单,if的判断条件过滤了所有View不响应的状态,我们看if代码块内的内容。值得注意的是,当为View设置了OnTouchListener后,并且重写的onTouch方法返回true的时候,整个方法直接返回了。换句话说就是onTouchEvent方法并不会执行,这点要注意。另外,注意到整个dispatchTouchEvent方法的返回值也就是onTouch或者onTouchEvent的返回值,代表了当前的事件传递到了View是否被消耗了,如果并没有被消耗,根据上文中我们得出的结论,那么上一级View的dispatchTouchEvent就会被调用,这个关系可以依次向上级传递。
上面的方法中调用了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));
刚进入方法时来了一段这样的片段,不难理解出它的意思。在当前的View状态为DISABLE时,只要它是可点击的,或者是可以长点击的话,onTouchEvent就会返回true并且将当前的事件消费掉,但是并不响应这个事件。
继续向后来阅读:
if (mTouchDelegate != null)
if (mTouchDelegate.onTouchEvent(event))
return true;
看到了这样一个代码块,如果当前的View设置有TouchDelegate(代理,一般用于增加点击的范围,有兴趣的可以自行了解一下)的话,那么事件是否被消耗掉取决于代理onTouchEvent的返回值。
而后面的代码比较多也和事件分发无关,如果感兴趣可以去读一读。值得注意的是,在当前的View的Clickable或LongClickable至少有一个为真时,onTouchEvent就会返回true,也就是说View会将这个事件消费掉,同时要注意上文中有一行代码是ViewGroup调用super的dispatchTouchEvent方法,那么最终也是会执行到这里的。
以上是关于从源码角度解析Android事件分发机制的主要内容,如果未能解决你的问题,请参考以下文章
Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
(转) Android事件分发机制完全解析,带你从源码的角度彻底理解(上)