与滑动冲突的首次邂逅
持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第20天,点击查看活动详情
前置知识
- 了解 View 的分发事件(没了解也没事)
- 有 Android 开发基础
PS:本文写给未曾遇见过滑动冲突的 Android 初学者,当然也欢迎大佬们对我指点一二☺️
前言
如果你是一名Android 新手,那么你很可能没有遇见过滑动冲突,甚至不知道滑动冲突是什么?那是因为你的业务需求可能还不够复杂,作为一名初学者,没有将多种组件结合使用,那自然就没有遇见到滑动冲突了。
但是,滑动冲突等到遇到的时候再去翻阅资料、学习处理,那么就会显得过于仓惶。所以本篇文章,我将用一个简单的案例,带你来一场与滑动冲突的完美邂逅。
PS:本篇不会尚详细展开讲解滑动冲突的各种场景和全面的解决思路,在后续的文章才会进行详细讲解。
邂逅时刻
前面我们说到,滑动冲突的出现,是基于较为复杂的组合应用场景。下面,我提出一个稍微复杂一点的需求,而这个需求直接实现之后,是会出现滑动冲突的。
需求描述是这样子的:我需要在一个页面中,展示三个榜单(电影榜,电视剧榜,综艺榜),这三个榜单分别是通过点击导航栏或者左右滑动展示出来,并且榜单可以上下滑动来查看更多的榜单数据。
根据上述的需求,我们会想到:导航栏可以使用 TabLayout
,榜单左右滑动使用 Viewpager
载入Fragment
,点击展示的话就吧 TabLayout
和 Viewpager
联动即可。最后的榜单上下滑动查看数据,我们可以在 Fragment
里面载入 NestedScrollView
做纵向的页面滚动。
想好了实现的方法,我们可以将这个功能实现出来啦,实现的效果如下:
但是当我们实现之后,我们会发现,这个功能不顺手。怎么不顺手呢?就是当我有时想下滑的时候,他变成了左右滑动了,而一般突变为左右滑动的触发点是我快速滑动的时候。我们可以看一下下方的动图。
下图中,以同样的角度滑动,慢滑动的时候,事件被子View捕获,实现的是 子View
的上下滑动。
而快速滑动的时候,事件被父布局的View捕获,实现的是外层 ViewPager2
的左右滑动。
是的,这种情况,就是滑动冲突。具体来说是,我们的左右滑动和上下滑动发送冲突了,导致软件无法判别清楚我们是想执行哪一种滑动命令。当然,上面的滑动冲突并不是很重大,不会影响业务,只会影响体验罢了。
作为一名负责人的 coder,与滑动冲突的相遇只是开始时刻,若没有产生知识的沉淀或是使得问题被解决,必然不能算一场美好的相遇啦!
所以,让我们一起来体验一把如何解决这个问题,提高用户使用体验吧。
问题的解决
在这里,笔者先笼统的告知你,滑动冲突的解决大致分为:外部解决法和内部解决法。而其原理,就是基于前文View体系(上)|青训营笔记 - 掘金 (juejin.cn)中讲到的事件分发机制了。这里我引用其一小段代码简要的说明下其分发机制。
View
的事件分发是,首先 View
层层分发下来,若是 onInterceptTouchEvent(ev)
为 true
就拦截,为 false
就继续下发。
当某一层级拦截后,就调用 onTouchEvent(event)
来处理,若是该层无法处理,就传递给父层的 onTouchEvent(event)
来处理。如此层层传递直到有对应可以处理的父层。
//伪代码
public boolean dispatchTouchEvent(MotionEvent ev){
boolean result = false;
if(onInterceptTouchEvent(ev)){
result = onTouchEvent(ev);
}else{
result = child.dispatchTouchEvent(ev);
}
return result;
}
现在我们再次说回这两种解决方法,其中重写 onInterceptTouchEvent()
方法就是外部解决法,重写 dispatchTouchEvent()
就是内部解决法
我们这里选用的是 内部解决法。在下面的代码中,如果你是小白,那无需很深刻的理解我解决的过程,大致了解即可。
我们分析上面的需求得出,产生冲突的是 Viewpager
和 NestedScrollView
;而由于我们使用的是 Viewpager2
,其没有默认解决滑动冲突,且无法继承,所以我们选择使用 NestedScrollView
来解决。所以选用的是 内部解决法。
下面给出重写代码:
public class NestedScrollViewVP extends NestedScrollView {
public NestedScrollViewVP(@NonNull Context context) {
super(context);
}
public NestedScrollViewVP(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public NestedScrollViewVP(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private int startX, startY;
boolean isDisallowIntercept = false;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = (int) ev.getX();
startY = (int) ev.getY();
getParent().requestDisallowInterceptTouchEvent(true);//告诉viewgroup不要去拦截我
break;
case MotionEvent.ACTION_MOVE:
int endX = (int) ev.getX();
int endY = (int) ev.getY();
int disX = endX - startX;
int disY = endY - startY;
//角度正确,则让上层view别拦截我的事件
float r = (float)Math.abs(disY)/Math.abs(disX);
if (r > 0.6f) isDisallowIntercept = true;
getParent().requestDisallowInterceptTouchEvent(isDisallowIntercept);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
isDisallowIntercept = false;
getParent().requestDisallowInterceptTouchEvent(false);
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
}
上面的而代码中,我们继承了 NestedScrollView
之后,主要对 dispatchTouchEvent()
方法进行了重写。修改逻辑大致如下
- 在 ACTION_DOWN 中,记录手指触发的初始位置,然后请求父View不对事件做拦截,默认先把事件传递给
NestedScrollView
。 - 在 ACTION_MOVE 中,计算当前的位置与初始位置形成的角度,如果它的正切值大于 0.6 则认为这是上下滑动的命令,需要父View不对事件做拦截。
- 在 ACTION_UP 和 ACTION_CANCEL 中,则请求父View进行事件拦截,让其回到默认的状态。
上面有一点需要注意:在上述的代码中,我们设置了一个变量 isDisallowIntercept
来记录是否阻止父View拦截事件。这个变量的设置很重要,它达到的效果是记录是否发生纵向滑动;如果有发生纵向滑动就请求父View禁止拦截事件,让事件都交给 NestedScrollView
处理,这样子连续的上下滑动就不会被判断为左右滑动了。
为何不设置这样子一个变量就会被判断为左右滑动呢?请看下图,首次滑动的轨迹1是被判断为纵向滑动的,而轨迹2我们直观上去是纵向滑动,但是实际上系统记录的起点->终点的轨迹是红色轨迹,因为起点是一直不变的,这也就导致了轨迹2被判定为横向滑动。所以我们需要一个变量来记录第一次滑动的方向,以供很好的判断。
这个方法中,我们只需要考虑捕获纵向滑动的事件,让纵向滑动不会被误判为横向滑动就行,而不用考虑横变纵的问题。因为触发横向滑动后,是被父View拦截处理的,一旦父View拦截后,NestedScrollView
中的事件就直接变为 ACTION_CANCEL 类型了,NestedScrollView
中的事件分发方法直接不会被执行了。
所以,经过上述的处理,该页面在每次触点按下之后,只要触点不离开,那么处理事件的布局就不会变化。
最后,我们在布局文件中引用我们的修改的子类即可。
<com.qxy.potatos.module.videorank.myview.NestedScrollViewVP
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
</com.qxy.potatos.module.videorank.myview.NestedScrollViewVP>
下面是解决了冲突之后的效果,以及不会出现纵向滑动变成横向滑动的问题了,体验感得到极大的提升。
如上所示,我们完美解决了它的滑动冲突,提高了用户体验。
从本文中,你也学到了何为滑动冲突,且窥探了大致的解决过程。算是初试滑动冲突,实现了从0到1的进步!
后续我们会继续讲解滑动冲突的原理以及解决方法,敬请期待!