深入了解View的滑动冲突
持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第21天,点击查看活动详情
前置知识
- 了解 View 的分发事件
- 遇到过滑动冲突
- 有 Android 开发基础
前言
在《与滑动冲突的首次邂逅》一文中,笔者举了一个开发过程中出现的一个简单的滑动冲突问题,带大家直观的了解何为滑动冲突,并且使用了内部拦截法(内部解决法)来解决了这个滑动冲突。建议还不了解何为滑动冲突的同学先去阅读一下这篇文章。但是在上文中,我们并未全面的深入了解滑动冲突,以及学习它的各种解决方法。
本篇文章就继续接着上文,带大家深入了解滑动冲突。但是由于各种滑动冲突的场景复杂多样,本篇文章不会再引入新的 demo 讲述不同场景下的滑动冲突的解决,滑动冲突的解决思路基本是一致的,大家可以举一反三的应用到不同场景中。
何为滑动冲突
什么是滑动冲突?
滑动冲突较难使用语言进行直接描述,如果非要说,那就是:View体系中的滑动事件的实际处理和期望处理不一致,导致不同组件对事件的消费发生冲突。这个是笔者对滑动冲突的理解,需要更加直观理解的请查阅上文
但是单单这样理解显然是不够的,下面我们便从滑动冲突事件的三大场景来分析和深入理解。
滑动冲突的三大场景
滑动冲突的三大场景图如下,下面我们逐个对其进行分析。
场景一:外部滑动方向与内部滑动方向不一致
这个场景,就是我们在《与滑动冲突的首次邂逅》一文中提到的滑动冲突问题。其造成冲突的原因是,外部含有左右滑动的组件,内部含有上下滑动的组件,当触点(手指)在中间滑动的时候,系统无法自动判别这个滑动是需要交给外部组件还是交给内部组件来处理。所以往往会造成只有其中某一层能够滑动,当然上文中的
NestedScrollView
其实有做一些滑动冲突的处理的,所以表现出来的冲突并不是很明显。场景二:外部滑动方向与内部滑动方向一致
这一个场景产生的原因往往是两个纵向滑动的组件嵌套了,导致触点上下滑的时候,系统不清楚需要让哪一个组件滑动。这也会造成只有某一层能够滑动或者说滑动得很卡顿。
场景三:多层嵌套
这一种场景就比前两个都要复杂了。它是指多个组件嵌套在一起,可能是最外层和中层都是横向滑动,里层纵向滑动。这种也是会导致只有其中某一层能够滑动或是滑动得很卡顿。但是这种解决方法也不是很难,就是两层两层之间解决滑动冲突就好。
如何解决滑动冲突
滑动冲突解决思路
由上面对滑动冲突的解释,我们不难得出,滑动冲突产生的原因只是因为系统无法正确识别出我们期望的滑动是哪种。也就是说,期望与实际不符合,而造成不符合的根本原因是各层对滑动事件的获取和消费没有规律。那解决滑动冲突的方法,自然就是找出有滑动冲突的布局层,然后给他们加上事件的拦截规律即可。
而给组件附加上的拦截规律通常有以下几种:
对滑动轨迹进行测量,以其角度(dy/dx)或者宽高的差值(dy-dx)来判断该事件应该被哪一层拦截和消费。最经典的场景一大部分应用的就是这种拦截规律。
例如可以设定 dy-dx>0 就判断滑动期望是上下滑,外层布局不做拦截,让里层布局消费;反之就判定期望为左右滑动,外层布局拦截自己消费。
对滑动速度进行判定。这种对速度的检测和判定一般是作为辅助手段,用于优化滑动冲突的体验,而不是作为主要解决滑动冲突的方法。
基于业务要求联动。这种拦截规律就很抽象了,他不是特指某一种执行逻辑,而是基于业务要求出发的,同时也是最复杂的一种。
例如场景二中,要求里层和外层需要有联动的效果,里层滑动到尽头之后,如果继续滑动就要执行外层的滑动。这种情况就需要对里层进行监测,监测到里层滑动到尽头之后就让外层对事件进行拦截和消费。
下图是对滑动轨迹进行测量判断的示意图,dy 是滑动轨迹的纵向距离,dx 是滑动轨迹的横向距离。
滑动冲突解决法
基于上述的滑动思路,那么我们就会有两种滑动冲突的解决方法:外部拦截法和内部拦截法。名如其意,这两种方法分别是在外层 View 中添加拦截规律和在内层 View 中添加拦截规律。
在讲解两种解决法的通用思路之前,我们继续来回顾一下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 中需要做自定义的判断和修饰,其他地方也是样板代码。当然,只是大致规则上需要遵循,你想做更多的解决和判断,都是随你定义的。
内部拦截法要点注意
内部拦截法其实要设置两个点,一个是上述子 View 的 dispatchTouchEvent,还有一个就是父元素要默认拦截除了 ACTION_DOWN 之外的其他事件,这样子元素调用 getParent().requestDisallowInterceptTouchEvent(false)
才有用。
究其原因很简单,如果拦截了 ACTION_DOWN,那就一开始啥都无法传递给子元素了,就无法让他在 ACTION_DOWN 中设置父类不要拦截,也无法在后面再获取到位置数据来判断设置了。
所以内部拦截法需要设置两处,相对麻烦一些。
可以在父 View 中进行如下设置:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
mLastX = x;
mLastY = y;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
return true;
}
return false;
} else {
return true;
}
}
如果你还想看实战的解决,可以点击 与滑动冲突的首次邂逅 查阅。本篇的讲述就到此结束啦!感谢阅读,欢迎点赞!
参考
第七章 View的滑动冲突 - 简书 (jianshu.com)
《Android开发艺术探索》