Android view 事件分发流程(四)— 事件拦截
1、事件拦截介绍
事件拦截就是拦截事件分发到子View中去。事件拦截的方式有两种:内部拦截和外部拦截。
2、内部拦截
内部拦截是DOWN事件已经到分到了子View后,接收到MOVE事件,由于某种判断确定要把子View的事件返回到给父容器处理(注意:事件处理一旦回到了父容器手上后,后面的事件处理就没有子View的分了,不需得重新开始DOWN事件后再次分发事件,子View才有可能再次拿到事件的处理)。
1)代码分析
内部拦截时调用getParent().requestDisallowIntercepteTouchEvent(boolean disallowIntercept)分发实现的。disallowIntercept 为 true 时是请求父容器允许事件分发到子View;disallowIntercept 为 false 时请求父容器拦截事件分发到子View的。
看下requestDisallowIntercepteTouchEvent(boolean disallowIntercept)代码,主要改变的是mGroupFlags的值,disallowIntercept == true 时 mGroupFlags = FLAG_DISALLOW_INTERCEPT,disallowIntercept == false 时 mGroupFlags = 0
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
由上面代码得出结果是,mGroupFlags会影响到拦截,然后再ViewGroup.dispatchTouchEvent方法中的拦截模块找到了它,final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; mGroupFlags == FLAG_DISALLOW_INTERCEPT 时 disallowIntercept = true → intercepted = false; mGroupFlags == 0 时 disallowIntercept = false → intercepted = onInterceptTouchEvent(ev);由此看出调用getParent().requestDisallowIntercepteTouchEvent(false)时,还得父View的onInterceptTouchEvent(ev)方法配合
// 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;
}
由上代码可以看出调用调用getParent().requestDisallowIntercepteTouchEvent(false)会影响到intercepted的值,在看看intercepted除了在获取目标子View的时候还有在哪里用到,在事件处理代码模块时使用到了 final boolean cancelChild = resetCancelNextUpFlag(target.child)|| intercepted; dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits),intercepted的值决定cancelChild的值,cancelChild的值会影响dispatchTransformedTouchEvent方法的处理
// 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;
}
}
在这里我们看看cancelChild在dispatchTransformedTouchEvent方法的使用,cancel为true时会强制执行MotionEvent.ACTION_CANCEL事件,执行完MotionEvent.ACTION_CANCEL后,mFirstTouchTarget一定为null,下一次事件只要不是DOWN事件,一定会执行if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}也就是super.dispatchTouchEvent(event); 不会再分发事件给子View了
// 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;
}
2)代码实现
在子View中重写dispatchTouchEvent方法,在DOWN事件时必须调用getParent().requestDisallowInterceptTouchEvent(true);方法,mGroupFlags = FLAG_DISALLOW_INTERCEPT → disallowIntercept = true → intercepted = false; 不拦截 不让调用onInterceptTouchEvent方法,直接赋值false,在MOVE事件特定的条件下,调用getParent().requestDisallowInterceptTouchEvent(false);方法,必须调用onInterceptTouchEvent方法获取拦截状态,也就是说onInterceptTouchEvent方法在MOVE事件下必须返回true。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_DOWN){
getParent().requestDisallowInterceptTouchEvent(true);
point = new Point((int) ev.getX(),(int) ev.getY());
}else if(ev.getAction() == MotionEvent.ACTION_MOVE){
if(point != null){
Point newPoint = new Point((int) ev.getX(),(int) ev.getY());
if(getDirection(point,newPoint)){
getParent().requestDisallowInterceptTouchEvent(false);
System.out.println("把事件给回到上层");
}
}
}
return super.dispatchTouchEvent(ev);
}
父View中重写onInterceptTouchEvent方法,DOWN事件时返回false,允许分发事件到子View,MOVE事件时,必须返回true
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_DOWN){
return false;
}else{
return true;
}
}
3)总结
move事件 → 子View在dispatchTouchEvent方法中调用getParent().requestDisallowIntercepteTouchEvent(false) → mGroupFlags = 0 → disallowIntercept = false → intercepted = onInterceptTouchEvent(ev); 此时onInterceptTouchEvent必须返回true → intercepted = true → cancelChild = true → dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits) → mFirstTouchTarget = null;走完这一流程,下一次除DOWN事件外来时,由于mFirstTouchTarget = null,最后调用的是super.dispatchTouchEvent(event);
3、外部拦截
外部拦截事件是父View自己通过onInterceptTouchEvent方法拦截事件分发到子View的。最终想要的效果是改变cancelChild值,最终强制给子View执行CANCEL事件,mFirstTouchTarget = null。后面的流程和内部拦截是一样的,主要是获取intercepted值是不一样的,这里的intercepted值必须从onInterceptTouchEvent(ev)方法获取,所以事件的拦截判断是从onInterceptTouchEvent(ev)实现。mGroupFlags没有其他的干预时的值是0,所以一直都可以intercepted = onInterceptTouchEvent(ev);
1)代码实现
DOWN事件默认返回false,MOVE事件根据判断返回结果,如果返回true就会给子View执行CANCEL事件,mFirstTouchTarget = null,实现拦截子View,后面的事件都是不方法到子View
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_DOWN){
downPoint = new Point((int) ev.getX(),(int)ev.getY());
direction = false;
}else if(ev.getAction() == MotionEvent.ACTION_MOVE){
if(downPoint != null){
Point newPoint = new Point((int) ev.getX(),(int)ev.getY());
direction = getDirection(downPoint,newPoint);
downPoint = null;
}
}
return direction;
}