其他
事件分发机制,我们从细节中学习!
https://www.jianshu.com/u/a101c0b39b29
private TouchTarget mFirstTouchTarget;
简单来说,在ViewGroup.dispatchTouchEvent()遇到非拦截事件,且事件类型为ACTION_DOWN或ACTION_POINTER_DOWN,则会触发一个遍历子控件以查找"触摸目标"的流程。
// The touched child view.
public View child;
// The combined bit mask of pointer ids for all pointers captured by the target.
public int pointerIdBits;
// The next target in the target list.
public TouchTarget next;
}
被点击的子控件,即消耗事件的目标控件。
"目标捕获的所有指针的指针ID的组合位掩码",光看注释难以理解,其实这里涉及到安卓所偏爱的位运算。为了区分多点触控时不同的触控点,每一个触控点都会携带一个pointerId。而pointerIdBits即是所有被目标控件消耗的触控点的pointerId的组合。即pointerIdBits包含一个或以上的pointerId数据。这个pointerIdBits的运算相关实现,将会在下面提到idBitsToAssign的时候说明。
0对应0000 0001, 2对应0000 0100, 5对应0010 0000,然后通过或运算合并为0010 0101。
记录下一个TouchTarget对象,由此组成链表。
非多点触控:mFirstTouchTarget链表退化成单个TouchTarget对象。 多点触控,目标相同:同样为单个TouchTarget对象,只是pointerIdBits保存了多个pointerId信息。 多点触控,目标不同:mFirstTouchTarget成为链表。
然后进入到ViewGroup.dispatchTouchEvent()方法中看具体实现,去除无关逻辑的代码, 首先是:
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();
}
在ACTION_DOWN事件触发时,重置ViewGroup状态,且mFirstTouchTarget会被置空。此时,mFirstTouchTarget = null。然后检测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); // 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;
}
只会在ACTION_DOWN事件时直接触发。 其他的事件会根据是否存在消耗ACTION_DOWN事件的目标控件(即是否有mFirstTouchTarget记录)而决定。
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;
该事件是否需要取消,由resetCancelNextUpFlag(this)或者事件本身决定,基本由事件本身决定。
resetCancelNextUpFlag内部实际上是对PFLAG_CANCEL_NEXT_UP_EVENT进行操作。
当控件持有PFLAG_CANCEL_NEXT_UP_EVENT标记时,则清除该标记并返回true,否则返回flase。
是否支持多点触控,此处默认基本为true。
performButtonActionOnTouchDown():该方法在View.onTouchEvent()中调用,输入事件为鼠标右键的情况下触发,一般情况无需理会(一般不接入鼠标)。 onStartTemporaryDetach():该方法在子控件与父控件"临时分离"时调用。
boolean alreadyDispatchedToNewTouchTarget = false;
>
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);
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;
}
}
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
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;
}
>>>
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;
}
ACTION_DOWN被A消耗,ACTION_POINTER_DOWN也被A消耗,此时相当于A是2个触控点的目标元素。当释放任意一个触控点时,对应的事件是ACTION_POINTER_UP而不是ACTION_UP,导致不产生点击事件。(原理为dispatchTransformedTouchEvent逻辑中,传入的id为A对应的pointerIdBits,此时应为0000 0011,然后会进入if(newPointerIdBits == oldPointerIdBits)的逻辑,该部分逻辑不会通过event.split拆解事件,则为ACTION_POINTER_UP)。假如是ACTION_DOWN的情况,mFirstTouchTarget必然为空,则继续以下流程:
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
上文提及isTransformedTouchPointInView()中进行了坐标偏移处理,同样,该方法中也有相同的操作,只是偏移值直接保存到了MotionEvent中,并在调用完View.dispatchTouchEvent还原。
该方法中,对MotionEvent进行了拆解,获取对应触摸点的MotionEvent,拆解参考的是传入的位分配ID。假如传入的View消耗了该事件,dispatchTransformedTouchEvent将会返回true,然后执行以下逻辑后跳出【遍历】。通过addTouchTarget(),生成一个新的TouchTarget(包裹着消化事件的View),并添加到mFirstTouchTarget头部,并使newTouchTarget指向生成的TouchTarget。alreadyDispatchedToNewTouchTarget标记为true。
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;
}
标记位alreadyDispatchedToNewTouchTarget只会在新建TouchTarget时设置true。 ACTION_DOWN无法找到目标时会导致后续所有的派分都直接传到ViewGroup本身。 ACTION_POINTER_DOWN无法找到目标时视为ACTION_DOWN目标接收派分。
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
}
// 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;
}