前言
前段时间找工作,看了好多关于事件分发机制的书,各路大牛从不同的角度进行了分析。本人受益匪浅,于是有了这篇吸取天地之精华的解析。
本文章会从什么是事件分发机制开始,一直深入到源码分析。
主要目的是让自己理解更深入,也希望能让读者更容易读懂而不觉干涩。
概念
本节都是基础,我化身十万个为什么提出以下几个问题!如果读者都明了那就直接跳向下一节!
事件分发机制是什么?
事件分发机制就是点击事件的分发。那么点击事件又是什么?
在手指接触屏幕后产生的同一个事件序列都是点击事件。-
点击事件分为哪几种类型?
- 手指刚接触屏幕
- 手指在屏幕上滑动
- 手指从屏幕上松开的一瞬间
同一个事件序列是什么?
是从手指接触屏幕的一瞬间起,直到手指从屏幕上松开的一瞬间所产生的一切事件。点击事件用代码如何表示?
在源码中MotionEvent就是点击事件,对点击事件的分发就是对MotionEvent对象的分发传递过程。-
MotionEvent的点击事件类型?
- ACTION_DOWN:手指刚接触屏幕
- ACTION_MOVE:手指在屏幕上滑动
- ACTION_UP:手指从屏幕上松开的一瞬间
那这个MotionEvent到底是如何传递的?
那就来看下一节!
事件分发机制
所谓事件分发机制,其实就是对MotionEvent(点击事件)的分发过程。
当一个MotionEvent(点击事件)产生之后,系统需要把它传递给一个具体的View,这个传递过程就是事件分发机制。
1. 我们来简单描述一次点击事件(不涉及方法调用,先有个大概的体系)
- 用户接触屏幕产生MotionEvent(点击事件)
- MotionEvent(点击事件)总是由Activity先接收
- Activity接收后将MotionEvent(点击事件)进行传递:Activity->Window->DecorView(DecorView是当前界面的底层容器,就是setContentView所设置View的父容器)
- DecorView是一个ViewGroup,将MotionEvent(点击事件)分发向各个子View
2. 三个方法
相信大家对点击事件已经有所了解,那接下来我们介绍事件分发机制很重要的三个方法,点击事件的分发机制都是根据这三个方法共同完成的:dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent()。
-
dispatchTouchEvent():用来进行事件的分发,如果MotionEvent(点击事件)能够传递给该View,那么该方法一定会被调用。返回值由 本身的onTouchEvent() 和 子View的dispatchTouchEvent()的返回值 共同决定。
- 返回值为true,则表示该点击事件被本身或者子View消耗。
- 返回值为false,则表示该ViewGroup没有子元素,或者子元素没有消耗该事件。
onInterceptTouchEvent():在dispatchTouchEvent()中调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中不会再访问该方法。
onTouchEvent():在dispatchTouchEvent()中调用,返回结果表示是否消耗当前事件,如果不消耗(返回false),则在同一个事件序列中View不会再次接收到事件。
3. 三个方法的关系
这么多概念,别头疼!咱们用伪代码看一下三个方法的关系!
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onInterceptTouchEvent(ev)) {
handled = onTouchEvent(ev);
} else {
handled = child.dispatchTouchEvent(ev)
}
return handled;
}
这段伪代码可以很好地理解事件的传递机制:
用户点击屏幕产生MotionEvent(点击事件),View的dispatchTouchEvent()接收MotionEvent(点击事件)后,先执行该View的onInterceptTouchEvent()判断是否拦截该事件,若拦截执行该View的onTouchEvent()方法,若不拦截则调用子View的dispatchTouchEvent()。在事件传递的源码中,使用的就是类似的逻辑。
4. 事件传递顺序
- 用户点击屏幕产生MotionEvent(点击事件)
- Activity接收MotionEvent(点击事件)—>传递给Window—>传递给DecorView(ViewGroup)—>执行ViewGroup的dispatchTouchEvent()
- ViewGroup接收到MotionEvent(点击事件)之后,按照事件分发机制去分发事件。
- 若当子View不消耗事件,onTouchEvent()返回false,那么这个事件会传递回其父View的onTouchEvent(),如若父View也不消耗,最后会传递回给Activity进行处理。
总的来说点击事件的传递顺序是由父到子,再由子到父的。
图解事件传递机制
现在网上的大部分文章都是通过源码和log讲解事件的传递,对看文章的人来说体验并没有那么好,看的云里雾里摸不出个头。在这献上一本葵花宝典!看了这张图妈妈再也不用担心我的学习啦!
友情提示:
- 还是不理解的同学可以对照上一部分一起看效果更佳。
- 图中View的onTouchEvent返回false,将事件传递给ViewGroup的过程,并不是直接传递。是上级ViewGroup的dispatchTouchEvent()方法接收到子View的onTouchEvent()返回的false,再将事件分发给自己(ViewGroup)的onTouchEvent。
- ViewGroup里面没有复写onTouchEvent,然而ViewGroup本身就是View,View中有onToucheEvent。
源码解析
看了这么久咱们终于来看源码啦!不多废话!一库!
1. Activity对点击事件的分发
先来看Activity的dispatchTouEvent,所有点击事件接收的源头
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
这段代码中我们着重看getWindow().superDispatchTouchEvent(ev),方法将点击事件传递给了Window。返回值表示是否消耗掉了该点击事件。如果所有的View都没有消耗掉点击事件,则Activity调用自己的onTouchEvent。
再来看Window的源码:
public abstract boolean superDispatchTouchEvent(MotionEvent event);
发现其实是一个接口,那实现方法在哪?不急,不难找,源码的最上方注释里写道
The only existing implementation of this abstract class is android.view.PhoneWindow,
该接口的唯一实现方法是PhoneWindow,那咱们再去看PhoneWindow的源码:
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
是不是很熟!其实他也把这个锅直接甩给了DecorView ,之前介绍过,DecorView是当前界面的底层容器,就是setContentView所设置View的父容器。所以再来看DecorView:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
码个蛋!竟然又传递出去了,这次是调用了super,而DecorView是继承自ViewGroup,所以调用了ViewGroup的dispatchTouchEvent!那这样咱们就先来瞧一瞧ViewGroup里的源码!
2.ViewGroup对事件的分发
先来看ViewGroup中的dispatchTouchEvent中的一小段
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;
}
咱们从头开始看,MotionEvent.ACTION_DOWN 这个之前介绍过,那mFirstTouchTarget 是什么?后面的代码表示,当ViewGroup的点击事件被子View消耗,那mFirstTouchTarget就会指向该子View。所以如果事件被子View消耗 或者 是ACTION_DOWN事件,那就访问该ViewGroup的onInterceptTouchEvent,如果不那就全部被当前ViewGroup拦截。换句话说,如果View决定拦截事件,那么这一个事件序列都会由这个View来处理。
那么大家也注意到FLAG_DISALLOW_INTERCEPT这个标志位,看起来它可以影响ViewGroup是否拦截该事件。这个标志位是通过requestDisallowInterceptTouchEvent()方法来设置的,一般用于子View中。当标志位设置之后ViewGroup将无法拦截除了ACTION_DOWN以外的事件了。为啥说除了ACTION_DOWN以外呢?因为dispatchTouchEvent每次接收到ACTION_DOWN都会初始化状态,代码如下。
// 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();
}
综上所述,requestDisallowInterceptTouchEvent()方法不能影响ACTION_DOWN事件。
总结一点,onInterceptTouchEvent()方法不一定会每次都执行,如果想对每个事件都进行处理,那还是在dispatchTouchEvent()里面处理吧。
咱们继续往下走,当该ViewGroup不拦截点击事件的时候,事件会传递给他的子View:
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
final ArrayList preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
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 (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
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;
}
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
以上这段是ViewGroup进行事件分发的主要代码,看起来比较简单。当ViewGroup有子View的时候,进行子View的遍历,其中有一个判断条件:
canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)
判断当前点击事件是否在子View的坐标范围内,且子View没有在坐标系中移动(执行动画),如果子View符合以上两个情况那么就把点击事件传递给他处理。往下走,会看到这么一个判断条件:
dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
这个方法其实就是用来将事件分发给子View的,来看一下这个方法的其中一段源码你就会清晰很多:
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为null那就交给该ViewGroup的dispatchTouchEvent(),反之就将点击事件交给该子View(也有可能是ViewGroup)处理,一次分发就完成了。
再跳回到之前那段超长代码,如果dispatchTransformedTouchEvent()返回true,表明点击事件被子View消耗,执行addTouchTarget()方法给最开始的mFirstTouchTarget赋值。
如果遍历完了所有的子View,点击事件都没有被消耗掉,可能有两种情况:一、ViewGroup下面没有子View。二、子View没有消耗点击事件。这两种情况下,ViewGroup会自己处理点击事件。当子View不消耗点击事件,那点击事件将交由给他的父View去处理。
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
代码里面child参数赋值为null,当child为null时,访问当前ViewGroup的super.dispatchTouchEvent(event),因为ViewGroup是继承自View,所以其实访问的就是View的dispatchTouchEvent()方法。
3.View对事件的分发
再来看看View的dispatchTouchEvent()方法的其中一段代码,注意知识其中一段,篇幅不能太长,想要全部查看一定打开Studio看看源码!
public boolean dispatchTouchEvent(MotionEvent event) {
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
View的dispatchTouchEvent()就比较简单了,onFilterTouchEventForSecurity(event)是用来判断点击事件来到时,窗口有没有被遮挡住,如果被遮挡住则直接返回false,不消耗事件。
反之,接收到事件后看到一个类ListenerInfo,那这是个啥?看源码啊!
static class ListenerInfo {
public OnClickListener mOnClickListener;
protected OnLongClickListener mOnLongClickListener;
private OnKeyListener mOnKeyListener;
private OnTouchListener mOnTouchListener;
......
}
看完源码发现它是一个View的静态内部类,定义了一系列的Listener。
继续看View的dispatchTouchEvent()的源码发现,View会先判断自己是否有设置OnTouchListener,如果所设置的OnTouchListener得onTouch返回true,则直接消耗点击事件,不再执行onTouchEvent()方法。
得出一个结论,OnTouchListener的优先级高于onTouchEvent()。这样做的好处是方便在外部处理事件。
如果没有设置OnTouchListener那就会执行到View的onTouchEvent(),继续看下onTouchEvent()的源码,咱们一段一段来,有点长:
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
当我们把View设置为不可用状态,View依然会消耗点击事件,只是看起来不可用。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
之后如果View设置有代理,那么就会直接执行代理的onTouchEvent()。下面再来看一下点击事件的主要代码:
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
......
break;
case MotionEvent.ACTION_CANCEL:
......
break;
case MotionEvent.ACTION_MOVE:
......
break;
}
return true;
}
当View的CLICKABLE、LONG_CLICKABLE和CONTEXT_CLICKABLE有其中一个为true那么View就会消耗掉这个事件。并且在ACTION_UP的时候会执行performClick()方法:
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
如果View设置了OnClickListener,performClick()这个方法就会去执行这个监听事件。
再来一个结论,OnTouchListener的优先级高于OnClickListener,OnClickListener是在ACTION_UP的时候执行的。
看到这里事件传递机制的源码分析终于结束了!!!
结论
- 事件分发机制就是点击事件的分发,在手指接触屏幕后产生的同一个事件序列都是点击事件。
- 点击事件的传递顺序是由父到子,再由子到父的。
- 正常情况下事件只能被一个View拦截。
- 如果View决定拦截事件,那么这一个事件序列都会由这个View来处理。
- 当子View不消耗点击事件,那点击事件将交由给他的父View去处理,如果所有的View都没有消耗掉点击事件,则Activity调用自己的onTouchEvent。
- onInterceptTouchEvent()方法不一定会每次都执行,如果想对每个事件都进行处理,那还是在dispatchTouchEvent()里面处理吧。
- OnTouchListener的优先级高于onTouchEvent()。这样做的好处是方便在外部处理事件。
- 当我们把View设置为不可用状态,View依然会消耗点击事件,只是看起来不可用。
最后给大家推荐一篇View源码分析的文章,里面有Log日志分析。大家可以看一看增深理解。《Android View 事件分发机制源码详解(View篇)》
最最后再给大家推荐一本书《Android开发艺术探索》,各大网站都有卖,对于突破瓶颈有很大的意义。
结后谈
博主花了一段时间终于理顺完了这篇文章,当然由于博主的技术原因,文章并不是十全十美的,只希望给还处在迷茫期的朋友们指引一条方向。
希望我的文章能给大家带来一点点的福利,那在下就足够开心了。
下次再见!