Action、ActionMask、ActionIndex
getAction()返回值包含pointerIndex和事件类型,其中前8位代表pointerIndex,后8位代表事件类型
public final int getAction() {
return nativeGetAction(mNativePtr);
}
public final int getActionMasked() {
return nativeGetAction(mNativePtr) & ACTION_MASK;
}
public final int getActionIndex() {
return (nativeGetAction(mNativePtr) & ACTION_POINTER_INDEX_MASK) >> ACTION_POINTER_INDEX_SHIFT;
}
引子
在安卓事件分发过程中,mFirstTouchTarget记录着后续事件分发的目标,但是对于如此核心的代码,你真的了解全部细节吗?
- 为什么要把mFirstTouchTarget设计成链表?
- TouchTarget中的pointerIdBits又起到了什么作用?
思考一下
假设ViewGroup(VG)中有两个Button:A、B:
- 按下A,再按下A(多点触控),为什么释放后A的点击事件只会触发一次?
- 按下A,按下VG(空白区域),为什么先释放A,却无法触发A的点击事件,继续释放VG,又会触发A的点击事件
- 按下VG(空白区域),为什么点击A、B无响应
正文
TouchTarget
// First touch target in the linked list of touch targets.
@UnsupportedAppUsage
private TouchTarget mFirstTouchTarget;
从定义来看,mFirstTouchTarget是触摸目标链表的头节点,但是什么是“触摸目标”呢?
触摸目标:描述触摸控件以及该控件捕获的点击事件。在ViewGroup.dispatchTouchEvent()遇到非拦截、非取消事件,且事件类型为ACTION_DOWN或ACTION_POINTER_DOWN,则会触发一个遍历子控件以查找“触摸目标”的流程
private static final class TouchTarget {
private static final int MAX_RECYCLED = 32;
private static final Object sRecycleLock = new Object[0];
private static TouchTarget sRecycleBin;
private static int sRecycledCount;
public static final int ALL_POINTER_IDS = -1; // all ones
// The touched child view.
public View child;
// The combined bit mask of pointer ids for all pointers captured by the target.
public int pointerIdBits;
public TouchTarget next;
private TouchTarget() {
}
public static TouchTarget obtain(@NonNull View child, int pointerIdBits)
public void recycle()
}
其中:
- View child:为被点击的子控件,即消耗事件的目标控件
- int pointerIdBits:目标捕获的所有事件ID的组合位掩码
- TouchTarget next:目标控件列表中的下一个目标
- obtain和recycle用于对象的分配和回收,类似Message对象池
TouchTarget是对消耗事件的View以链表方式保存,且记录各个View对应的触控点列表pointerIdBits,以实现后续的事件派分处理,同时可以推理出:
- 非多点触控:mFirstTouchTarget链表退化成单个TouchTarget对象
- 多点触控,目标相同:同样为单个TouchTarget对象,只是pointerIdBits保存了多个pointerId信息
- 多点触控,目标不同:mFirstTouchTarget成为链表
ViewGroup.dispatchTouchEvent
一、首先重置状态,在ACTION_DOWN事件触发时,重置ViewGroup状态,mFirstTouchTarget被置空,此时mFirstTouchTarget = null
// 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();
}
二、检测ViewGroup是否拦截事件
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);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
- ACTION_DOWN事件直接走拦截判断逻辑
- 其他事件会根据是否存在消耗ACTION_DOWN事件的目标控件(mFirstTouchTarget)而决定
- 当不存在消耗ACTION_DOWN事件的目标控件或者事件已经被拦截时,后续事件的拦截标记intercepted将会越过用户处理表现为true,可以理解为ViewGroup退化成View
- 只要有一个子View不允许父View拦截,父View则无法拦截
这里可以回答开篇的第三个问题,当点击VG空白位置时,由于不存在消耗ACTION_DOWN的子控件,导致mFirstTouchTarget为空。任何后续事件的派分,都会由于拦截标记intercepted=true而被拦截,包括多点触控ACTION_POINTER_DOWN事件
顺便复习一下拦截处理onInterceptTouchEvent()和requestDisallowInterceptTouchEvent():如果子类调用了requestDisallowInterceptTouchEvent(true)时,ViewGroup会越过用户设置的拦截逻辑onInterceptTouchEvent(),表现为优先使子控件处理事件
final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
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;
}
- boolean canceled:该事件是否需要取消,由resetCancelNextUpFlag(this)或者事件本身决定,基本由事件本身决定。resetCancelNextUpFlag内部实际上是对PFLAG_CANCEL_NEXT_UP_EVENT进行操作,当控件持有PFLAG_CANCEL_NEXT_UP_EVENT标记时,则清除该标记并返回true,否则返回false。一般来说,原生控件中通常在RecycleView/ListView中可能会发生这样的情况,即控件执行轻量级临时分离,在触发onStartTemporaryDetach后,又触发了控件的dispatchTouchEvent
- boolean split:是否支持多点触控,此处默认基本为true,FLAG_SPLIT_MOTION_EVENTS标记在Api11后默认支持,也可以通过setMotionEventSplittingEnabled手动管理
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
- TouchTarget newTouchTarget:当事件已经做出派分时,记录派分对应的控件
- boolean alreadyDispatchedToNewTouchTarget:记录事件是否已经做出派分,用于过滤已派分的事件,避免重复派分
假如事件未被标记为取消或者拦截时,将会进行核心的遍历逻辑,该逻辑中将会尝试查找消耗事件的newTouchTarget
if (!canceled && !intercepted) {
>
逻辑中,会对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
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
>>
- int actionIndex:触控点下标,表明这是第几个触控点
- int idBitsToAssign:位分配ID,通过触控点的PointerId计算,又是安卓各种神奇位运算的一个实例
接下来调用removePointersFromTouchTargets来检查并清除是否有之前记录的相同的pointerId
>>
removePointersFromTouchTargets(idBitsToAssign);
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;
}
}
假如mFirstTouchTarget不为空,检查mFirstTouchTarget链表,检索是否存在记录了该触控点的TouchTarget,存在时,则移除该触控点记录;移除后,如TouchTarget不存在其他的触控点记录,则从链表中移除
当控件的子控件数量大于0时执行遍历,以下部分忽略“子控件布局排序机制”的源码
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = i;
final View child = children[childIndex];
if (!child.canReceivePointerEvents()|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
>>>
continue中断逻辑:
- canReceivePointerEvents:判断控件是否可以接受事件,当控件可见性为VISIBLE或者正在执行动画时,返回true
- isTransformedTouchPointInView:判断View是否包含事件的坐标,计算过程中通过transformPointToViewLocal()计算当前的真实坐标(其中包括了滚动量mScroll,及View在ViewGroup中的位置数据)
假如当前遍历的View不可接受事件,或点击坐标不在其中,则跳过当前遍历的View
>>>>
protected boolean canReceivePointerEvents() {
return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}
protected boolean isTransformedTouchPointInView(float x, float y, View child, PointF outLocalPoint) {
final float[] point = getTempPoint();
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;
}
当View可接受事件且点击坐标在该View空间内时,执行下一步:
只要View消费过DOWN/POINTER_DOWN事件,后续落入该View区域的其他POINTER_DOWN事件也会被默认消费,只有第一次消费的时候才会关心dispatchTransformedTouchEvent返回true还是false
mFirstTouchTarget还有个关键作用,记录事件由谁来消费(只关注DOWN/POINTER_DOWN事件,不关注其他事件类型)
>>>
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
如果mFirstTouchTarget已经存在对应View的TouchTarget,则可以直接把idBitsToAssign添加到TouchTarget中,并跳出【遍历】
这里可以回答开篇的第一个问题,ACTION_DOWN被A消耗,ACTION_POINTER_DOWN也被A消耗,此时相当于A是2个触控点的目标元素。当释放任意一个触控点时,对应的事件是ACTION_POINTER_UP而不是ACTION_UP(涉及到dispatchTransformedTouchEvent事件拆解),导致不产生点击事件
假如mFirstTouchTarget为空(ACTION_DOWN事件或者新的ACTION_POINTER_DOWN事件没有落入已经存在的TouchTarget中),则继续以下流程:
>>>
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
此处涉及到一个关键方法dispatchTransformedTouchEvent,主要用于调用View.dispatchTouchEvent()以执行View的事件分发流程,但由于篇幅问题,此处先直接说明作用:
- 当传入参数View child为空时:视为传入View为ViewGroup本身
- 当传入参数boolean cancel为true时:将MotionEvent的Action设置为ACTION_CANCEL分发到传入的View
- 上文提及isTransformedTouchPointInView()中进行了坐标偏移处理,同样,该方法中也有相同的操作,只是偏移值直接保存到了MotionEvent中,并在调用完View.dispatchTouchEvent还原。该方法中,对MotionEvent进行了拆解,获取对应触摸点的MotionEvent,拆解参考的是传入的位分配ID
如果传入的View消耗了该事件,dispatchTransformedTouchEvent将会返回true,然后执行以下逻辑后跳出遍历
- 通过addTouchTarget(),生成一个新的TouchTarget,并添加到mFirstTouchTarget头部,并使newTouchTarget指向生成的TouchTarget
- alreadyDispatchedToNewTouchTarget标记为true
在>逻辑(即处理ACTION_DOWN、ACTION_POINTER_DOWN)中最后一部分的逻辑为:
>
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;
}
假如当前的newTouchTarget等于空,即无法找到消耗ACTION_POINTER_DOWN事件的View,但mFirstTouchTarget不为空,则:使newTouchTarget指向mFirstTouchTarget链表最后的元素(一般即为消耗ACTION_DOWN的控件),并把当次ACTION_POINTER_DOWN事件的PointID记录到该元素
这里则可以回答文章开篇的第二个问题: 此处原理和问题一一样,只是添加的条件发生变化:ACTION_DOWN被A消耗,则mFirstTouchTarget的末尾元素为A,后续没有被消耗的ACTION_POINTER_DOWN事件都会传入A中,此时相当于A是2个触控点的目标元素
这时候就处理完>的逻辑,完成ACTION_DOWN,ACTION_POINTER_DOWN引起的目标查找。总结如下:
- 标记位alreadyDispatchedToNewTouchTarget只会在新建TouchTarget时设置true
- ACTION_DOWN无法找到目标时会导致后续所有的派分都直接传到ViewGroup本身
- ACTION_POINTER_DOWN无法找到目标时视为ACTION_DOWN目标接收派分
Dispatch to TouchTarget
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
}
ACTION_DOWN没有派分目标:此处dispatchTransformedTouchEvent传入的View参数为null,视为ViewGroup,即派分到自身,执行View.dispatchTouchEvent方法
else {
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;
}
}
- alreadyDispatchedToNewTouchTarget && target == newTouchTarget:用到了alreadyDispatchedToNewTouchTarget标记,用于过滤新建TouchTarget时已消耗事件的情况,避免重复派分
- final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted: intercepted标记的作用区域,主要处理ACTION_DOWN类型事件的目标控件的后续事件派分被拦截的情况
- 当拦截时,需要对目标控件传入一个ACTION_CANCEl事件以通知目标控件当次事件派分被拦截需要进行取消操作。并在后续处理中将cancelChild的目标控件从mFirstTouchTarget中移除
- 当不拦截时,派分事件到mFirstTouchTarget链表中的所有目标控件,由于dispatchTransformedTouchEvent存在触控点ID判断和事件分割,所以实际上只有链表部分的目标控件会收到事件派分
以上,完成了后续事件对mFirstTouchTarget的派分
if (canceled || actionMasked == MotionEvent.ACTION_UP) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
剩余的部分,是对ACTION_UP类型事件进行清理:
- ACTION_UP:说明这是最后一个触控点抬起,通过resetTouchState()完全清理派分目标和状态
- ACTION_POINTER_UP:移除触控点对应的TouchTarget内的pointerIdBits记录,当移除后pointerIdBits = 0(即没有其他触控点记录),则把该TouchTarget从mFirstTouchTarget中移除
最后可以回答开篇问题的1和2:
-
mFirstTouchTarget设计成链表的作用,是用于记录多点触控情况下,多目标控件的派分逻辑
-
pointerIdBits的作用,是配合mFirstTouchTarget,使多点触控时,同个目标可以对多个触控点进行合理的处理逻辑
View.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
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;
}
return result;
}
- 1、如果设置了onTouchListener,首先执行onTouch,如果返回false,继续执行onTouchEvent
- 2、在onTouchEvent中,如果View是clickable的,一定返回true
- 执行touchDelegate
- 执行onClick、onLongClick逻辑
ACTION_CANCEL触发时机
/**
* Constant for {@link #getActionMasked}: The current gesture has been aborted.
* You will not receive any more points in it. You should treat this as
* an up event, but not perform any action that you normally would.
*/
public static final int ACTION_CANCEL = 3;
ACTION_CANCEL意味着当前的手势被中止了,你不会再收到任何事件了,你可以把它当做一个ACTION_UP事件,但是不要执行正常情况下的逻辑
有四种情况会触发ACTION_CANCEL:
if (mFirstTouchTarget == null) {
...
} 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) {
...
} else {
// 判断一:此时cancelChild == true
final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
// 判断二:给child发送cancel事件
if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
handled = true;
}
...
}
...
}
}
- 在子View处理事件的过程中,父View对事件拦截
- 子View被设置了
PFLAG_CANCEL_NEXT_UP_EVENT标记时 - ACTION_DOWN初始化操作(cancelAndClearTouchTarget)
- 子View被父View移除,且View存在于mFirstTouchTarget中时
滑出子View区域会发生什么?
滑出view后依然可以收到ACTION_MOVE和ACTION_UP事件。
为什么有人会认为滑出view后会收到ACTION_CANCEL呢?
我想是因为滑出view后,view的onClick()不会触发了,所以有人就以为是触发了ACTION_CANCEL。
那么为什么滑出view后不会触发onClick呢?再来看看View的源码:
在view的onTouchEvent()中:
case MotionEvent.ACTION_MOVE:
// 判断是否超出view的边界
if (!pointInView(x, y, mTouchSlop)) {
removeLongPressCallback();
if ((mPrivateFlags & PRESSED) != 0) {
// 这里改变状态为 not PRESSED
mPrivateFlags &= ~PRESSED;
}
}
break;
case MotionEvent.ACTION_UP:
removeLongPressCallback();
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
// 可以看到当move出view范围后,这里走不进去了
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
...
performClick();
...
}
mIgnoreNextUpEvent = false;
break;
1,在ACTION_MOVE中会判断事件的位置是否超出view的边界,如果超出边界则将mPrivateFlags置为not PRESSED状态。
2,在ACTION_UP中判断只有当mPrivateFlags包含PRESSED状态时才会执行performClick()等。
因此滑出view后不会执行onClick()。
onLongClick
如果同时设置了onClickListener和onLongClickListener,onLongClickListener返回true的时候,不会触发onClickListener的回调,返回false的时候两个回调都会触发
onLongClickListener的返回值最终会复制给mHasPerformedLongPress变量,而且在View的onTouchEvent()方法中判断了这个变量
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
onLongClick回调通过postDelayed(CheckForLongPress)实现,具体逻辑在checkForLongClick方法中
private void checkForLongClick(int delayOffset, float x, float y) {
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE ||
(mViewFlags & TOOLTIP) == TOOLTIP) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.setAnchor(x, y);
mPendingCheckForLongPress.rememberWindowAttachCount();
mPendingCheckForLongPress.rememberPressedState();
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}
其中,在ACTION_DOWN事件的时候肯定是调用了checkForLongClick()的
再次查看onTouchEvent()中ACTION_UP下的代码:
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback(); //移除长按的callback
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}