前言:触摸事件是基于控件的位置进行派发的,它在ViewRootImpl中不需要进行是否派发到输入法的判断,并且触摸事件有个拆分机制;触摸事件的传递在日常开发中运用的比较多,所以,这一节会讲的非常详细,代码的注释也是非常的多;
本节分析的是:触摸事件的派发
触摸事件是基于控件的位置进行派发的,而且ViewRootImpl不需要将触摸事件派发到输入法,因为点击到输入法的事件在WMS中会直接派发给输入法,不需要ViewRootImpl转发;
我们来看下触摸事件的派发流程,还是先从这个代码开始:
stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
此时的输入事件是触摸事件,mEvent的类型是MotionEvent,并且输入事件的类型是触摸,所以:
stage = mFirstInputStage ;
那就先从EarlyPostImeInputStage开始分析了;
EarlyPostImeInputStage.java
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) {
//处理触摸事件
return processPointerEvent(q);
}
}
return FORWARD;
}
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
//当这是一个按下事件时,将会进入触摸模式,此时将会重新设置焦点控件
final int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_SCROLL) {
/*ensureTouchMode负责进入或退出触摸模式,它会重新设置焦点控件,并将触摸模式同步
到WMS,以便以后所建窗口可以从WMS得知当前工作在何种模式下*/
ensureTouchMode(event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
}
/*ViewRootImpl接收到的触摸事件位于窗口的坐标系下,将其派发给根控件时,需要将其坐标转换到根控件下;
根控件的坐标系与窗口坐标系的区别在于Y方向上的滚动量mCurScrollY*/
if (mCurScrollY != 0) {
event.offsetLocation(0, mCurScrollY);
}
return FORWARD;
}
点评:以上代码的逻辑,一是强制进入触摸模式,而是将触摸事件的坐标系转换到根控件下;
继续分析:
ViewPostImeInputStage.java
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) {
//处理触摸事件
return processPointerEvent(q);
} else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
//处理轨迹球事件
return processTrackballEvent(q);
} else {
//处理其他Motion事件,如游戏手柄等等
return processGenericMotionEvent(q);
}
}
}
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
//将触摸事件派发给控件树
boolean handled = mView.dispatchPointerEvent(event);
return handled ? FINISH_HANDLED : FORWARD;
}
View.java
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
//如果是触摸事件
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}
点评:
PointerEvent包含以MotionEvent .getAciton()进行区分的两种事件:
第一种是由dispatchTouchEvent()处理的ACTION_DOWN/UP/MOVE等实际接触到屏幕所产生的事件;
第二种是由dispatchGenericMotionEvent()处理的ACTION_HOVER_ENTER/UP/MOVE等未接触到屏幕所产生的事件;
由于第一种事件才是真正的触摸事件,所以我们只研究View.dispatchTouchEvent(),至于HOVER类型的实现,其派发思想与实现原理与触摸事件的派发十分相似,这里就不再赘述;
在讨论到触摸事件在控件树中的传递时,就会接触到MotionEvent,MotionEvent对触摸事件进行了封装,但由于多点触摸的存在,它的结构比较复杂,我们先分析一下它;
①MotionEvent继承自InputEvent,用于描述一次触摸的详细信息,如getAction()获取触摸的动作信息,getX()/getY()获取触摸的位置信息;
②由于多点触摸的存在,MotionEvent.getAction()所获取得动作信息是一个复合值,它在低8位描述了实际的动作如ACTION_DOWN/UP等,通过MotionEvent.getActionMasked()获取;高8位也就是9~16位描述了引发此事件的触控点从0开始的索引号,通过MotionEvent.getActionIndex()获取;在使用过程中,需要将这两个信息分离出来;
③虽然一个MotionEvent由一个触控点所触发,然而他却包含了所有触控点的位置信息,因此,MotionEvent.getX()/getY()方法可以接受触控点的索引号作为参数,以返回特定触摸点的触摸位置;
由于多点触摸的存在,我们不能通过触控点的索引号来追踪一个特定的触控点,因为一个触控点的索引并不是一成不变的,比如,第二根手指按下时,它的触控点索引为1,但随着第一根手指的抬起动作,第二根手指的触控点索引就变成了0,为了解决这一问题,我们需要用到触控点的ID,可以通过MotionEvent.getPointerId()方法获得;
④我们分析MotionEvent的流程一般是:首先通过MotionEvent.getActionMasked()获取它的实际动作,然后通过MotionEvent.getActionIndex()获取事件的触控点的索引号,然后根据索引号,通过MotionEvent.getX(index)/getY(index)获取触控点的坐标信息,以及通过MotionEvent.getPointerId(index)获取其ID,因此,索引号只不过是获取这些信息的一个临时工具而已;
⑤但是有一点要注意一下,当MotionEvent携带的动作是ACTION_MOVE时,其getAction()获得的动作信息并不包含索引,不过它仍然保存了所有触控点的位置/ID等信息,可以通过MotionEvent.getPointerCount()得知此时有多少触控点处于活动状态;从这里也可以看出,getAction()中所包含的触控点索引号其实是为了通知我们是否产生了新的的触控点(按下),或者某个触控点被移除(抬起);
⑥接下来我们分析下,从第一个手指按下开始,到最后一个手指抬起结束这一过程中,MotionEvent内部的变化历程;
(1)最简单的情况,就是单点触摸,手指按下,一个ACTION_DOWN开始,手指移动,即经过一系列的ACTION_MOVE,最后手指抬起,以一个ACTION_UP结束;
(2)多点触摸:第一个手指按下时,也是从一个ACTION_DOWN开始,手指移动,即经过一系列的ACTION_MOVE,此时第二根手指按下,产生一个ACTION_POINTER_DOWN,第二根手指移动,也就是经过了一系列的ACTION_MOVE,当两根手指中的其中一只抬起时,产生ACTION_POINTER_UP,当最后一根手指抬起时,以一个ACTION_UP结束;
在此过程中,触控点的索引随着当前处于活动状态的触控点的数量的变化而变化,但是触控点的ID 则 始终保持不变,可以通过事件所携带的触控点的ID追踪某一个触控点的始末,同时整个事件序列中每一个事件都包含了所有触控点的信息。
⑦多点触摸下事件序列的拆分:通过MotionEvent.split()方法,从当前的MotionEvent中产生一个新的仅包含特定触控点信息的MotionEvent;为什么要拆分MotionEvent,举个例子,一个ViewGroup中有两个View,两个手指分别按在两个View上,两个触控点都在ViewGroup之内,所以ViewGroup会收到一条双点的事件序列,当ViewGroup将触摸事件派发给两个View时,就必须将其拆分为两个单点序列,并分别派发给这两个View,一旦View决定了接受这一个触控点的ACTION_DOWN或者ACTION_POINTER_DOWN事件,表示控件将接受序列的所有后续事件,即便触控点移动到控件区域之外(这种情况还是经常碰到的);
⑧ACTION_ CANCEL也是触摸事件的结束标志。如,一个正在接受事件序列的控件从控件树中被移除,或者发生了Activity切换等, 那么它将收到ACTION_CANCEL而不是ACTION_UP,此时控件需要中断对事件的处理并将自己恢复到接受事件序列之前的状态。
说完了MotionEvent,接下来就是要继续研究View.dispatchTouchEvent()了;dispatchTouchEvent()方法在View和ViewGroup中有不同的实现;
先看在View中的实现:
View.java
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
final int actionMasked = event.getActionMasked();
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//先是尝试让此控件的OnTouchListener处理事件
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//如果OnTouchListener对事件不感兴趣,则尝试调用onTouchEvent处理事件
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
//如果view是不可用的,即DISABLED,返回clickable,即view是否可以被点击取决于
//它是否有CLICKABLE,LONG_CLICKABLE以及CONTEXT_CLICKABLE属性
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
return clickable;
}
//如果是View可点击的,则处理相应的事件
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
//手指抬起动作
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
//view尝试获取焦点
focusTaken = requestFocus();
}
if (prepressed) {
//设置按压状态
setPressed(true, x, y);
}
/*当view处理了OnLongClickListener的onLongClick方法之后,mHasPerformedLongPress 便为true,表示不再处理点击事件*/
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
//移除掉长按事件,即移除OnLongClickListener.onLongClick();
removeLongPressCallback();
//view只有处于按下状态时才可以执行单击操作
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
/*如果OnClickListener的onClick方法没有执行,执行OnClickListener的onClick方法*/
if (!post(mPerformClick)) {
//播放点击音乐,并且执行OnClickListener的onClick方法
performClick();
}
}
}
removeTapCallback();
}
break;
//手指按下事件
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
//设置按压状态
setPressed(true, x, y);
/*准备执行长按事件,在系统默认时间的延迟后,执行OnLongClickListener.onLongClick()*/
checkForLongClick(0, x, y);
}
break;
//手指移动
case MotionEvent.ACTION_MOVE:
//如果手指移动到了控件外面,由于事件传递的不可中断性,view还是可以接受到事件的
if (!pointInView(x, y, mTouchSlop)) {
//移除长按事件
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
}
return true;
}
return false;
}
点评:从上面的代码,可以看到View对于dispatchTouchEvent()的实现,主要用于事件接收与处理工作,主要流程的总结如下:
①先是将event交给OnTouchListener处理,这里注意,View必须是ENABLED_MASK,即能用的;
②如果OnTouchListener对event不感兴趣,即view没有设置OnTouchListener对象,或者OnTouchListener的onClick方法返回了false,那么事件传递给onTouchEvent()方法处理;
③onTouchEvent()方法先判断view是否是可用的,如果不可用,即(viewFlags & ENABLED_MASK) == DISABLED,那么直接返回clickable,即view是否是可点击的,clickable取决于CLICKABLE,LONG_CLICKABLE以及CONTEXT_CLICKABLE属性,只要三个属性有一个满足即可;
④如果view是可点击的,即clickable为true,那么便处理相应的触摸事件;
首先是ACTION_DOWN:它主要是设置view的按压状态,以及准备执行长按事件,如果手指在一个view上按压一段时间后并没有抬起,便执行OnLongClickListener.onLongClick(),即长按事件;
ACTION_MOVE:它主要是判断手指是否移动到了控件外面,如果手指移动了控件外面,便移除长按事件,很多人会问,为什么手指移动到了控件外面,控件还能接收到事件呢,前面说过,由于事件传递的不可中断性,一旦一个view确定了要接受该事件,即ACTION_DOWN返回true,那么此后的事件该view还是可以接受到的;
ACTION_UP:主要是移除长按事件,播放view的点击音效,并且执行OnClickListener的onClick方法,但要注意一点,当view处理了OnLongClickListener的onLongClick方法之后,便不再处理点击事件,即不再执行,即OnLongClickListener的onLongClick方法一旦返回true,就不再执行OnClickListener的onClick方法;
再看下dispatchTouchEvent()方法在ViewGroup中的实现;
ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
//获取事件的实际动作
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
/*ACTION_DOWN意味着一条崭新的事件序列的开始,此时ViewGroup会重置所有与触摸事件派发相关的状态,包括清空TouchTarget列表*/
cancelAndClearTouchTargets(ev);
resetTouchState();
}
/*检查ViewGroup是否需要对事件进行截取,mFirstTouchTarget != null表示此时已经有控件在处理事件了*/
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
/*FLAG_DISALLOW_INTERCEPT标签表示父控件不拦截,通过ViewGroup.requestDisallowInterceptTouchEvent()方法给ViewGroup添加FLAG_DISALLOW_INTERCEPT标签,该方法还会回溯到ViewGroup的根控件,也就是说,一旦添加了这个标签,输入事件从根控件树开始派发,一直到此ViewGroup的过程中,都不会被拦截*/
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//ViewGroup默认是没有FLAG_DISALLOW_INTERCEPT标记,即拦截输入事件
if (!disallowIntercept) {
/*onInterceptTouchEvent方法返回了拦截结果,默认返回false,表示不拦截,该方法可由ViewGroup进行重写,根据实际情况返回拦截结果*/
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
/*代码能执行到这一句,说明此时事件动作是ACTION_DOWN或者是ACTION_UP,并且mFirstTouchTarget == null,那也就是表示ACTION_DOWN事件并没有找到派发目标,那就直接拦截了,事件不再派发给子控件*/
intercepted = true;
}
/*canceled表示ViewGroup收到的事件序列是否被取消,比如控件在处理事件的过程中被移除,activity发生转换等*/
final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
/*split表示了此ViewGroup是否启用了事件序列的拆分机制,就是我们之前说的,默认没有启动,可以通过setMotionEventSplittingEnabled方法进行设置*/
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
/*newTouchTarget用来保存ACTION_DOWN或者ACTION_POINTER_DOWN事件所产生的新的派发目标,简单来说,就是有控件处理了ACTION_DOWN或者ACTION_POINTER_DOWN事件后返回了true,于是newTouchTarget便对这些控件以及此时的触摸事件进行封装*/
TouchTarget newTouchTarget = null;
//当确定了事件的派发目标之后,alreadyDispatchedToNewTouchTarget便会被设置为true,这样就可以跳过后续的派发过程
boolean alreadyDispatchedToNewTouchTarget = false;
/*①以下内容是用来确定输入事件的派发目标,也就是查找哪个view在ACTION_DOWN或者ACTION_POINTER_DOWN事件中返回了true;
②因为确定派发目标的过程也包含了派发过程,所以下面代码也包含了输入事件在控件树中的派发流程;
③如果事件没有被取消,也没有被该ViewGroup拦截,那就开始分析下面代码吧*/
if (!canceled && !intercepted) {
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
/*如果事件是ACTION_DOWN或者ACTION_POINTER_DOWN,标志着新的事件序列的开始,此时要进行派发目标的确定*/
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
/*①通过索引号获取触控点的id;
②注意这里的split,表示对事件序列的拆分,1 << ev.getPointerId(actionIndex)是啥意思?将1进行左移若干个位的方式将ID转换为2的ID次方的形式并存储在idBitsToAssign变量中,比如ID是1,将1左移1位,ID是3,将1左移3位,为啥要这样做呢?其实理解成就是为了给触控点的id设置一个值,跟设置成1,2,3没啥区别,但这个值比1,2,3特殊,它是通过二进制转化来的,具有唯一性;
③1的二进制有32位,从这里我们也可以看出android系统最多支持32个触控点;
④当split为false时,则意味着ViewGroup没有启动拆分机制,那么所有触控点的事件都会派发给后续被确定的目标控件*/
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex): TouchTarget.ALL_POINTER_IDS;
removePointersFromTouchTargets(idBitsToAssign);
//接下来就是检查哪一个控件对新的事件序列感兴趣,通俗一点,就是检查哪个控件对ACTION_DOWN的处理后返回true
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
//获取根据触控点的索引获取触摸事件的坐标
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
/*从最后一个被添加进ViewGroup的view开始检查,ViewGroup中有个数组,被添加进ViewGroup中的view会按照被添加的先后次序一一保存在ViewGroup的数组中,最后一个添加进ViewGroup的view会默认在ViewGroup的最上面,所以检查是逆序进行的*/
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
/*先检查事件坐标是否在view的内部,如果事件坐标在被检查的view区域的外边,那就继续查找下一个控件*/
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
/*从mFirstTouchTarget列表中查找控件所对应的TouchTarget;
之前我们说过TouchTarget是对派发目标view以及输入事件的封装,在TouchTarget中,派发目标view与输入事件一一对应;
循环遍历mFirstTouchTarget中的TouchTarget,以TouchTarget中的child为检查标准,检查是否有相同的子控件的存在,如果存在,就将该child的封装类直接赋值给newTouchTarget*/
newTouchTarget = getTouchTarget(child);
/*如果newTouchTarget为null,则表示手指触摸在一个新的,还没有接收事件序列的view上;如果newTouchTarget不为null,表明此控件在此之前已经成为了事件派发目标了,即ACTION_DOWN或者ACTION_POINTER_DOWN返回了true;那么ViewGroup会默认此控件对这一条子序列也感兴趣,此时将触控点ID绑定在其上,并终止派发目标的查找,那后续的派发工作会据此将此事件派发给这一控件;
举个例子,一根手指放在了一个view上,假设view可以根据手指的移动来移动自己,此时第二根手指也放在了该view上,由于这个view在第一根手指down的时候就已经返回了true,并且第二根手指按下的地方也是同一个view,那么直接将自己的触摸点id绑定到之前的TouchTarget上面去,也就是将第二根手指的事件序列交给这个view处理,这样两根手指就可以共同控制这个view了*/
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//调用dispatchTransformedTouchEvent()方法,尝试将事件派发给当前子控件,若返回true,表示有子控件处理了事件,确定了派发目标;注意这里的参数是child,稍后会剖析该方法
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;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
/*当子控件决定接受这一事件时,为其创建了一个TouchTarget并保存在了mFirstTouchTarget链表中,从此来自此触控点的事件都会派发给这个子控件
newTouchTarget = addTouchTarget(child, idBitsToAssign);
如前文所述,事件在子控件中得到了处理,alreadyDispatchedToNewTouchTarget被设置为true,ACTION_DOWN或者ACTION_POINTER_DOWN将不会再次发送此事件到这一子控件*/
alreadyDispatchedToNewTouchTarget = true;
break;
}
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
/*newTouchTarget == null,表示输入事件没有找到派发目标,即ACTION_DOWN或者ACTION_POINTER_DOWN无人处理;
mFirstTouchTarget != null,表示此时有子控件在处理输入事件;
出现这种情况怎么办呢?那就强行将事件交给最后一次接受事件序列的子控件;
具体现象是什么样子呢?
举个例子,手指1按压在view1上,view1随着手指1的移动而移动,view1处理了手指1的ACTION_DOWN事件,将此次事件派发过程中的newTouchTarget加入
到mFirstTouchTarget中;手指2按压在view2上,view2没有处理手指2的ACTION_DOWN事件,此次事件派发过程中的的newTouchTarget为null,
但是mFirstTouchTarget != null,于是android就强制手指2的事件交给view1,所以现在手指1和手指2都可以控制view1的移动*/
if (newTouchTarget == null && mFirstTouchTarget != null) {
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
//将事件序列的触控点ID绑定到TouchTarget中
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
/*强调一下,以上确定输入事件的派发目标的代码逻辑均是在事件ACTION_DOWN或者ACTION_POINTER_DOWN下执行的*/
if (mFirstTouchTarget == null) {
/**mFirstTouchTarget == null,表示没有找到派发目标,那么事件就由ViewGroup自己处理;比如,ViewGroup的子控件均不处理ACTION_DOWN事件,那么事件只能交给ViewGroup处理,注意dispatchTransformedTouchEvent()的第三个参数是null,表示无人处理*/
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//代码能走到这里,说明ViewGroup已经找到了派发目标,恭喜!!!后续事件将由它处理
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
/*遍历mFirstTouchTarget链表,为链表中的每个派发目标派发输入事件,也就是说,有几个派发目标,输入事件就会传递几次,这点要留意一下*/
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
/*如果TouchTarget是新确定的TouchTarget,那么在确定目标控件的过程中已经完成了事件处理,因此不需要派发,alreadyDispatchedToNewTouchTarget只有在ACTION_DOWN或者ACTION_POINTER_DOWN的情况下才会为true,所以这句判断只能在ACTION_DOWN或者ACTION_POINTER_DOWN的情况下为true,ACTION_MOVE/UP是不走这句代码的*/
handled = true;
} else {
/*cancelChild表示因为某种原因需要中断目标控件继续接受事件序列,这往往由于目标控件即将被移出控件树,或者ViewGroup决定拦截此事件序列。此时仍然会事件发送给目标控件,但是其动作会被改成ACTION_ CANCEL*/
final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
/*使用dispatchTransformedTouchEvent派发事件给目标控件,代码能走到这里,输入事件只剩下有两种情形了,一种情形是ACTION_DOWN事件在确定目标的时候已经完成了处理,传递到这里的事件只能是ACTION_MOVE或者是ACTION_UP等非ACTION_DOWN事件;另一种是ACTION_DOWN事件无人处理, ViewGroup强行将ACTION_DOWN事件事件交给了最后一次接受事件序列的子控件*/
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
/*如果决定终止目标控件继续接受事件序列,则将其对应的TouchTarget从链表中删除并回收,下次事件到来时将不会为其进行事件派发;举个例子,ViewGroup中的一个View处理了ACTION_DOWN事件,那么ACTION_MOVE/UP事件这个view都可以接受到,突然ViewGroup决定对ACTION_MOVE事件进行拦截,即intercepted为true,也就是cancelChild为true,此时ViewGroup依然按照代码流程执行,调用dispatchTransformedTouchEvent()将ACTION_MOVE事件派发给view,ACTION_MOVE事件派发结束以后,便将其该view对应的TouchTarget从链表中删除并回收,此后发生在这个view上面的ACTION_MOVE以及ACTION_UP等事件它再接受不到了*/
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
/*当ViewGroup收到一个ACTION_CANCEL事件或ACTION_UP事件时,整个事件序列已经结束,因此删除所有TouchTarget */
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
/*当事件是一个ACTION_POINTER_UP时,将其对应的触控点ID从对应的TouchTarget中移除*/
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
/*removePointersFromTouchTargets()会在TouchTarget的最后一个触控点ID被移除的同时,将这个TouchTarget从mFirstTouchTarget链表中删除并销毁*/
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
点评:上面的代码比较多,备注也比较多,但逻辑十分清晰,只做了以下几件事:
①寻找派发目标;
②如果找到合适的派发目标,遍历每一个TouchTarget并将事件传递给它,否则,事件将派发给ViewGroup自己;
接下来就是分析如何查找派发目标以及事件如何派发给派发目标了,也就是分析dispatchTransformedTouchEvent()方法;
ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
/*如果需要终止子控件对事件的处理,如父控件拦截,子控件被移除出viewgroup等,注意,也就是说,父控件的onInterceptTouchEvent方法如果返回了true,根据前面的分析知道,ViewGroup会将child对应的TouchTarget从链表中删除并回收,所以子控件收到的最后一个事件动作是ACTION_CANCEL*/
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
//将事件动作替换成ACTION_CANCEL
event.setAction(MotionEvent.ACTION_CANCEL);
//直接调用dispatchTouchEvent处理事件
if (child == null) {
//child == null,表示事件需要派发给ViewGroup自己
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
/*oldPointerIdBits表示了原始事件中所有触控点的列表,也就是当前屏幕上所有手指的id列表
final int oldPointerIdBits = event.getPointerIdBits();
/*newPointerIdBits表示了目标希望接受的触控点的列表,desiredPointerIdBits表示目标希望接受的触控点;比如现在屏幕上有两根手指,id分别是1和2,也就是oldPointerIdBits,此时事件是由手指2产生的,因此desiredPointerIdBits为2,所以newPointerIdBits为2;既然desiredPointerIdBits就已经描述了目标希望接受的触控点,为啥还要newPointerIdBits呢?是因为desiredPointerIdBits可能为TouchTarget.ALL_POINTER_IDS,这个值是-1,此时不能准确的表示实际它需要派发的触控点列表*/
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
if (newPointerIdBits == 0) {
Log.i(TAG, "Dispatch transformed touch event without pointers in " + this);
return false;
}
/*tranformedEvent是一个来自原始MotionEvent的新的MotionEvent,它只包含了目标所感兴趣的触控点,派发给目标事件对象是它而不是原始事件*/
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
//如果newPointerIdBits == oldPointerIdBits,则表示目标对原始事件的所有触控点全盘接受,因此transformedEvent仅仅是原始事件的一个复制;
//什么时候会出现newPointerIdBits == oldPointerIdBits呢?比如,一根手指的触摸操作,又比如没有启用触摸事件的拆分机制
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
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 {
//如果newPointerIdBits != oldPointerIdBits,此时需要使用MotionEvent.split()方法从事件队列中分离一个子集出来以构成transformedEvent,
//代码运行到了这里,说明ViewGroup启动了拆分机制
transformedEvent = event.split(newPointerIdBits);
}
//接下来就是对transformedEvent进行坐标系转换,并发送给目标控件
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
//对transformedEvent进行坐标系转换,使它位于派发控件的坐标系当中
//先是计算ViewGroup的滚动量以及目标控件的位置
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
/*然后当目标控件中存在使用setScaleX()等方法设置的矩阵变换时,将对事件坐标进行变换。此次变换完成之后,事件坐标点便位于目标控件的坐标系了*/
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
点评:上述代码有如下主要步骤:
①·生成 transformedEvent,根据目标所感兴趣的触控点列表,transformedEvent有可能是原始事件的一个副本,或者仅包含部分触控点信息的一个子集;
②对transformedEvent进行坐标系变换,使之位于目标控件的坐标系之中;
③通过dispatchTouchEvent()将transformedEvent发送给目标控件。
关于MotionEvent.split()的内部解析:
MotionEvent.split()是用来对事件序列进行拆分的,也就是修改事件的Action;
如何进行拆分呢,分下面3种情况:
①ViewGroup中有两个控件,分别是view1和view2;
手指1按在了view1上并移动,此时viewgroup和view1收到的事件序列为ACTION_DOWN/MOVE;
这时,手指2按在了view2并移动,那么viewgroup收到的事件序列为ACTION_POINTER_DOWN/MOVE,ACTION_POINTER_DOWN在viewgroup中是合理的,但是如果直接将ACTION_POINTER_DOWN传递给view2,在view2看来就不太合理了,因为这是子控件的一条完整的事件序列,而且根据事件序列的性质, 要求其以ACTION_DOWN开始并以ACTION_UP结束,所以在为view2分离手指2的起始与终止事件时,需要将ACTION_POINTER_DOWN/UP 修改为ACTION_DOWN/UP;
②在前面分析ViewGroup的dispatchTouchEvent方法时,我们特别留意到,一旦确定了派发目标(假设有3个派发目标),接下来的输入事件都会派发给这3个目标,不管手指按压位置是否在目标区域之内;我们还是以上面的情形为例,当手指2按在了view2上时,ViewGroup会产生一个ACTION_POINTER_DOWN事件,MotionEvent.split()会将ACTION_POINTER_DOWN修改为ACTION_DOWN传递给view2,我们这里还有一个派发目标view1,根据刚才所说的,事件也是要传递给view1的,那事件在传递给view1之前需要修改动作吗?答案是需要的,因为view1对手指2的ACTION_POINTER_DOWN事件不处理,所以MotionEvent.split()会将ACTION_POINTER_DOWN/UP修改为ACTION_MOVE后传递给view1;
③最后一种情况是,手指2按在了view2后,手指3也按在了view2上,当手指3按下的时候,view2确实需要一个ACTION_POINTER_DOWN事件,假设此时3跟手指的索引号分别是0,1,2,但是MotionEvent.split()会修改ACTION_POINTER_DOWN/UP所对应的触控点索引,修改完了以后,手指3所产生的ACTION_POINTER_DOWN事件中只携带了2根手指的信息,这两根手指就是之前的手指2和手指3,并且之前手指2和手指3的索引从1,2也修改成了0,1,但是他们的id不变,变得只是索引号;
至此,触摸事件的派发也就结束了。