嵌套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);
	}
}

 

最后总结下滑动冲突的处理思路:

常规想法,

如果要把事件传给子view处理,可以通过public boolean onInterceptTouchEvent(MotionEvent ev)的返回值为false来实现.

子view也可以也可以在public boolean dispatchTouchEvent(MotionEvent ev)中根据MotionEvent的类型请求父View要不要拦截事件,就是通过getParent().requestDisallowInterceptTouchEvent(true | false);方法实现,

如果子view不消费事件,可以通过public boolean dispatchTouchEvent(MotionEvent ev)返回值false来控制,把事件返回给父view.

但是,

如果这个思路没能把解决掉冲突怎么办呢?出现这个情况,依据上面的思路,要先去打印事件分发拦截那几个方法的返回值,是否合乎预期,尤其是getParent().requestDisallowInterceptTouchEvent(true | false)这个方法的调用,因为是去作用父View,如果父view多重继承,或者重写了requestDisallowInterceptTouchEvent(),就可能出现调用这个方法无效的情况,

在出现这中情况时,首先要分析的是什么条件导致了requestDisallowInterceptTouchEvent无效,

然后,尝试重写这个方法,因为requestDisallowInterceptTouchEvent最终作用到的属性是ViewGroupGroup中mGroupFlags,也就是说,重写这个方法要能保证最终可以把请求传递到ViewGroupGroup才能有效.

最后,参考ViewGroup中这个方法的做的事情,可以通过反射的方式去设置mGroupFlags的值,达到预期.

    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);
        }
    }

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值