ViewGroup的派发事件代码主要由dispatchTouchEvent(MotionEvent ev)方法实现,如下
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
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();
}
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
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.
intercepted = true;
}
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
&& !isMouseEvent;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// If the event is targeting accessibility 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 =
isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
final float y =
isMouseEvent ? ev.getYCursorPosition() : 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--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(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();
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 (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// 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 {
// 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;
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;
}
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
mInputEventConsistencyVerifier是用来调试使用的,正式版本里面为null
9-10行是处理辅助功能的,这个需要在设置里开启辅助功能才生效,暂不分析这块的逻辑。
13行声明的变量代表事件是否被消费,初始为false。
14行onFilterTouchEventForSecurity方法进行安全检查,为了防范恶意软件误导用户。如果这个检查没通过的话,该控件会丢弃该事件。
/**
* Filter the touch event to apply security policies.
*
* @param event The motion event to be filtered.
* @return True if the event should be dispatched, false if the event should be dropped.
*
* @see #getFilterTouchesWhenObscured
*/
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
//noinspection RedundantIfStatement
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
return false;
}
return true;
}
onFilterTouchEventForSecurity方法主要过滤的是当前Window被遮挡的情况下的触摸事件。
接着回到ViewGroup里面的处理事件,获取到事件action,并且与MotionEvent.ACTION_MASK做&操作,因为ACTION_POINTER_DOWN和ACTION_POINTER_UP的action里面包括pointer index。
事件开始,进行初始化
19到24行,在事件为ACTION_DOWN的情况下,做一些初始化处理。事件正常可能由ACTION_DOWN,ACTION_MOVE,ACTION_POINTER_DOWN(多于一个手指时),ACTION_POINTER_UP(多于一个手指时),ACTION_UP组成。而ACTION_DOWN是事件的开始
,这个时候设置状态和初始化。会调用cancelAndClearTouchTargets()和resetTouchState()
/**
* Cancels and clears all touch targets.
*/
private void cancelAndClearTouchTargets(MotionEvent event) {
if (mFirstTouchTarget != null) {
boolean syntheticEvent = false;
if (event == null) {
final long now = SystemClock.uptimeMillis();
event = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
syntheticEvent = true;
}
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
resetCancelNextUpFlag(target.child);
dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
}
clearTouchTargets();
if (syntheticEvent) {
event.recycle();
}
}
}
cancelAndClearTouchTargets方法通过名字可以知道取消和清理触摸对象的,开始判断成员变量mFirstTouchTarget不等于null,重点说一下mFirstTouchTarget,他是一个链表,连接所有的需要处理事件的当前ViewGroup的子View,这个链表是在事件为ACTION_DOWN和ACTION_POINTER_DOWN的时候来确定的,后续在发生的事件就会由mFirstTouchTarget连接的对象来处理。正常情况下,一个事件结束的时候,mFirstTouchTarget应该为null。cancelAndClearTouchTargets方法在mFirstTouchTarget不等于null的情况下,会合成一个ACTION_CANCEL事件发送到对象。之后调用clearTouchTargets()方法,
/**
* Clears all touch targets.
*/
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
clearTouchTargets()用来清除所有的触摸对象,所有的触摸对象会调用recycle(),并且在最后会将mFirstTouchTarget 置为null。
public void recycle() {
if (child == null) {
throw new IllegalStateException("already recycled once");
}
synchronized (sRecycleLock) {
if (sRecycledCount < MAX_RECYCLED) {
next = sRecycleBin;
sRecycleBin = this;
sRecycledCount += 1;
} else {
next = null;
}
child = null;
}
}
}
触摸对象有个缓存,缓存的最大容量是MAX_RECYCLED,它的值是32,recycle()方法会在缓存容量不到MAX_RECYCLED的时候,将该TouchTarget对象放进缓存容量,在将对象的child成员变量置为null。
cancelAndClearTouchTargets再最后会检查Motion对象如果是合成的,调用Motion对象的recycle()。
dispatchTouchEvent()方法在ACTION_DOWN下面接着调用resetTouchState(),
resetTouchState()代码如下
/**
* Resets all touch state in preparation for a new cycle.
*/
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
该方法是为了新的事件循环,重设所有的触摸状态。也会调用clearTouchTargets(),接着调用resetCancelNextUpFlag方法,
/**
* Resets the cancel next up flag.
* Returns true if the flag was previously set.
*/
private static boolean resetCancelNextUpFlag(@NonNull View view) {
if ((view.mPrivateFlags & PFLAG_CANCEL_NEXT_UP_EVENT) != 0) {
view.mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
return true;
}
return false;
}
该方法检查View的cancel next up flag,如果View设置过,将该标志清除,并且返回true。如果没有该标志返回false。cancel next up flag会在事件的后续处理中,用来判断ACTION_CANCEL事件。
通过在View的代码中搜索PFLAG_CANCEL_NEXT_UP_EVENT标志的设置位置,发现在performButtonActionOnTouchDown()和onStartTemporaryDetach()中会设置该标志。performButtonActionOnTouchDown()是在执行鼠标右键的时候,onStartTemporaryDetach()是在需要临时将View从父控件detach的时候执行。
resetTouchState()接着会清除FLAG_DISALLOW_INTERCEPT(禁止拦截)标志,还会将mNestedScrollAxes设置为SCROLL_AXIS_NONE。
获取拦截状态
接着回到dispatchTouchEvent()方法中,28到42行用来判断是否拦截事件,判断拦截事件是在事件为ACTION_DOWN或者触摸对象链表不为null的情况下。如果当前控件设置了禁止拦截状态,那么得到的拦截状态就会为false。如果没有设置,则会调用onInterceptTouchEvent()来得到拦截状态intercepted。onInterceptTouchEvent()方法是用来设置是否拦截事件的,并且这个方法只在ViewGroup类型的控件中存在。这个方法默认对触摸事件是不拦截的。
接下来的46到48行,通过注释来看,是说当前控件是拦截状态或者已经有一个处理手势的View,就清除辅助功能标记的状态,做正常事件的派发。
接下来判断是不是取消状态,设置canceled变量的值,就是通过上面已经说过的resetCancelNextUpFlag函数来做判断,或者当前的事件Action就是ACTION_CANCEL。
55到57行的代码来判断事件是否分解。如果当前控件的mGroupFlags有FLAG_SPLIT_MOTION_EVENTS标志,并且当前事件不是鼠标事件,就可以分解。事件分解是用来处理多点触控的。这个标志在HONEYCOMB版本及之后默认添加在mGroupFlags中。
接下来定义两个变量newTouchTarget和alreadyDispatchedToNewTouchTarget,newTouchTarget是新找到的触摸对象,alreadyDispatchedToNewTouchTarget是一个boolean,代表当前的事件是否已经分发给了新触摸对象。
寻找触摸事件派发对象
寻找触摸事件派发对象的代码单独提出来如下:
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// If the event is targeting accessibility 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 =
isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
final float y =
isMouseEvent ? ev.getYCursorPosition() : 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--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(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();
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 (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
以上代码主要是为了寻找触摸事件派发对象的。
在不是取消状态并且不是拦截状态的情况下,开始寻找当前事件的触摸对象。注意,这两个状态是个大前提,分析代码的时候,要考虑这点。
9行代码,在事件是isTargetAccessibilityFocus的情况下,先找到accessibility focus的View。
再接着向下12到14行代码,就是在三种情况下去寻找触摸事件。 1、事件类型是ACTION_DOWN 2、split为true并且事件类型为ACTION_POINTER_DOWN 3、事件类型是ACTION_HOVER_MOVE。
第一种对应第一根手指按下的情况,第二种对应第二根手指按下的情况(ViewGroup初始化的时候,在AndroidManifest文件中配置的targetSdkVersion大于等于API 11,默认设置FLAG_SPLIT_MOTION_EVENTS flag,所以目标targetSdkVersion API 11往上的split为true),第三种对应鼠标悬浮移动的情况。只考虑触摸事件,可以先不管第三种情况。
先获取到当前事件pointer index,再通过它获取到id的bit位值。因为split为true,所以idBitsToAssign里对应id的位为1。调用removePointersFromTouchTargets()先清除触摸事件派发对象链中对象接收该id的标志位,如果派发对象的接收id的标志位都是0,将触摸对象从对象链中删除。
事件的pointer index从0开始,如果事件是ACTION_DOWN,则其pointer index为0,并且在多点触控中,如果手指抬起,该值会变化。事件的id也是从0开始,该值不会随着手指抬起发生变化。所以查找特定手指的事件,需要通过事件的id来做依据。
newTouchTarget代表新的触摸对象,当前是null,如果现在当前ViewGroup对象的孩子控件数量不为0,就去寻找。先得到坐标x,y值,可以看到如果是鼠标事件,使用getXCursorPosition()和getYCursorPosition()得到的x,y的坐标值。接着调用buildTouchDispatchChildList()获得子View集合preorderedList,这个集合是按照子View的Z坐标的位置从小到大排序的。看一下该方法的代码
public ArrayList<View> buildTouchDispatchChildList() {
return buildOrderedChildList();
}
…………
ArrayList<View> buildOrderedChildList() {
final int childrenCount = mChildrenCount;
if (childrenCount <= 1 || !hasChildWithZ()) return null;
if (mPreSortedChildren == null) {
mPreSortedChildren = new ArrayList<>(childrenCount);
} else {
// callers should clear, so clear shouldn't be necessary, but for safety...
mPreSortedChildren.clear();
mPreSortedChildren.ensureCapacity(childrenCount);
}
final boolean customOrder = isChildrenDrawingOrderEnabled();
for (int i = 0; i < childrenCount; i++) {
// add next child (in child order) to end of list
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View nextChild = mChildren[childIndex];
final float currentZ = nextChild.getZ();
// insert ahead of any Views with greater Z
int insertIndex = i;
while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
insertIndex--;
}
mPreSortedChildren.add(insertIndex, nextChild);
}
return mPreSortedChildren;
}
该方法首先调用了另外的一个函数buildOrderedChildList(),继续进入方法里面,如果子View的数量小于等于1或者子View的Z坐标值都是0,该方法返回null。意味着不用管Z坐标值,要么按照子View的添加顺序,要么按照自定义顺序来寻找触摸对象。这里要说下ViewGroup自定义顺序,需要调用setChildrenDrawingOrderEnabled()函数,并且要重写getChildDrawingOrder(int childCount, int drawingPosition)函数。前者用来设置mGroupFlags的FLAG_USE_CHILD_DRAWING_ORDER标志,后者用来得到顺序号。buildOrderedChildList()接下来就是按照子View Z坐标值按照从小到大的顺序放到成员变量mPreSortedChildren中并作为结果返回。
返回到前面函数32行,如果刚才buildTouchDispatchChildList()返回的结果preorderedList不为null,那么变量customOrder的值就为false,因为结果集已经按照Z轴坐标大小排过序了,后面直接就按照变量preorderedList集合里面的顺序来查找。
如果刚才buildTouchDispatchChildList()返回的结果preorderedList为null,并且调用过setChildrenDrawingOrderEnabled(),那么isChildrenDrawingOrderEnabled()返回结果就为true,自然变量customOrder的值就为true。下面调用getAndVerifyPreorderedIndex()函数就可以通过重写的getChildDrawingOrder(int childCount, int drawingPosition)得到对应的子View的查找序号。这种情况就对应我们没有设置子View的Z坐标值,然后想要改变子View的查找顺序。
如果返回的结果preorderedList为null,并且没有setChildrenDrawingOrderEnabled(),这种情况就按照子View的添加顺序的倒序来查找触摸对象。
35行开始的循环就是按照我上面描述的额三种情况来寻找触摸对象的。注意,看到顺序是从childrenCount - 1往前找的。怎么判断子View能不能处理该事件呢?通过40行到43行代码可以知道,view是canReceivePointerEvents()并且isTransformedTouchPointInView(x, y, child, null)。
/**
* Returns whether this view can receive pointer events.
*
* @return {@code true} if this view can receive pointer events.
* @hide
*/
protected boolean canReceivePointerEvents() {
return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}
1、view是canReceivePointerEvents(),通过代码可知view必须可见或者计划播放或正播放相关动画不为null。
/**
* Returns true if a child view contains the specified point when transformed
* into its coordinate space.
* Child must not be null.
* @hide
*/
@UnsupportedAppUsage
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempLocationF();
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, child);
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
………………………………
public void transformPointToViewLocal(float[] point, View child) {
point[0] += mScrollX - child.mLeft;
point[1] += mScrollY - child.mTop;
if (!child.hasIdentityMatrix()) {
child.getInverseMatrix().mapPoints(point);
}
}
2、isTransformedTouchPointInView用来判断触摸事件的坐标点是否落在View的范围之内。调用transformPointToViewLocal()方法将坐标值转化为子View里面的坐标值,然后通过pointInView()方法判断触摸点是否在View的范围之内。
如果这两个条件都满足了,那么触摸对象就找到了。找到触摸对象之后,返回到代码45行,调用getTouchTarget(child)来在触摸对象链表中是否能找到。
/**
* Gets the touch target for specified child view.
* Returns null if not found.
*/
private TouchTarget getTouchTarget(@NonNull View child) {
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
if (target.child == child) {
return target;
}
}
return null;
}
触摸派发对象使用TouchTarget类来描述的,通过TouchTarget类的内部成员next来维持一个链表。如果得到的触摸派发对象不为null,说明该对象已经添加到链表中了。这种情况应该对应的是MotionEvent.ACTION_POINTER_DOWN的情况(不是第一个手指按下的时候),并且按到了同一个控件上面。
代码46行到51行,如果发现控件已经在触摸对象链表中,将待处理的控件的id bit位设置到触摸对象里面就跳出循环了,这个事件的处理需要走到下面的代码了。
代码53行调用resetCancelNextUpFlag(child),去除控件的PFLAG_CANCEL_NEXT_UP_EVENT标志。
接着会调用dispatchTransformedTouchEvent()方法,去处理控件的事件。dispatchTransformedTouchEvent()这个方法后面也会用到,等到后面再讲。在这里如果这个方法返回true,说明这个控件消费了当前事件,设置成员变量mLastTouchDownTime的值。57行到67行是为了设定mLastTouchDownIndex,preorderedList是前面说过的子View按照Z轴坐标大小从小到大排列的一个集合,如果preorderedList不为null,看代码注释是为了寻找当前消费事件的子VIew在mChildren中的顺序。因为变量childIndex是在preorderedList集合中的顺序,不过看他的代码可能有点问题,因为变量children本身就是mChildren,感觉这块children应该写成preorderedList。后面接着设置mLastTouchDownX和mLastTouchDownY,然后会调用addTouchTarget()方法,将控件加入到触摸事件对象链表中,后续的相关事件会接着让该控件处理,还会将变量alreadyDispatchedToNewTouchTarget设置为true,并且会跳出循环。设置变量alreadyDispatchedToNewTouchTarget的值表明这个事件已经处理完毕,下面的处理代码就会跳过。如果dispatchTransformedTouchEvent()方法返回的是false,会接着循环下一个控件,接着寻找触摸事件对象。
寻找触摸事件对象的for循环完毕之后,如果preorderedList不为null,会清空它,因为不再需要它了。
寻找触摸对象的最后一段代码,即82行到90行的if。newTouchTarget == null && mFirstTouchTarget != null这种情况,是什么情况?这种情况应该是多点触控,已经发现了触控事件对象的情况下,另一只手指也按下了,但是这个手指的触摸事件没找到对应的子View进行处理,也就是上面说的每个子View的dispatchTransformedTouchEvent()方法返回了false,这个时候,newTouchTarget==null,mFirstTouchTarget != null。看下这个处理,会将newTouchTarget设置成mFirstTouchTarget链表中最后一个。mFirstTouchTarget链表中最后一个对应最先加入mFirstTouchTarget的触摸对象,后加入的会排在链表的前面。设置完newTouchTarget之后,将newTouchTarget的pointerIdBits对应的事件的id bit位置位,表示该触摸对象接收该事件。
派发触摸对象处理事件
这一段代码如下:
// 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 {
// 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;
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;
}
}
前面寻找了触摸对象之后,会派发对应事件。看这代码主要就是通过dispatchTransformedTouchEvent()事件进行处理,前面在寻找触摸事件对象,也是通过该方法执行ACTION_DOWN和ACTION_POINTER_DOWN事件的。
前面寻找触摸对象的代码是在事件类型为ACTION_DOWN和ACTION_POINTER_DOWN的时候执行的,这个派发事件的代码是处理所有的事件类型的。只不过如果前面处理过ACTION_DOWN和ACTION_POINTER_DOWN之后,在这块代码是通过变量alreadyDispatchedToNewTouchTarget 来避免重复处理的。
如果mFirstTouchTarget 对象为null,则说明前面在该ViewGroup的子View中没找到对应的触摸对象。没找到触摸对象,就会将参数child设置为null,这个就是交给ViewGroup本身来处理。
如果mFirstTouchTarget 对象不为null,则需要将触摸事件派发给它,由它进行处理。循环触摸对象链表,让每个触摸对象处理事件。可以看到如果alreadyDispatchedToNewTouchTarget变量为true,并且触摸对象和newTouchTarget是同一个,则直接设置变量handled的值为true,表示处理过了。对于其他的触摸对象则会调用dispatchTransformedTouchEvent()方法进行处理。只要某一个对象的dispatchTransformedTouchEvent()返回true,就会将变量handled的值设为true。
还可以看到控件如果设置了PFLAG_CANCEL_NEXT_UP_EVENT标识或者变量intercepted为true,这个时候会将cancelChild赋值true,这个值会传递到dispatchTransformedTouchEvent()方法的参数cancel,进行处理。intercepted是在前面的段落 获取拦截状态 中描述了该值的获取。dispatchTransformedTouchEvent()方法的参数cancel为true,则会生成ACTION_CANCEL事件进行传递,后续分析。如果cancelChild的值为true,还会将该触摸对象从触摸对象链中去除,代码22到31行就是去除触摸对象,从链表中去除了之后,会调用recycle()方法进行回收。predecessor变量就是为了从链表中去除当前触摸对象的。
dispatchTouchEvent()收尾的清理工作
最后一部分的代码如下:
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
如果变量canceled等于true或者事件是ACTION_UP或者ACTION_HOVER_MOVE,会执行resetTouchState()方法。canceled的赋值是在前面的 获取拦截状态 那部分,如果当前View设置了PFLAG_CANCEL_NEXT_UP_EVENT或者事件是ACTION_CANCEL的时候,该变量就等于true。事件是ACTION_UP则是事件流的最后一个事件,ACTION_HOVER_MOVE是鼠标的相关事件。在这几种情况下,resetTouchState()方法在前面也分析过相关的代码了。从这里可以知道,事件流结束之后,会将mFirstTouchTarget触摸事件链表置为null,即清除所有触摸对象。
在事件类型为ACTION_POINTER_UP,即多点触控中,抬起了一个手指,会得到该手指的id bit位idBitsToRemove,然后调用removePointersFromTouchTargets(),代码如下:
private void removePointersFromTouchTargets(int pointerIdBits) {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if ((target.pointerIdBits & pointerIdBits) != 0) {
target.pointerIdBits &= ~pointerIdBits;
if (target.pointerIdBits == 0) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
该方法是为了清除所有与该抬起手指相关触摸对象的pointerIdBits 中保存的Id位,如果该pointerIdBits == 0,则会将该触摸对象从触摸对象链表中清除掉。
收尾的最后会将变量handled的结果返回,就是事件的处理结果。
上面是将VIewGroup类的dispatchTouchEvent()方法从前到后讲了一下,不过重要的dispatchTransformedTouchEvent()还没分析,现在开始。
重要的dispatchTransformedTouchEvent()
dispatchTransformedTouchEvent()方法的代码很重要,包括后面的其他的事件类型,也是通过该方法处理,如下
/**
* Transforms a motion event into the coordinate space of a particular child view,
* filters out irrelevant pointer ids, and overrides its action if necessary.
* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {
return false;
}
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
final MotionEvent transformedEvent;
if (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 {
transformedEvent = event.split(newPointerIdBits);
}
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
该方法参数event是派发的事件;参数cancel是代表取消状态,如果该参数为true,会将事件类型设置为ACTION_CANCEL;参数child是需要处理该事件的控件;参数desiredPointerIdBits是child想要处理的事件的id的bit位。
该方法主要处理如下:
一、事件如果是ACTION_CANCEL或参数cancel 是true,设置事件类型为MotionEvent.ACTION_CANCEL,进行处理,处理完毕将原来的事件类型还原,然后返回结果。通过代码可以看到如果child==true,会调用super.dispatchTouchEvent(event),即执行父类(这里指View类)的dispatchTouchEvent()方法,否则调用child.dispatchTouchEvent(event)。
二、判断事件是否分解,如果需要分解的话,调用MotionEvent 的split()方法进行分解出来新事件接着进行传递;如果事件不需要分解,并且child == null,会执行super.dispatchTouchEvent(event),即执行父类(这里指View类)的dispatchTouchEvent()方法,并将执行结果返回;如果事件不需要分解,并且child不为null,并且child.hasIdentityMatrix(),需要将event进行简单转化,然后派发到child,执行child.dispatchTouchEvent(event),恢复event,返回执行结果handled;如果事件不需要分解,但是也不符合上面说的那两种情况,需要调用MotionEvent.obtain(event)获取一个新的事件transformedEvent,并且复制event的属性信息,后面再将该新事件进行属性转化再派发。
三、对新生成的事件进行处理,如果child等于null,调用super.dispatchTouchEvent(transformedEvent);如果child不等于null,则先进行转化,然后会调用child.dispatchTouchEvent(transformedEvent)进行处理。在最后返回结果之前,会将新生成的transformedEvent进行回收,最后返回结果。
其中可以看到,什么情况下会产生ACTION_CANCEL事件?在dispatchTransformedTouchEvent的参数cancel的值为true的时候,会将当前事件类型设置成ACTION_CANCEL,进行派发。什么情况下,设置参数cancel为true呢,前面派发触摸对象处理事件里面已经阐释
参数child如果为null,就是没找到子View,这个时候,是需要该ViewGroup本身进行处理(调用父类View的方法dispatchTouchEvent()进行处理,代码为super.dispatchTouchEvent(event));如果参数child不为null,那么调用child.dispatchTouchEvent(event)进行处理。这个child如果也是ViewGroup类型,那就会嵌套执行dispatchTouchEvent()。
dispatchTouchEvent()的复杂也就在这个嵌套执行上面,理这块的逻辑的时候不能着急,需要慢慢理清楚,碰到问题才能找到解决办法。尤其在前面讲的步骤 寻找触摸事件派发对象也涉及到调用dispatchTouchEvent(),一层一层嵌套进去,出来结果再一层一层返回,很容易乱。
参数desiredPointerIdBits是指子View接收处理的事件的id的bit位。oldPointerIdBits 临时变量是事件中所有的id的bit位。两者进行位操作&,得到变量newPointerIdBits。如果newPointerIdBits和oldPointerIdBits 不相等,进行事件分解。这个怎么理解呢?举个例子,多点触控,A控件需要处理id=0的事件,B控件处理id=1的事件,MotionEvent 事件对象的id中包括0和1,即oldPointerIdBits=0x00000003。当事件分发到A控件的时候,desiredPointerIdBits=0x00000001。这个时候需要进行事件分解,将id=0的事件的相关属性放到一个新MotionEvent 对象中。然后将这个新事件交给A控件处理。同样,事件分发到B控件的时候,也需要事件分解,只不过desiredPointerIdBits=0x00000002。事件分解代码详细可以见https://blog.csdn.net/q1165328963/article/details/120032644
可以看到事件交由子View处理之前需要经过转化,还要注意,在事件不需要分解的情况,子View child.hasIdentityMatrix()的情况下,转化完的事件经过子View处理之后,还需要将属性还原调用的是event.offsetLocation(-offsetX, -offsetY)。View的hasIdentityMatrix()是什么意思呢,就是有初始化矩阵。这个怎么理解?View相对于父控件有时会做复杂的变化,包括位移,旋转等,这些复杂的变化会保存在一个矩阵中。以后父控件的属性如果要改成相对子View的属性,那就可以通过这个矩阵进行转化。
dispatchTransformedTouchEvent()方法如果找到子View,嵌套执行dispatchTouchEvent()。如果找到子View类型是View类型,则会执行View类的dispatchTouchEvent(MotionEvent event)方法,包括没有找到触摸对象,将child设置为null的时候,也会执行View类的dispatchTouchEvent(MotionEvent event)方法,进入其中看一下代码:
View类的dispatchTouchEvent(MotionEvent event)方法
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
该方法的逻辑还是很清晰的
1、事件是ACTION_DOWN的时候,调用stopNestedScroll()
2、事件在控件ENABLED状态下,如果设置了mOnTouchListener接口,先执行接口的onTouch()方法,该接口方法返回true,则将变量result设置为true。
3、result如果为false,执行控件的onTouchEvent()方法,该方法返回true,则将变量result设置为true。
4、事件如果是ACTION_UP或者ACTION_CANCEL或者 ACTION_DOWN并且result如果为false的情况下,调用stopNestedScroll()方法。最后会将变量result的值返回。
从这里可以看到,如果控件在ENABLED状态,并且设置了mOnTouchListener接口,会先调用该接口之后,才会执行onTouchEvent()方法。如果mOnTouchListener接口返回true,那么控件的onTouchEvent()方法是不会执行的。onTouchEvent()方法等到下一篇文章再讲。
在这里总结一下ViewGroup的dispatchTouchEvent()主要内容:
首先在非拦截非取消的转态下,在手指按下的时候去寻找子View触摸对象
然后如果找到了触摸对象,后续的事件如ACTION_MOVE及ACTION_UP都将派发到触摸对象;如果没有找到触摸对象则会调用父类View的dispatchTouchEvent()方法执行。
这块说起来很简单,但是实际中确实相当复杂,细节极多,因为涉及到嵌套执行和多点触控。下面是一幅流程简图,里面包含主要流程。