嵌套View的滑动,及拦截冲突问题.
分析嵌套view滑动时为什么会有冲突,怎样解冲突
这里的一个场景是:父View是一个可以左右滑动的界面(可以自定义ViewPage,模拟出冲突的情况,因为ViewPager已经处理了滑动冲突,所以如果不重写,模拟不出这里的场景),其子View是一个可以上下话的界面,比如是一个listView.
抛开ims侧的事件处理逻辑,直接说应用侧.
应用侧事件分发的起点从Activity开始.
public boolean dispatchTouchEvent(MotionEvent ev) #Activity.java{
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
这里的getWindow返回的是phonewindow. 如果phonewindow,view树没有处理事件,最终事件交给Activity的onTouchEvent(ev)消费.
public boolean superDispatchTouchEvent(MotionEvent event) #PhoneWindow.java{
return mDecor.superDispatchTouchEvent(event);
}
这里的mDecor是DecorView,也即是view树的根,滑动冲突主要是在view树的嵌套处理中.接下来的事件处理就是ViewGroup,View的分发处理.
仅关注重点代码.
首先分析Down事件:
public boolean dispatchTouchEvent(MotionEvent ev) #ViewGroup.java{
//第一次执行down事件,先考虑canceled,intercepted都为false,
//先说canceled通常是这个事件被它///的父view拦截时触发,然后intercepted通常是由ViewGroup中
//重写的onInterceptTouchEvent(ev)返回值决定.
if (!canceled && !intercepted) {
//第一次down事件,newTouchTarget这个接受事件的view是null,同时view个数不为零.
if (newTouchTarget == null && childrenCount != 0) {
//把一个ViewGroup中的所有子View按Z轴排序,父View的index小于子View的index,
//然后开始循环从子View也即是index最大的view开始,判断当前的事件是否在其坐标范围内.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
//这里newTouchTarget依然为null,因为 getTouchTarget方法中mFirstTouchTarget,导致其中的
//循环没有执行.
newTouchTarget = getTouchTarget(child);
//分发事件给具体的view去处理,这个方法中会去调用child的 //child.dispatchTouchEvent(transformedEvent);的方法.
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//这个child的dispatchTouchEvent方法,返回值表示了事件是否被其消费,
//消费的情况有两种,一个是执行了其中的onTouch方法,一个是通过onTouchEvent消费了事件.
//如果事件被消费了,事件分发就结束了.同时会给newTouchTarget = mFirstTouchTarget= addTouchTarget(child, idBitsToAssign);赋值为当前消费了事件的view.
//如果这个child没有消费这个事件,通过循环就会把事件出给child的父View,去消费.
}
}
}
}
}
正常的down事件,上面就处理完了.
下面看 如果down事件被拦截了,会怎么样?
这里的拦截是通过在具体的ViewGroup中重写boolean onInterceptTouchEvent(MotionEvent event)方法,让其返回true来实现.
比如在自定义的ViewPager中,重写这个 onInterceptTouchEvent方法,返回true.现象是整个界面可以左右滑动,但是listview不能上下滑动.
通过源码看下为什么?
public boolean dispatchTouchEvent(MotionEvent ev) #ViewGroup.java{
final boolean intercepted;
//还是分析的down事件,
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// mGroupFlags默认不包含 FLAG_DISALLOW_INTERCEPT,根据重写的 onInterceptTouchEvent,这里的 intercepted将会为true.
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
}
//如果intercepted为true,下面的条件里面的代码不会执行了,根据前面的了解,这里面的代码是去处理ViewGroup的子View的事件分发,所以这种情况,其子View就没有消费事件的机会了.
if (!canceled && !intercepted) {}
//接着是通过下面的调用,根据前面的分析, mFirstTouchTarget 这个情况下会为null, 所以由自定义viewpager的ontouchEvent来消费事件,那么他的子View ,即listview就没机会消费事件了.
// 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);
}
}
类似的道理,如果在自定义的ViewPager中,重写这个 onInterceptTouchEvent方法,返回false.现象是整个界面不可以左右滑动,但是listview可以上下滑动,因为这种情况下viewpager没有消费事件,所以传给了listview消费了.
前面都是分析的down事件,下面看move事件.为什么自定义的ViewPager中,重写这个 onInterceptTouchEvent方法,返回false.他就不能左右滑动了?
还是看 dispatchTouchEvent方法,只是现在的事件是Move.
public boolean dispatchTouchEvent(MotionEvent ev) #ViewGroup.java{
//down事件后, mFirstTouchTarget不为null了,所以这个条件依然成立,通过 onInterceptTouchEvent判读后, intercepted就是false了.
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;
}
}
//这个条件依然是成立的.
if (!canceled && !intercepted) {
//但是这里不会进入,这个跟down有区别的地方.
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {}
//所以,直接走到这里,但是走的是else
if (mFirstTouchTarget == null) {}
else {
//这里就一直会是listview去消息事件,
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
}
}
最后,我们看下该怎么解决这种嵌套view的冲突问题.
先看一个方法,请求不要拦截touch事件.
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) #ViewGroup.java{
if (disallowIntercept) {
//主要做的事情,就是给 mGroupFlags设置标记为.
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
}
仍然以上面的场景,这里自定义的viewpager中, onInterceptTouchEvent返回true,
public boolean onInterceptTouchEvent(MotionEvent event) {
//这里之所以要对down区别处理,是因为viewgroup的 dispatchTouchEvent中有段代码,在down时会resetTouchState.
if (event.getAction() == MotionEvent.ACTION_DOWN) {
super.onInterceptTouchEvent(event);
return false;
}
return true;
}
自定义的listView中,重写 dispatchTouchEvent方法,down的时候,请求parentView不要拦截,move的时候,当左右滑动时,恢复了这个flag.
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
}
}
在down的时候,我们再看ViewGroup中的处理,
public boolean dispatchTouchEvent(MotionEvent ev) #ViewGroup.java{
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//这里因为 mGroupFlags先 | 然后在 & FLAG_DISALLOW_INTERCEPT,结果不等于0,
//所以 disallowIntercept为true了,所以 intercepted就置为false.
//这种情况,实际上自定义的viewpager的中 onInterceptTouchEvent方法就没用了.
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;
}
}
}
重点看下move时,是怎么处理的.
自定义listview的move事件,如果是上下滑动,跟down事件是一致的,会有listview消费.
当左右滑动时,因为设置了getParent().requestDisallowInterceptTouchEvent(false),最终将有viewpager来消费左右滑动事件.
public boolean dispatchTouchEvent(MotionEvent ev) #ViewGroup.java{
//当前是move事件,但是 mFirstTouchTarget不为null,因为 getParent().requestDisallowInterceptTouchEvent(false),所以(mGroupFlags & FLAG_DISALLOW_INTERCEPT) 是0, 那么disallowIntercept就为false,结果就会调用 onInterceptTouchEvent,会把 intercepted置为true;
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;
}
}
//intercepted为true,下面的条件代码不会执行
if (!canceled && !intercepted) {}
//重点代码是else部分,
if (mFirstTouchTarget == null) { }
else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// intercepted为true,会把 cancelChild置为true.
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//因为 cancelChild为true,这个函数的执行中,会执行event.setAction(MotionEvent.ACTION_CANCEL);然后因为target.child不为null,所以实际的结果将是cancel了listview的执行.
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
//cancelChild为true,会把 mFirstTouchTarget = next;因为前面的处理next为null,所以 mFirstTouchTarget也被置为null了.
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
}
记住上面的条件变量,并且记住当前拥有事件的是listview,因为move事件是会多次执行的,那么在接着执行move事件时,同样走 dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) #ViewGroup.java{
//结合前面一次move的变量值,这里 mFirstTouchTarget为null,
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {}
}
跟进入dispatchTransformedTouchEvent,因为child为null,将会执行viewpager的 dispatchTouchEvent方法,这样就把move事件从listview转到了viewpager.
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) #ViewGroup.java{
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
}
}
上一篇: Python Exceptions 模块
下一篇: Java判断经纬度是否在某个区域内