事件分发在android中非常重要,写了3篇文章总结其中的故事
android事件分发(三)重要的函数requestDisallowInterceptTouchEvent
上一节做了个事件分发的各种情况总结,今天我们再从源码角度看一下这一系列过程,对其中的一些问题,从代码(源码6.0.0)角度给予答案。
各种情况总结
首先定义down,move,move....,up为一组事件,或者一个cycle(官方说法),从手按下到手放开。
我们从一个viewgroup的角度来分析下一组事件到来,会发生什么事?
我是viewgroup,没有onTouchListener,所以可以简单的认为onTouchEvent的返回值就是dispatchTouchEvent的返回值。
基本的规则是:
0.0我是一个坏父亲,父亲吃到肉了,绝不会再给儿子,儿子吃到肉了,父亲还可能抢。
1.0 down事件首先会传递到我的onInterceptTouchEvent()方法
2.0 onInterceptTouchEvent返回true我就会拦截事件,我的儿子们不可能收到这个事件,返回false就相当于神马都不干,把事件传递给子控件
2.1.0 down事件的onInterceptTouchEvent返回true,之后会调用我的view:dispatchTouchEvent(大部分情况其实就是onTouchEvent),如果我的onTouchEvent也返回true,那么之后的move事件和up事件都不会经过onInterceptTouchEvent,而是直接传递到onTouchEvent。简单的说,我在某刻拦下来了事件,那么后面的事件都会直接拦截,根本不再调用onInterceptTouchEvent。
2.1.1.down事件的onInterceptTouchEvent返回true,之后会调用自己的onTouchEvent,如果onTouchEvent也返回false,那后面的move,up事件都不会执行。为什么?还记得吗?onTouchEvent返回了false,那view:dispatchTouchEvent就是返回了false,而一个cycle内的前一个action的dispatchTouchEvent返回了false,后面的action直接就丢弃了。
2.2.0 如果down的时候onInterceptTouchEvent返回false,然后某个move的onInterceptTouchEvent返回了true,move的onTouchEvent也返回了true,那么之后的move和up等事件都不会触发onInterceptTouchEvent,而是直接传递给onTouchEvent(注意这里所有的onInterceptTouchEvent和onTouchEvent都是指父视图内的)
2.2.1 如果down的时候onInterceptTouchEvent返回false,那就丢给了儿子处理,儿子处理在onTouchEvent里返回了true,表示他处理好了,那下一个action move依然先传给我的onInterceptTouchEvent,我还可以拦截。以前有个错误的理解,我以为action给儿子了并且成功处理了之后,下一个action就直接给儿子了,其实不对,还是会先给父亲,父亲看看是否拦截,再给儿子。父亲比儿子霸道很多,父亲拦下了一个action,后面的action都默认拦下,但是父亲放过了一个action,下一个action来的时候,父亲还是会拦拦看,这其实就是某些滑动冲突的原因。
2.2.2 如果down的时候onInterceptTouchEvent返回false,那就丢给了儿子处理,儿子处理在onTouchEvent里返回了false,那依然会传递到我这里来,会调用我的onTouchEvent
2.2.3如果down的时候onInterceptTouchEvent返回false,那就丢给了儿子处理,儿子处理在onTouchEvent里返回了true。然后下一个事件move来了,我依然不拦截,儿子处理在onTouchEvent里返回了false,那就不会到我的onTouchEvent里面来了,此事件会被丢弃。2.2.2和2.2.3的区别就在于mFirstTouchTarget是否是空。
注意,这里说的onTouchEvent严格意义上说,该是dispatchTouchEvent,只是dispatchTouchEvent一般都是调用onTouchEvent
down事件的传递过程
先来看一个down事件的传递过程,假定viewgroup的onInterceptTouchEvent返回false。
首先传递到ViewGroup的dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
...
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 split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// If the event is targeting accessiiblity 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 = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
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;
}
L5一般为true,进去
L22满足actionMasked ==MotionEvent.ACTION_DOWN,所以进去,disallowIntercept这个参数我们后面再讲,暂时把他看做false。所以就调用了我们熟悉的onInterceptTouchEvent,这里返回false
接着进入for循环,在dispatchTransformedTouchEvent内会掉child的dispatchTouchEvent,如果child处理了这个事件,就给mFirstTouchTarget赋值并且break出这个for循环。mFirstTouchTarget在哪里赋值的呢?newTouchTarget= addTouchTarget(child, idBitsToAssign);这句话
onTouchEvent
for (int i = childrenCount - 1; i >= 0; i--) {
。。。
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;
}
}
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
*/
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
如果没有child处理这个事件呢?会走到L170 mFirstTouchTarget为null,调用handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);,因为传进去的child为null,所以调用的是本身的View:dispatchTouchEvent,大约就是onTouchEvent。
这个地方,实际上有3个分支,
case 1子view处理了down事件
case 2本viewgroup处理了down事件
case 3本viewgroup也没处理down事件,那就会继续往上层viewgroup传递。
我们先看case2,对着前面总结的0.0,为什么下一个事件会直接拦截,看下面这段代码,因为在down的时候,子view并没有处理事件,而是viewgroup处理的,所以mFirstTouchTarget还是null,我们再来看下一个事件来临的时候,会怎么样?看下边的代码,会调用intercepted = true;,直接拦截。这就是“父亲吃到肉了,绝不会再给儿子”。
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;
}
再来看子view处理了事件,mFirstTouchTarget非空,就会走onInterceptTouchEvent,viewgroup依然可能拦截,这就是“儿子吃到肉了,父亲还可能抢”。
再来看case3,我是viewgroup,我不处理,丢给我的父亲,一直往上丢,如果有人处理了,那就类似于case 2,如果一直没人处理,那会怎么样?会一直掉parent的View: dispatchTouchEvent,或者说onTouchEvent,一直到PhoneWindow$DecorView,看看DecorView的onTouchEvent,与众不同,
@Override
public boolean onTouchEvent(MotionEvent event) {
return onInterceptTouchEvent(event);
}
DecorView的onTouchEvent一般返回false(因为SWEEP_OPEN_MENU为true)。
因为DecorView的mFirstTouchTarget为null,所以下一个move或者up事件来的时候,DecorView默认拦截自己处理,也返回false。
整个拦截过程中,mFirstTouchTarget非常关键,用来表示一个cycle内的第一个处理了事件的子类。