什么是滑动冲突?
滑动冲突较难使用语言进行直接描述,如果非要说,那就是:View体系中的滑动事件的实际处理和期望处理不一致,导致不同组件对事件的消费发生冲突。但是单单这样理解显然是不够的,下面我们便从滑动冲突事件的三大场景来分析和深入理解。
滑动冲突的三大场景
滑动冲突的三大场景图如下,下面我们逐个对其进行分析。
-
场景一:外部滑动方向与内部滑动方向不一致
其造成冲突的原因是,外部含有左右滑动的组件,内部含有上下滑动的组件,当触点(手指)在中间滑动的时候,系统无法自动判别这个滑动是需要交给外部组件还是交给内部组件来处理。所以往往会造成只有其中某一层能够滑动。
-
场景二:外部滑动方向与内部滑动方向一致
这一个场景产生的原因往往是两个纵向滑动的组件嵌套了,导致触点上下滑的时候,系统不清楚需要让哪一个组件滑动。这也会造成只有某一层能够滑动或者说滑动得很卡顿。
-
场景三:多层嵌套
这一种场景就比前两个都要复杂了。它是指多个组件嵌套在一起,可能是最外层和中层都是横向滑动,里层纵向滑动。这种也是会导致只有其中某一层能够滑动或是滑动得很卡顿。但是这种解决方法也不是很难,就是两层两层之间解决滑动冲突就好。
如何解决滑动冲突
滑动冲突解决思路
由上面对滑动冲突的解释,我们不难得出,滑动冲突产生的原因只是因为系统无法正确识别出我们期望的滑动是哪种。也就是说,期望与实际不符合,而造成不符合的根本原因是各层对滑动事件的获取和消费没有规律。那解决滑动冲突的方法,自然就是找出有滑动冲突的布局层,然后给他们加上事件的拦截规律即可。
而给组件附加上的拦截规律通常有以下几种:
-
对滑动轨迹进行测量,以其角度(dy/dx)或者宽高的差值(dy-dx)来判断该事件应该被哪一层拦截和消费。最经典的场景一大部分应用的就是这种拦截规律。
例如可以设定 dy-dx>0 就判断滑动期望是上下滑,外层布局不做拦截,让里层布局消费;反之就判定期望为左右滑动,外层布局拦截自己消费。
-
对滑动速度进行判定。这种对速度的检测和判定一般是作为辅助手段,用于优化滑动冲突的体验,而不是作为主要解决滑动冲突的方法。
-
基于业务要求联动。这种拦截规律就很抽象了,他不是特指某一种执行逻辑,而是基于业务要求出发的,同时也是最复杂的一种。
例如场景二中,要求里层和外层需要有联动的效果,里层滑动到尽头之后,如果继续滑动就要执行外层的滑动。这种情况就需要对里层进行监测,监测到里层滑动到尽头之后就让外层对事件进行拦截和消费。
下图是对滑动轨迹进行测量判断的示意图,dy 是滑动轨迹的纵向距离,dx 是滑动轨迹的横向距离。
滑动冲突解决法
基于上述的滑动思路,那么我们就会有两种滑动冲突的解决方法:外部拦截法和内部拦截法。名如其意,这两种方法分别是在外层 View 中添加拦截规律和在内层 View 中添加拦截规律。
首先 View
层层分发下来,若是 onInterceptTouchEvent()
为 true
就拦截,为 false
就继续调用子类的 dispatchTouchEvent()
下发。
当某一层级拦截后,就调用 onTouchEvent()
来处理,若是该层无法处理,就会继续向上传递给父层的 onTouchEvent()
来处理。如此层层传递直到有对应可以处理的父层。
//伪代码
public boolean dispatchTouchEvent(MotionEvent ev){
boolean result = false;
if(onInterceptTouchEvent(ev)){
result = onTouchEvent(ev);
}else{
result = child.dispatchTouchEvent(ev);
}
return result;
}
复制代码
外部拦截法
外部拦截法是在 **外层 View ** 中添加拦截规律,主要拦截的方法是在外层布局中重写 onInterceptTouchEvent()
方法。在方法内添加拦截逻辑,主要判断拦截哪些滑动事件到本层,哪些不做拦截,继续下发。
其主要模板如下:
public boolean onInterceptHoverEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;//1
break;
case MotionEvent.ACTION_MOVE:
if (外层View是否需要拦截) { //2
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
mLastXIntercept = x;//4
mLastYIntercept = y;
return intercepted;
}
复制代码
上述的代码中,有以下几个点需要注意:
-
注释1处,必须要把
intercepted
变量标注为false
,因为如果在 ACTION_DOWN 返回true
,那么一按下就对事件进行拦截,后续的 ACTION_MOVE 和ACTION_UP事件都会直接交给父容器处理,这就没法将事件再传递给子元素了。 - 注释2处,在这里做逻辑判断,判断是否要把事件拦截在外层View中。
-
注释3处,此处必须返回
false
,因为 ACTION_UP 本身已无意义,我们不可在此对事件进行拦截了 - 注释4处,此处需要对上一个触点的位置进行更新。
上述代码相信大家都能看懂,除了注释2处,其余地方皆为样板代码,所以说滑动冲突的解决其实是有套路的。外部拦截法其实是比较直观的一种方法,也是较为推荐的方法。
内部拦截法
内部拦截法是在**内层 View **中添加拦截规律,主要拦截的方法是在内层布局中重写 dispatchTouchEvent
方法。在方法内添加拦截逻辑,其借助 requestDisallowInterceptTouchEvent()
方法,判断父布局需要拦截哪些滑动事件不去下发到本层;如果需要父布局不做拦截,调用getParent().requestDisallowInterceptTouchEvent(true)
来让父布局继续下发事件到本层。
由上述说明可见,内部拦截法是较为复杂的,所以一般不使用内部拦截。但是当外部拦截无法使用的时候,自然就需要使用到内部拦截。
样板代码如下:
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 (外层View是否需要拦截) { //1
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
上述代码和外部拦截法的思路基本一致,只有 ACTION_MOVE 中需要做自定义的判断和修饰,其他地方也是样板代码。当然,只是大致规则上需要遵循,你想做更多的解决和判断,都是随你定义的。
网友评论