很多Android开发者,对事件分发的流程都有大致了解。但很多人都只是停留在diaptchTouchEvent->onInterceptTouchEvent->onTouch->onTouchEvent这样的流程。
那么问题来了:
1、对应用来说,事件的起始位置从哪里开始?
2、事件的分发的整体流程是?
3、怎么样响应触摸事件?怎么不响应触摸事件?
4、嵌套的view,父级要求响应垂直滑动,子级要求响应水平滑动,如何实现?
下面我们对这些问题进行一一剖析:
1、对应用来说,事件的起始位置从哪里开始?
有的人说时候从Activity开始,那么事件又是怎么传到Activity的呢?估计很多人在被问到这个问题的时候都一脸懵逼,确实对我们应用开发者来说事件貌似是从Activity开始,最终也是传到Activity进行分发,或许我们只要关注Activity就可以了。但是对于应用而言,事情其实位置是从ViewRootImpl的dispatchInputEvent开始的,而从哪里进入到ViewRootImpl的dispatchInputEvent呢?目前初步查了网上的资源,大多都说是从硬件底层而来,我也查了ActivityThread、PhoneWindow没有发现调用ViewRootImpl的dispatchInputEvent的地方,所以我们先估摸这是从硬件底层调用了ViewRootImpl的dispatchInputEvent方法。(如果有知道的朋友还望指点一二)
2、事件的分发的整体流程是?
步骤一中分析了事件分发的起始位置,那么我们从这个起始位置开始一一分析:
- 第一阶段:ViewRootImpl事件分发
事件起始的实现:
public void dispatchInputEvent(InputEvent event) {
//调用重载方法进行分发事件
dispatchInputEvent(event, null);
}
public void dispatchInputEvent(InputEvent event, InputEventReceiver receiver) {
//切换到UI线程处理事件
SomeArgs args = SomeArgs.obtain();
args.arg1 = event;
args.arg2 = receiver;
Message msg = mHandler.obtainMessage(MSG_DISPATCH_INPUT_EVENT, args);
msg.setAsynchronous(true);
mHandler.sendMessage(msg);
}
dispatchInputEvent的工作主要是切换到UI线程进行分发事件,我们看下MSG_DISPATCH_INPUT_EVENT的分支实现:
case MSG_DISPATCH_INPUT_EVENT: {
SomeArgs args = (SomeArgs) msg.obj;
InputEvent event = (InputEvent) args.arg1;
InputEventReceiver receiver = (InputEventReceiver) args.arg2;
//调用enqueueInputEvent将事件加入队列中排队处理
enqueueInputEvent(event, receiver, 0, true);
args.recycle();
} break;
enqueueInputEvent的实现还是比较简单的,先看下其实现:
void enqueueInputEvent(InputEvent event,
InputEventReceiver receiver, int flags, boolean processImmediately) {
adjustInputEventForCompatibility(event);
//将事件包装成QueuedInputEvent 对象
QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);
QueuedInputEvent last = mPendingInputEventTail;
//将事件加入队列的队尾中
if (last == null) {
mPendingInputEventHead = q;
mPendingInputEventTail = q;
} else {
last.mNext = q;
mPendingInputEventTail = q;
}
mPendingInputEventCount += 1;
Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
mPendingInputEventCount);
if (processImmediately) {
//因为我们已经在Ui线程,所以立即处理事件
doProcessInputEvents();
} else {
//非UI线程,切换到UI线程,最终调用doProcessInputEvents进行事件处理
scheduleProcessInputEvents();
}
}
enqueueInputEvent的核心工作是将事件包装成QueuedInputEvent ,再加入队列的末尾,最后调用doProcessInputEvents处理事件,我们接着看doProcessInputEvents的实现:
void doProcessInputEvents() {
// Deliver all pending input events in the queue.
while (mPendingInputEventHead != null) {
//遍历事件队列
//取出队列头部
QueuedInputEvent q = mPendingInputEventHead;
mPendingInputEventHead = q.mNext;
if (mPendingInputEventHead == null) {
mPendingInputEventTail = null;
}
q.mNext = null;
mPendingInputEventCount -= 1;
Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
mPendingInputEventCount);
long eventTime = q.mEvent.getEventTimeNano();
long oldestEventTime = eventTime;
if (q.mEvent instanceof MotionEvent) {
MotionEvent me = (MotionEvent)q.mEvent;
if (me.getHistorySize() > 0) {
oldestEventTime = me.getHistoricalEventTimeNano(0);
}
}
mChoreographer.mFrameInfo.updateInputEventTime(eventTime, oldestEventTime);
//对事件进行分发
deliverInputEvent(q);
}
//...省略
}
doProcessInputEvents的核心工作是遍历事件队列,取出一个一个事件通过deliverInputEvent方法进行分发,我们看下deliverInputEvent方法的实现:
private void deliverInputEvent(QueuedInputEvent q) {
//...省略
InputStage stage;
if (q.shouldSendToSynthesizer()) {
stage = mSyntheticInputStage;
} else {
stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
}
//...省略
if (stage != null) {
handleWindowFocusChanged();
//使用InputStage进行分发
stage.deliver(q);
} else {
finishInputEvent(q);
}
}
deliverInputEvent使用InputStage进行事件的分发,而InputStage是个抽象类,其有很多实现类,其中处理Touch事件是ViewPostImeInputStage,所以最终是由ViewPostImeInputStage进行分发的。而InputStage在分发事件是,使用了模板设计模式,我们先看下InputStage的deliver方法的实现:
public final void deliver(QueuedInputEvent q) {
if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) {
forward(q);
} else if (shouldDropInputEvent(q)) {
//事件已结束
finish(q, false);
} else {
//事件处理
apply(q, onProcess(q));
}
}
ViewPostImeInputStage重载了onProcess方法,我们看下其实现:
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {
//按键事件
return processKeyEvent(q);
} else {
final int source = q.mEvent.getSource();
if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
//点击、touch事件
return processPointerEvent(q);
} else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
return processTrackballEvent(q);
} else {
return processGenericMotionEvent(q);
}
}
}
onProcess方法针对不同的时间调用不同的方法进行处理相应的逻辑,而我们目前分析的是touch事件,所以我们看下processPointerEvent的实现:
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
//...省略
boolean handled = mView.dispatchPointerEvent(event);
//...省略
}
processPointerEvent的核心调用了mView的dispatchPointerEvent进行touch事件的分发,而mView是DecorView,至于为什么是DecorView可以查看Android View绘制流程,这篇文章有说明。
这里分析到了调用DecorView的dispatchPointerEvent,而DecorView并没有重写dispatchPointerEvent,其父类FrameLayout-》ViewGroup也没有重写,所以我们进入View的dispatchPointerEvent实现:
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
//是触摸touch事件
return dispatchTouchEvent(event);
} else {
//非触摸touch事件
return dispatchGenericMotionEvent(event);
}
}
View的dispatchPointerEvent有调用dispatchTouchEvent,而DecorView重写了dispatchTouchEvent,我们看下DecorView的dispatchTouchEvent实现:
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
DecorView的dispatchTouchEvent有调用window.callback对象的dispatchTouchEvent。而window的callback对象其实是activity,是在activity的attch方法中将activity设置为window的callback,这里不具体展开说明。
第一阶段已经剖析完。
- 第二阶段:Activity事件分发
第一阶段中剖析时,事件流入到Activity的dispatchTouchEvent,我们顺着此流程继续往下剖析:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//屏保阶段用于响应按键的输入
onUserInteraction();
}
//调用window的superDispatchTouchEvent方法
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//如果window的并不拦截消耗事件,则传递给Activity的onTouchEvent进行处理
return onTouchEvent(ev);
}
Activity dispatchTouchEvent的核心思想是:将事件分发给window,如果window拦截消耗事件,则完成事件的分发;如果window不拦截消耗事件,则将事件传递给Activity的onTouchEvent进行处理。
其中window的实现类是PhoneWindow,我们来看下PhoneWindow的superDispatchTouchEvent:
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
Window的superDispatchTouchEvent又调用DecorView的superDispatchTouchEvent,我们继续看下DecorView的superDispatchTouchEvent:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView的superDispatchTouchEvent调用的是父类的dispatchTouchEvent,其父类FrameLayout没有重写dispatchTouchEvent,所以进入的是ViewGroup的dispatchTouchEvent。
第二阶段剖析完。
- 第三阶段:ViewGroup事件分发
第二阶段分析时,事件进入了ViewGroup的dispatchTouchEvent:
public boolean dispatchTouchEvent(MotionEvent ev) {
//...省略
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//down事件或者已经有子view要拦截消耗事件
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.
//不是down事件并且没有子view要拦截消耗事件,则代表自己处理
intercepted = true;
}
if (!canceled && !intercepted) {
//事件没有取消并且事件没有被当前view拦截
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.
//构建按Z坐标排序的子view列表
final ArrayList preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
//子view列表
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
//遍历子view列表
//获取子view的坐标
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
//根据坐标获取子view
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//...省略
//将事件传递分发给子view
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
//如果子view拦截消耗事件
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;
}
//保存坐标、子view
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
//没有子view拦截消耗事件,则分发给自己进行处理
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
//遍历事件触摸点对应的view进行分发
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
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;
}
}
}
上面的代码,如果将事件分发给子view时,调用dispatchTransformedTouchEvent进行分发;如果自己拦截消耗事件或者子view不拦截消耗此事件,那么也是调用dispatchTransformedTouchEvent进行分发,只是此时传递的child为空,我们看下dispatchTransformedTouchEvent的实现:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
//child为空代表自己拦截消费或者子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 {
//将事件分发给子view
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
//...省略
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
//代表事件被当前的child处理
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
//自己处理此事件
handled = super.dispatchTouchEvent(event);
} else {
//将事件分发给子view
//计算相对父控件的位置
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
//...省略
return handled;
}
dispatchTransformedTouchEvent的工作是将事件交给自己处理或者分发给子view。
ViewGroup进行进行事件分发时,其核心思想是:
1、 调用onInterceptTouchEvent判断自己是否要拦截消耗此事件
2、 如果自己不拦截消耗事件,则遍历子view列表,根据事件坐标找到对应的子view,然后将事件分发给此view,如果此子view是ViewGroup,那么又走ViewGroup的dispatchTouchEvent逻辑;如果此子view是View,那么进入第四阶段
3、如果自己拦截消耗事件那么调用父类的dispatchTouchEvent,其父类是View,而View的事件分发dispathcTouchEvent请看第四阶段
- 第四阶段: View事件分发
第三阶段分析时,如果自己拦截消费此事件或者子view不拦截消费此事件,那么就会进入View的dispatchTouchEvent,我们来看下其实现:
public boolean dispatchTouchEvent(MotionEvent event) {
//...省略
if (onFilterTouchEventForSecurity(event)) {
//...省略
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
//如果当前view enable并且设置了onTouchListener,且此onTouchListener消耗了此事件
result = true;
}
//没有设置onTouchListener,且onTouchListener不消耗此事件,则将事件传给onTouchEvent
if (!result && onTouchEvent(event)) {
result = true;
}
}
//...省略
return result;
}
View的dispatchTouchEvent优先将事件传递给onTouchListener,如果onTouchListener消耗此事件,则直接放回true;如果没有设置onTouchListener或者onTouchListener没有消耗此事件,则将事件传递给onTouchEvent,我们接着看onTouchEvent的实现:
public boolean onTouchEvent(MotionEvent event) {
//...省略
//判断是否可点击
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
//如果此view disabled,则不响应此事件并返回clickable值
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
if (mTouchDelegate != null) {
//设置了touch代理
if (mTouchDelegate.onTouchEvent(event)) {
//touch代理消耗了此事件,则返回true
return true;
}
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
//当前view可点击或者可以显示工具提示或者允许长按
//工具提示就是长按时在其顶部或者底部弹出的菜单工具栏
switch (action) {
case MotionEvent.ACTION_UP:
//松开事件
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
//弹出工具提示栏
handleTooltipUp();
}
if (!clickable) {
//不可点击,移除相关的消息callback
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
//...省略
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
//响应点击事件
if (!post(mPerformClick)) {
performClickInternal();
}
//...省略
}
//...省略
break;
case MotionEvent.ACTION_DOWN:
//按下事件
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (!clickable) {
//不可点击
//检验是否可以响应长按事件,如果可以那么延迟500ms响应长按事件
checkForLongClick(0, x, y);
break;
}
if (performButtonActionOnTouchDown(event)) {
break;
}
//...省略
break;
case MotionEvent.ACTION_CANCEL:
//事件已经取消
//...省略
break;
case MotionEvent.ACTION_MOVE:
//事件移动中
//...省略
break;
}
return true;
}
return false;
}
onTouchEvent的核心工作是:
1、先判断view是否enable,如果不enable则不响应事件,并返回clickable的值
2、如果设置了touch的代理,并且touch代理消耗了此事件则返回true代表消耗了事件
3、如果view不可点击,则返回false,代表不消耗此事件
4、如果view可点击,则返回true,代表消耗此事件。如果设置了长按事件,则在按下事件中延迟500ms响应长按事件;响应点击事件是在松开事件时响应的。
ok,Android的事件分发的整体流程就此剖析结束。
3、怎么样响应触摸事件?怎么不响应触摸事件?
要响应事件,根据View的事件分发流程可以有几种解决方案:
a、父控件拦截事件,将事件传递给view进行处理
b、自己拦截事件:
- 可以通过dispatchTouchEvent直接返回true,事件处理再dispatchTouchEvent中处理
- onInterceptTouchEvent返回true,并在onTouch或者onTouchEvent中消耗此事件,并返回true
不响应最简单的是clickable为false或者enable为false、或者不可见
4、嵌套的view,父级要求响应垂直滑动,子级要求响应水平滑动,如何实现?
a、父级控件拦截事件,滑动时判断水平滚动距离大于垂直滑动距离,则将事件传递给子view响应水平滑动;否则父级控件自己响应垂直滑动
b、子级在onInterceptTouchEvent中判断水平滚动距离大于垂直滑动距离,则拦截消耗此事件