欲哭无泪呀~~
老生常谈下 Android 的事件分发机制。面试经常被问到事件分发机制,看过好多篇博客、文章了,但被问起来总是系统的答不上来。脑海中只记得三个主要的函数,写代码时候也是多次测试去处理(其实大多数都是 百度 解决),这里自己记录一下。
我的粗略回答
Activity -> Window -> DecorView(ViewGroup) -> View
dispatchTouchEvent -> onInterceptTouchEvent -> onTouchEvent
OnTouchListener -> OnTouchEvent -> OnClick
汗啊、、
前言
所谓点击事件(Touch)的事件分发,其实就是对 MotionEvent (Touch 的封装)事件的分发过程,即当一个 MotionEvent 产生以后,系统需要把这这个事件传递给那个具体的 View。这个传递的过程就是事件分发过程。
MotionEvent
触摸状态 | 释义 |
---|---|
ACTION_DOWN | 按下 手势开始 |
ACTION_UP | 抬起 手势结束 |
ACTION_CANCLE | 取消 当前手势已终止 |
ACTION_MOVE | 移动 在down和up之间发生 |
流程图
一个 MotionEvent 产生后,按 顺序传递,View 传递过程就是事件分发(可以理解为责任链设计模式)。
三个主要方法
方法 | 作用 | 调用时刻 | 返回结果 |
---|---|---|---|
dispatchTouchEvent | 用来进行事件分发 | 如果事件能够传递给当前View/ViewGroup就会被调用 | 表示是否消耗当前事件 |
onInterceptTouchEvent | 用于判断是否拦截事件(ViewGroup有此方法,View没有) | 在dispatchTouchEvent()方法中调用 如果当前View拦截了某个事件,那么同一事件序列将不再会调用此方法 | 表示是否拦截当前事件 |
onTouchEvent | 用来处理点击事件 | 在dispatchTouchEvent()方法中调用,不消耗当前事件,那么当前View在同一事件序列中无法再接受到事件 | 是否处理了当前事件,未处理则传递给父容器处理 |
源码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 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();清空 mFirstTouchTarget
resetTouchState();
}
// Check for interception.
// 检查是否拦截事件
// 1.当事件为 ACTION_DOWN 时
// 2. 当ViewGroup不拦截事件交给子元素处理 条件成立 即mFirstTouchTarget != null
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 调用onInterceptTouchEvent(ev)开始进行事件拦截
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;
}
......
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
// 非 MotionEvent.ACTION_CANCEL 并且没有拦截事件
// 进入 if 语句,判断条件为没有对事件进行拦截 , onInterceptTouchEvent 返回 false,同时事件没有结束。对 ViewGroup 的子元素进行遍历
if (!canceled && !intercepted) {
// If the event is targeting accessiiblity focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
// 对子元素进行遍历
for (int i = childrenCount - 1; i >= 0; i--) {
// 1.子元素是否在做动画
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
// 2.事件左边是否落在子元素区域内
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// 接受点击事件的View根据1.2条件判断
// 是否能够接受点击事件
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
//不符合要求 执行下一个循环
continue;
}
......
// 通过判断,将 ViewGroup 的子元素进行遍历,找到能够处理点击事件的子元素并调用 dispatchTransformedTouchEvent() 方法,进行事件的分发。
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
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;
// 条件满足跳出循环
// 这里是跳出内部for循环不是外部的
// 其实就是break的用法
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// 当子元素能够处理点击事件,就调用 addTouchTarget() 方法,对 mFirstTouchTarget() 方法进行赋值。
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
......
}
}
}
// if 语句不成立表示对事件进行拦截。并且mFirstTouchTarget == null没有在子元素的遍历中赋值,即条件成立。执行dispatchTransformedTouchEvent()方法。
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
......
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// MotionEvent.ACTION_UP 后 清空mFirstTouchTarget
// 在同一事件系列结束后调用resetTouchState();对mFirstTouchTarget清空还原。
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
......
}
注释比较详细,下面总结一下:
- 一个 Touch 事件序列只能一个 View 进行拦截且消耗。如果拦截事件,就不会进入 if 语句对子元素进行遍历与事件分发。同时如果拦截了某一事件,那么统一事件序列内的所有事件都交给它处理。
- 某个 View 一旦拦截事件,那么这一事件序列只能有它来处理。同时我们知道既然拦截就无法进入分发判断,那么 mFirstTouchTarget 就无法被赋值,那么开始的条件就不成立。所以调用 onInterceptTouchEvent() 不会再被调用。总的来说,也是 1 所说的如果拦截那么同一事件序列,那么所有事件都间给当前 View 处理。
- 再dispatchTransformedTouchEvent() 方法中,当 dispatchTouchEvent() 的返回值与 dispatchTransformedTouchEvent() 返回值相同。这样会直接影响 ViewGroup#dispatchTouchEvent() 返回值(两者相同)。也就是说:View 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件( onTounchEvent() 返回 false)。那么统一事件序列的其他事件都不会交给他处理。并会重新交给父元素(注意是父元素,不是父类)去处理,即父元素 onTounchEvent() 会被调用。
- onInterceptTouchEvent() 默认返回false,即默认不拦截任何事件。
- View 没有 onInterceptTouchEvent()。一旦事件传递给他,那么他的onTouchEvent就会调用。