1、触摸消息总体派发过程
和按键消息派发过程类似,当底层读取到触摸消息,会回调ViewRoot内部的mInputHandler对象的dispatchMotion()方法来发送一个异步消息,该消息被函数deliverPointerEvent()处理。执行完该函数后,调用finishInputEvent()向消息获取模块发送一个回执,以便进行下一次消息派发。
下面来分析deliverPointerEvent()函数的具体过程,代码如下:
private void deliverPointerEvent(MotionEvent event) {
if (mTranslator != null) {
mTranslator.translateEventInScreenToAppWindow(event);
}
boolean handled;
if (mView != null && mAdded) {
// enter touch mode on the down
boolean isDown = event.getAction() == MotionEvent.ACTION_DOWN;
if (isDown) {
ensureTouchMode(true);
}
if(Config.LOGV) {
captureMotionLog("captureDispatchPointer", event);
}
if (mCurScrollY != 0) {
event.offsetLocation(0, mCurScrollY);
}
if (MEASURE_LATENCY) {
lt.sample("A Dispatching TouchEvents", System.nanoTime() - event.getEventTimeNano());
}
handled = mView.dispatchTouchEvent(event);
if (MEASURE_LATENCY) {
lt.sample("B Dispatched TouchEvents ", System.nanoTime() - event.getEventTimeNano());
}
if (!handled && isDown) {
int edgeSlop = mViewConfiguration.getScaledEdgeSlop();
final int edgeFlags = event.getEdgeFlags();
int direction = View.FOCUS_UP;
int x = (int)event.getX();
int y = (int)event.getY();
final int[] deltas = new int[2];
if ((edgeFlags & MotionEvent.EDGE_TOP) != 0) {
direction = View.FOCUS_DOWN;
if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
deltas[0] = edgeSlop;
x += edgeSlop;
} else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
deltas[0] = -edgeSlop;
x -= edgeSlop;
}
} else if ((edgeFlags & MotionEvent.EDGE_BOTTOM) != 0) {
direction = View.FOCUS_UP;
if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
deltas[0] = edgeSlop;
x += edgeSlop;
} else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
deltas[0] = -edgeSlop;
x -= edgeSlop;
}
} else if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
direction = View.FOCUS_RIGHT;
} else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
direction = View.FOCUS_LEFT;
}
if (edgeFlags != 0 && mView instanceof ViewGroup) {
View nearest = FocusFinder.getInstance().findNearestTouchable(
((ViewGroup) mView), x, y, direction, deltas);
if (nearest != null) {
event.offsetLocation(deltas[0], deltas[1]);
event.setEdgeFlags(0);
mView.dispatchTouchEvent(event);
}
}
}
}
}
1、进行物理像素到逻辑像素的转换。
2、如果是Down消息,调用ensureTouchMode(true)进入触摸模式
3、event.offsetLocation(0, mCurScrollY)方法中,mCurScrollY记录了视图在屏幕坐标中的Y轴滚动,该方法将屏幕坐标转换为视图坐标
4、调用mView.dispatchTouchEvent(event)将消息派发给根视图,该函数内部会把消息派发到整个View树。对于Activity包含的窗口,根视图就是PhoneWindow中的DecorView,而对于非应用窗口,根视图就是一个普通的ViewGroup。
5、如果根视图及子视图没有处理该消息,最后进行屏幕偏移,如果偏移后,匹配到了新视图,则将消息派发到该视图。
2、根视图内部消息派发过程
对于mView.dispatchTouchEvent()的派发过程。该函数是在ViewRoot中的deliverPointerEvent()方法中调用的。mView的类型是PhoneWindow中的DecorView或普通的ViewGroup。
下面贴出DecorView中dispatchTouchEvent 中的代码:
public boolean dispatchTouchEvent(MotionEvent ev) {
final Callback cb = getCallback();
return cb != null && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super
.dispatchTouchEvent(ev);
}
首先判断是否存在CallBack对象,这个对象就是Activity对象,如果有,则调用Activity的dispatchTouchEvent(),否则调用DecorView基类ViewGroup中的dispatchTouchEvent()。
在Activity中的dispatchTouchEvent()代码如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
(1)如果是DOWN消息,调用onUserInteraction(),便于在消息处理前做点什么。
(2)getWindow()返回的是PhoneWindow对象,调用superDispatchTouchEvent(ev),也就是将消息派发给根视图,继而派发到整个View树。
(3)如果Window类没有处理该消息,则调用onTouchEvent(),onTouchEvent()默认什么也没做。
3、ViewGroup内部消息派发过程
ViewGroup的内部处理逻辑也采用递归方式,但与按键处理的方式稍有不同。
触摸消息首先会发给View树的最后一个子视图,如果子视图没有处理,再派发给它的父视图处理。和按键消息的派发过程正好相反。
下面是ViewGroup的dispatchTouchEvent()方法,代码如下:
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();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
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
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;
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;
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);
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;
if (target == null) {
// We don't have a target, this means we're handling the
// event as a regular view.
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
}
// if have a target, see if we're allowed to and want to intercept its
// events
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
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);
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
return target.dispatchTouchEvent(ev);
}
(1)将布局坐标转换为视图坐标
(2)处理DOWN消息,判断视图坐标落在哪个子视图。
disallowIntercept代表不允许拦截触摸事件
如果disallowIntercept为true,则直接将消息传递给子视图,否则先调用onInterceptTouchEvent(),当该方法返回false时,也就是事件没有被用户拦截,则继续将事件传递给子视图。
然后遍历寻找子视图,调用child.getHitRect(frame),给frame赋值视图坐标范围,调用frame.contains(scrolledXInt, scrolledYInt)判断用户触摸点是不是落在了该视图上,如果是,通过ev.setLocation(xc, yc)给MotionEvent设置调整坐标,然后把事件传递给该子视图。
需要注意的是,这里遍历子视图,是从最顶层子视图开始的。对于找到的这个子视图调用child.dispatchTouchEvent(ev),如果事件被消费了,则会返回true,紧接着mMotionTarget = child,这样触摸事件的目标就找到了,最后调用target.dispatchTouchEvent(ev),把事件派发给这个View。如果返回的是false,说明这个child不是常规的view,而是一个ViewGroup,则继续进行递归调用,递归到最后,child的数量为0,则把这个ViewGroup也当成一个常规的View,调用super.dispatchTouchEvent(ev),把消息派发给了这个ViewGroup,这样你的ViewGroup也能执行onTouchEvent()等事件了。