根View内部消息派发过程
在 PhoneWindow.DecorView 中,首先判断是否存在 Callback 对象,它和按键消息派发时的Callback对象一样,就是Activity类。如果没有Callback对象,则直接调用 DecorView 基类的 ViewGoup 中的 dispatchTouchEvent()函数。
Activity.dispatchTouchEvent()的过程
1. 如果是ACTION_DOWN消息,则调用 onUserInteracction(),给应用程序一个机会,以便在消息处理前做点什么,该函数默认为空实现。
2. 调用所包含的 Window 对象的 superDispatchTouchEvent()。
3. 如果Window类没有消耗该消息,则调用 onTouchEvent(),该函数默认也是空实现,是给应用程序一个处理消息的机会。
Window类中的superDispatchTouchEvent()
此时Window类的实现就是PhoneWindow类,该函数继而调用 mDecor的superDispatchTouchEvent(),而在DecorView的函数中又调用 super.dispatchTouchEvent(),即ViewGroup 的 dispatchTouchEvent。
ViewGroup 内部消息派发过程——ViewGroup.dispatchTouchEvent()
@Override
public boolean dispatchTouchEvent(MotionEvent ev){
if (!onFilterTouchEventForSecurity(ev)) {
return false;
}
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
//1. 将布局坐标转换为视图坐标。
// 1. 视图坐标中,视图大小取决于视图本身包含多少内容,不受物理屏幕大小限制。而布局坐标则是有限制的,它是指父View给某子View分配的布局(layout)大小,超过这个大小的区域将不能显示到父视图的区域中。
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//2. 处理DOWN消息,其作用是判断该视图坐标落到了哪个子视图中。
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
// this is weird, we got a pen down, but we thought it was
// already down!
// XXX: We should probably send an ACTION_UP to the current
// target.
mMotionTarget = null;
}
// If we're disallowing intercept or if we're allowing and we didn't
// intercept
// 2.1. 首先判断该ViewGroup本身是否被禁止获取TOUCH消息,如果没有禁止,并且回调函数onInterceptTouchEvent()中没有消耗该消息,则意味着该消息可以传递给子视图。如果子视图消耗了该DOWN消息,则直接返回true。
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// reset this event's action (just to protect ourselves)
ev.setAction(MotionEvent.ACTION_DOWN);
// We know we want to dispatch the event down, find a child
// who can handle it, start with the front-most child.
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
// 2.2. 开始寻找子视图。调用child.getHitRect(frame) 函数获取该子视图在父视图中的布局坐标,即该ViewGroup为该child分配的位置是什么。参数frame执行完毕后的位置输出矩阵,得到该位置后,就可以调用frame.contain()方法判断该消息是否被包含到了该child中,如果包含,该child也是一个ViewGroup的话,则准备递归调用该child的DispatchTouchEvent(),在调用之前,首先需要把坐标重新转换到child的坐标系中。
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
// offset the event to the view's coordinate system
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
// 3. 完成了递归操作前的坐标转换工作,然后判断该child是否是ViewGroup类,如果是就递归到ViewGroup的dispatchTouchEvent(),重新上一步开始执行;如果child不是ViewGroup,而是一个View,则意味着递归调用的结束。
if (child.dispatchTouchEvent(ev)) {
// Event handled, we have a target now.
mMotionTarget = child;
return true;
}
// The event didn't get handled, try the next view.
// Don't reset the event's location, it's not
// necessary here.
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
//3. 如果是UP或者CANCEL消息,则清除mGroupFlags中的 FLAG_DISALLOW_INTERCEPT标识,即允许该ViewGroup截获消息。这就是为什么DOWN消息都能达到最底层的子View上的原因,其实该标志位是给ViewGroup一个控制的机会,其对应的方法为 ViewGroup.requestDisallowInterceptTouchEvent(boolean disallowIntercept)。
if (isUpOrCancel) {
// Note, we've already copied the previous state to our local
// variable, so this takes effect on the next event
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// The event wasn't an ACTION_DOWN, dispatch it to our target if
// we have one.
final View target = mMotionTarget;
//4. 判断target(View对象)变量是否为空
if (target == null) {
// We don't have a target, this means we're handling the
// event as a regular view.
//4.1. 如果为空代表所有的子窗口没有消耗掉该消息,所以该ViewGroup本身需要处理该消息。
// 4.1.1首先要还原消息的原始位置,因为在判断子View是否包含该消息中的位置的时候进行了从布局坐标到视图坐标的转换,所以需要把视图坐标重新转换为布局坐标,因为接下来要调用super.dispatchTouchEvent,即View类的dispatchTouchEvent(),View类中处理该函数时,需要用到布局坐标。
ev.setLocation(xf, yf);
// 4.1.2判断mPrivate
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
//4.1.3 Flags中是否包含CANCEL_NEXT_UP_EVENT标识,如果存在该标识,则直接将消息的action类型改为ACTION_CANCEL。
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
//4.1.4 直接调用super.dispatchTouchEvent(),并返回其执行结果,也就是View的方法,该函数的内部仅仅是回调onTouchEvent以及TouchListener的注册事件(onTouch方法)。
return super.dispatchTouchEvent(ev);
}
// 4.2. 如果匹配到了某个子View,并且该子View消耗掉了消息后,会将该子View赋值给父View的mMotionTarget变量中。
// if have a target, see if we're allowed to and want to intercept its
// events
//4.2.1 、如果target存在,并且变量 disallowIntercept 为false。
//5.1、在dispatchTouchEvent中,如果不允许截获消息的话,那么也就不会调用onInterceptTouchEvent()函数了。
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
// 5、disallowIntercept()为false,即允许截获。在默认情况下ViewGroup都是允许截获消息的,只有当该ViewGroup的子视图调用父视图的 requestDisallowInterceptTouchEvent() 时,方可禁止父视图再次截获消息,但每次UP消息或者CANCEL消息之后,该ViewGroup又会重新截获消息。
//5.2、如果允许,并且 onIntereptTouchEvent()消耗了该消息。
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
//5.3、将消息的action类型设置为CANCEL,即"取消"。
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
//5.4、目标视图内部能够根据该消息来取消之前可能存在的消息跟踪。说白了,也就是将CANCEL消息发送到目标视图中去了,目标消息根据CANCEL消息来进行后续的操作,也就是上面不会再给你发消息了。
if (!target.dispatchTouchEvent(ev)) {
// target didn't handle ACTION_CANCEL. not much we can do
// but they should have.
}
// clear the target
mMotionTarget = null;
// Don't dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal onTouchEvent().
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
// finally offset the event to the target's coordinate system and
// dispatch the event.
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
//6.检查target中是否声明过要取消随后的消息,即mPrivateFlags中是否包含CANCEL_NEXT_UP_EVENT
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
//6.1把消息的action值修改为CANCEL,置空mMotionTarget(当前的子View记录)变量,因为target不想处理接下来的消息了,那么也就可以认为没有target了。
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
//6.2最后调用目标view的.dispatchTouchEvent()来处理CANCEL消息。
return target.dispatchTouchEvent(ev);
}
布局坐标是ViewGroup属性用来判断该消息是否包含在子View中,视图坐标是用来确定该消息在视图中的位置
- onInterceptTouchEvent()是在ViewGroup中定义的,即只有ViewGrpup的子类能够重载该方法。
- onTouchEvent()有两个定义:
- 在View来中的定义,所有的View类的子类都可以重载该方法,包括ViewGroup。
- 在Activity中定义的,用户Activity可以重载该方法。
View系统的消息处理机制中,会先执行View内部的onTouchEvent,如果没有处理,才会调用Activity中的onTouchEvent()。
对于ViewGroup而言,在一般情况下都会先调用onInterceptTouchEvent()来询问自己是否需要该消息。只有当该函数没有消耗掉该消息,并且其包含的子视图也没有消耗掉该消息的时候,才会执行自己的onTouchEvent()。
而对于View来说,是没有onInterceptTouchEvent方法的。
并不是所有的消息处理过程都是先要调用onInterceptTouchEvent的
– 在DOWN消息来临时,并且ViewGroup允许截获消息时才会调用onInterceptTouchEvent()。
– 当ViewGroup中存在target对象,并且允许截获消息时才会调用onInterceptTouchEvent()。