Android 事件分发机制源码详解-最新 API
大体内容如下
- 概念
- Activity 对事件的分发
- ViewGroup 的事件分发过程
- View 的事件分发过程
- View 的点击事件处理
-
总结
分析了源码可以得到的结论
事件的传递过程文字描述
事件传递机制流程图
伪代码展示事件分发拦截和消费三者的关系
本篇文章是基于最新 Android 源码 ( API27 ),进行分析总结的,可以直接翻到文章末尾查看「源码总结」和「事件传递流程图」,带着大体流程和结论去看源码,效率更高。
概念
事件:就是用户手指从触摸屏幕的那一刻起,到手指离开屏幕的那一刻为止,中间产生的一系列动作,(DOWN MOVE UP等)都是事件,都被封装到了 MotionEvent 中。
所谓事件分发:就是当一个 MotionEvent 产生以后,系统需要把它传递给某一个具体的 View,而这个传递的过程就是分发过程。
image事件的大体流向:
image事件一级一级的往下传递,如果没有任何一个 View 消耗掉事件,那么最终还是会传递给 Activity 的,于是就有了网上的说法,事件传递是由外向内,事件消耗是由内向外传递的。
分发过程中几个重要的方法:
-
dispatchTouchEvent(MotionEvent)
-
onInterceptTouchEvent(MotionEvent)
-
onTouchEvent(MotionEvent)
-
requestDisallowInterceptTouchEvent(boolean)
下面开始一步一步详细分析源码:
Activity 对事件的分发
事件分发的第一个回调方法就是 dispatchTouchEvent,每次都会调用
/**
* Activity#dispatchTouchEvent
* You can override this to intercept all touch screen events before they are dispatched to the window.
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
说明:
onUserInteraction() 是一个空实现的方法,官方示意为:实现这个方法,就会告诉你用户与设备已经开始交互了;与之对应的还有一个 onUserLeaveHint(),这两个方法可以配合起来,来决定状态栏显示通知和取消的时机。
第二个 if 是重点,如果 Window.superDispatchTouchEvent(ev) 返回 true,那么事件被消费,到此就结束了。如果返回的 false,即事件一级一级向下传递,直至到最后一个 View#OnTouchEvent() 全部返回了 false,那么最终会回调到 Activity#onTouchEvent() 方法。
getWindow() 返回的就是一个 Window,是一个抽象类,它的唯一实现类是 PhoneWindow,Window#superDispatchTouchEvent(MotionEvent)也是一个抽象方法,所以事件是传递到了PhoneWindow#superDispatchTouchEvent(MotionEvent)
// PhoneWindow#superDispatchTouchEvent(MotionEvent)
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
我们看到事件进一步通过 mDecor 进行分发了,DecorView 就是我们在 Activity 里边通过 setContentView(xxx) 设置的布局挂载到的一个顶级父 View,换句话说,我们在 Activity 中通过 setContentView(xxx) 设置的 View,其实就是 DecorView的一个子 View。
DecorView 继承自 FrameLayout,是 ViewGroup 类型。
简单贴下 DecorView 的代码,下一篇会分析 Activity 的 setContentView(xxx) 到底是怎么加载我们的布局的
Activity#setContentView(int layoutResID) -> 会回调到 PhoneWindow#setContentView(int)
// DecorView的实例化过程
public class PhoneWindow extends Window implements MenuBuilder.Callback {
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
......
// PhoneWindow#setContentView(int)
@Override
public void setContentView(int layoutResID) {
// installDecor() new 出来一个对象
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
......
}
说明:
installDecor() -> 回调到 generateDecor(int),在该方法中最终通过 new DecorView(Context, int, PhoneWindow,WindowManager.LayoutParams) 出来的,留到下一篇分析
......
}
// 在 API 27,DecorView 单独是一个类,是 ViewGroup 类型
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
......
// DecorView#superDispatchTouchEvent(MotionEvent)
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event); // 实际调用的就是 ViewGrup 的方法
}
......
}
看到这里,我们已经知道了,其实啥也没干,事件就是简单的从 Activity 传到了 ViewGroup 的dispatchTouchEvent()。
ViewGroup 的事件分发过程
ViewGroup 对事件的分发,就是通过 ViewGroup#dispatchTouchEvent(MotionEvent) 来进行事件传递的过程
// ViewGroup#dispatchTouchEvent(MotionEvent)
// 这个方法每次都会调用
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// 该 if 条件默认返回 true;除非当前的 Window 被另一个可见的 Window 部分或者全部遮挡掉了,就会丢弃掉该事件
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 代码片段一
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 这个方法很关键,只要是 DOWN 事件传递到这里,会清除一些状态
// 注意 执行完 cancelAndClearTouchTargets(ev) 方法后 mFirstTouchTarget == null,
// 这个具体是什么等下细说
// 先记住一个结论:事件如果能够正常传递给子 View,并且被子 View 消费掉,
// 那么mFirstTouchTarget 就会被赋值(即 mFirstTouchTarget != null)
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 代码片段二 这个地方检测 ViewGroup 是否拦截事件
// DOWN 事件时,这个 mFirstTouchTarget == null,会判断ViewGroup 是否拦截事件
// 情况一:ViewGroup 拦截事件,即onInterceptTouchEvent(ev) 方法返回 true,
// 会直接调用 ViewGroup.onTouchEvent(MotionEvent)方法自己处理事件,这个时候 mFirstTouchTarget == null;
// 当 MOVE/UP事件到来时,该 if 条件不满足,ViewGroup.onInterceptTouchEvent(ev) 不会被调用,此时的 MOVE/UP事件直接传递到了 ViewGroup.onTouchEvent(MotionEvent)中
// 情况二:如果 ViewGroup 不拦截事件,事件顺利传递给子 View ,并且事件被子 view 消费掉的话,
// mFirstTouchTarget 会被赋值并指向子元素,mFirstTouchTarget != null 条件才成立;后续的 MOVE/UP 事件会走「代码片段四」进行传递
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;
}
......
// A. 往下走 DOWN 事件,会有两种情况
// 情况A1:onInterceptTouchEvent默认返回 false,不拦截;不取消,即默认的 if 条件满足,
// for 循环遍历所有的子 view,事件继续往下传递;注意:子 View 是否消费 DOWN 事件,
// 会影响到后续的 MOVE UP等事件的传递情况(即会影响到情况 B)
// 情况A2:如果我们重写 ViewGroup#onInterceptTouchEvent(MotionEvent)并返回 true,
// 那么这个 if 不满足,就会走下面 ViewGroup 自己的 OnTouchEvent()方法 (即代码片段三)
// B. 后续的MOVE/UP等事件 走到这里时,if条件不满足,又分两种情况:
// 情况B1:如果子 View 消费了 DOWN 事件,那么会直接走到 「代码片段四 」,进行事件的传递
// 情况B2:如果子 View 不消费 DOWN 事件,那么事件就会交给父View 处理,会走「代码片段三 」
TouchTarget newTouchTarget = null; // TouchTarget在这里声明
// 先看不拦截事件的情况
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 除了 DOWN 事件,后续的 MOVE 和 UP 事件进不来
final int childrenCount = mChildrenCount;
// DOWN事件走到这里,如果同时 ViewGroup 有子 View,就继续往下走了
if (newTouchTarget == null && childrenCount != 0) {
final View[] children = mChildren;
// 遍历所有的子 View,
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
......
// 剔除中间几行不重要的代码,删掉的代码直白的说下:如果当前的某个 view 处于获取到焦点状态,
// 那么优先把这个事件传递给它,如果该 View 不消费,不处理的话,事件就继续正常分发下去
// 重点来了:这个地方分为2种情况,取决于dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)返回值
// 情况1:返回 true,表示子 View 消费了事件(先别管是怎么消费的)
// 情况2:返回 false,表示子 View 不消费事件,if 条件不满足,这个时候mFirstTouchTarget == null,就不会被赋值
// 这里先分析 假设消费 true 的情况,那么 if 满足,正常进入,这时候 child!=null
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { //调用一 该 if 条件具体源码及分析 在下面
......
// 下面这句代码执行完 mFirstTouchTarget != null
// 同时 newTouchTarget != null,跳出循环
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
......
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// 代码片段三
// 这里有3种情况,都会走到这里
// 1. ViewGroup 主动拦截事件
// 2. ViewGroup 没有子 View
// 3. ViewGroup 有子 View,但是都不消费事件 dispatchTransformedTouchEvent() 返回了 false
// 注意这个时候,参数三 child传的为 null -> 会调用 ViewGroup.onTouchEvent(MotionEvent)
// 调用二
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// intercepted = false 的情况会 即 ViewGroup#onInterceptTouchEvent(ev) 返回了 false,不拦截事件,同时事件正常的传递到了目标子 View
// 代码片段四
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 子 View 消费了 DOWN 事件,那么后续的 MOVE/UP 等事件,就是在这里传递给对应的子 View 的
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
}
......
return handled;
}
默认第一次,就是DOWN事件传递到代码片段二的时候,if 满足条件,同时mFirstTouchTarget == null;是否拦截事件,这时候取决于 FLAG_DISALLOW_INTERCEPT,默认disallowIntercept = false,直接会走 ViewGroup#onInterceptTouchEvent(MotionEvent),这个值是子 View通过调用 requestDisallowInterceptTouchEvent(true) 来改变的,子 View 一旦调用设置后,ViewGroup将无法拦截除了 DOWN 以外的其它事件。
之所以说除了 DOWN 以外的其它事件,是因为每次当 DOWN 事件传递到 ViewGroup 的 dispatchTouchEvent()时候,会调用「代码片段一」 resetTouchState();
标记位 FLAG_DISALLOW_INTERCEPT会被重置,这将导致子 View 中设置的这个标记无效;
所以,当 DOWN 事件传递到 ViewGroup 时,ViewGroup 总是会调用自己的 onInterceptTouchEvent() 来询问是否要拦截事件;
/** 常量的声明 转为二进制位:1000 0000 0000 0000
* When set, this ViewGroup should not intercept touch events.
* {@hide}
*/
protected static final int FLAG_DISALLOW_INTERCEPT = 0x80000;
// 子 View 调用,从而影响到父 View 是否能拦截事件
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
......
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
image
# ViewGroup#onInterceptTouchEvent(MotionEvent)
/**
* 可以实现该方法拦截所有的触摸事件,
* 返回 true,就会调用自己的onTouchEvent()
* 返回 false,有可能会调用子 View 的 onTouchEvent()
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
/**
* View 的事件分发:把MotionEvent 传递给相关的 childview,如果传递过来的 child == null,那么该 ViewGroup 的onTouchEvent(MotionEvent)方法就会被调用
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// DOWN 事件传递过来,if 不成立
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;
}
......
// 重点 这里有2种情况
// 情况1:当前 ViewGroup 是有子 View 的情况,此时传过来的 child != null,由此事件就传递到了具体的 childView 了,事件消耗与否取决于子 View
// 情况2:ViewGroup 拦截了事件,此时传递过来的 child == null
if (child == null) {
// 调用 super 的方法,最终会调用到 ViewGroup#onTouchEvent(MotionEvent)里
handled = super.dispatchTouchEvent(transformedEvent);
} else {
......
// 事件继续向下传递,由此事件已经从父 View 传递给了下一层 View,接下来的传递过程与顶级父 View 的传递过程是一致的,如此循环,完成整个事件的分发
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}
接下来继续往下分析 View.dispatchTouchEvent(MotionEvent)
View 的事件分发过程
/**
* View#dispatchTouchEvent(MotionEvent)
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
* @return True 表示当前 view 消费掉此事件
*/
public boolean dispatchTouchEvent(MotionEvent event) {
......
boolean result = false;
final int actionMasked = event.getActionMasked();
if (onFilterTouchEventForSecurity(event)) {
// 默认情况下都是会进来的;除非当前的 Window 被另一个可见的 Window 部分或者全部遮挡掉了,就会丢弃掉该事件
// 重中之重的地方来了;首先 ListenerInfo 是 View.java 里边一个静态类,里边封装了各种监听事件,比如 焦点变化的监听,滑动状态改变的监听 点击、长按事件、触摸事件的监听等等
//这里我们如果设置了 触摸事件,那么就会回调到触摸事件的 onTouch()方法中,根据返回值决定了 result 的值
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 情况一:如果设置了触摸事件,并且li.mOnTouchListener.onTouch(this, event)返回 true,表示事件被消费掉了,不再往下传递;那么 View.onTouchEvent(MotionEvent)方法就不会执行
// 情况二:如果设置了触摸事件,但是返回了 false,或者用户就没有设置触摸事件,那么最终就会回调到 onTouchEvent(event)方法
// 由此我们可以看到设置的触摸事件的优先级会高于OnTouchEvent(),给用户提供一个外部处理触摸事件的回调,可以提前做一些事情
if (!result && onTouchEvent(event)) {
result = true;
}
}
......
return result;
}
说明:
如果是从上边 「调用二」super.dispatchTouchEvent(transformedEvent) 调用的,那么就会调用 ViewGroup.onTouchEvent(MotionEvent),事件就交给 ViewGroup 自己处理;
如果是具体的 View 调用的「调用一」,那么事件就相当于传递到末端了,是否消费事件取决于 onTouch(View, MotionEvent) 和 onTouchEvent(MotionEvent)这两个方法的返回值;
如果最终的 result = true,那么就表示事件被子 View 消费掉了,事件不再往上传递(子 View 都不处理,那么事件就会一层一层的传递给父 View ,父 View 的 OnTouchEvent(MotionEvent) 就会被调用)
View 的点击事件处理
我们给 View 设置的点击事件到目前为止还没有看到,实际上,点击事件的回调时机是在 View#onTouchEvent(MotionEvent) 的 case MotionEvent.ACTION_UP:
手指抬起中进行回调的,简单的贴下代码
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getAction();
// 值得一提的是,只要设置了点击事件或者长按事件,就会改变 viewFlags 的值,clickable = true
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:
......
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
// 点击事件
if (!post(mPerformClick)) {
performClick();
}
}
......
break;
......
}
// 只要是能够进入 if 条件,默认都是返回 true 的,即消费掉这个事件
return true;
}
总结
分析了源码可以得到的结论
-
ViewGroup 默认不拦截任何事件,
ViewGroup#onInterceptTouchEvent(MotionEvent)
方法默认返回 false -
ViewGroup#onInterceptTouchEvent(MotionEvent)
是用来拦截某个事件的,如果当前 ViewGroup 拦截了某个事件(这个事件可能是 DOWN 或者其它事件),那么同一个事件序列中的事件再次传递过来时,该方法不会被再次调用,而是直接调用了ViewGroup#onTouchEvent(MotionEvent)
方法 -
事件传递到某个 View,如果它不消耗 DOWN 事件( onTouchEvent(MotionEvent) 返回了 false),那么后续的MOVE UP 等事件都不会再传递给它,并且事件将重新交由它的父 View 去处理,即父 View 的 onTouchEvent(MotionEvent)会被调用。(mFirstTouchTarget == null,走「代码片段三」的情况)
-
View 没有onInterceptTouchEvent(MotionEvent) 方法,一旦事件传递给它,那么它的 onTouchEvent(MotionEvent) 就会被调用
-
onTouchEvent(MotionEvent) 返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前 View 无法再次接收到其它事件
-
如果给 View 设置了setOnTouchListener() 和 onTouchEvent(),要想两者都可以执行,触摸事件 onTouch() 返回 false 即可
事件的传递过程文字描述
事件的传递顺序是 Activity -> Window -> ViewGroup -> View,当一个点击事件产生后,就会按如下顺序传递到 父 ViewGroup 的 dispatchTouchEvent(),如果 ViewGroup 拦截事件,那么事件会直接传递到 ViewGroup 的 onTouchEvent() 方法中;如果父 ViewGroup 不拦截事件,那么就会 for 循环遍历子 View,进行事件的下发,如果事件往下传递的过程中有一个子 View 的 onTouchEvent() 返回 true消费掉事件,那么事件到此为止;
考虑一种情况:所有子 View 都不处理事件,那么这个事件就会传递到父 View 的 onTouchEvent(),如果父 View 也不消费事件,那么这个事件就会一级一级往上一个父 View 传递,如果所有的 View 都不消费事件,那么这个事件最终会传递到 Activity 的 onTouchEvent(MotionEvent),最终 Activity 默认也是把该事件丢弃掉的,但是源码里边给了我们一个思路,就是说如果事件是在 Window 的边界外产生的,那么我们就可以重写 Activity.onTouchEvent(MotionEvent) 来处理
事件传递机制流程图
最后贴一张图 Android 事件分发的流程图:
image伪代码展示事件分发拦截和消费三者的关系
一段有意思的伪代码[伪代码来源于艺术探索一书]:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false;
if (onInterceptTouchEvent(event)) {
consume = onTouchEvent(event);
} else {
consume = childView.dispatchTouchEvent(event);
}
return consume;
}
微信扫码关注,接收更多更全文章