在Android项目开发中,为了实现需求和兼并用户体验,相信很多人都碰到滑动事件冲突的问题。在Android系统中事件分发机制是一个很重要的组成部分,由于这事件分发机制不是本文重点,故不在此多述,如果有想详细了解的可以自己搜下,网上有很多相关资料详细描述了Android事件分发机制。
一、问题场景
由于RecyclerView自身的优点,使得它已经基本取代了GridView、ListView,而且ViewPager2也是基于RecyclerView实现的,所以现在涉及到列表的基本都离不开RecyclerView。
本文就就基于项目中采用RecyclerView + ViewPager + Fragment + RecyclerView这种嵌套方式出现了滑动冲突。
二、三种解决方式
首先讲下当下的几种处理方式:
- 在父RecyclerView中的事件拦截事件中处理;
自定义父recyclerView并重写onInterceptTouchEvent()方法,代码如下:public class ParentRecyclerView extends RecyclerView { public ParentRecyclerView(@NonNull Context context) { this(context,null); } public ParentRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs,0); } public ParentRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } //不拦截,继续分发下去 @Override public boolean onInterceptTouchEvent(MotionEvent e) { //当然这里可能要根据实际场景去处理下,不仅仅是返回false就结束了。 //todo : 实际场景处理代码 //--------------------------------------------------------------------------------- return false; } }
- 在子RecyclerView中的事件拦截事件中处理;
通过requestDisallowInterceptTouchEvent方法干预事件分发过程,该方法就是通知父布局要不要拦截事件
自定义子RecyclerView并重写dispatchTouchEvent,如下:public class ChildRecyclerView extends RecyclerView { public ChildRecyclerView (@NonNull Context context) { this(context,null); } public ChildRecyclerView (@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs,0); } public ChildRecyclerView (@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { //父层ViewGroup不要拦截点击事件,true不要拦截,false拦截 getParent().requestDisallowInterceptTouchEvent(true); return super.dispatchTouchEvent(ev); } }
- 采用优先级最高的OnTouchListener;
从事件分发机制上看,OnTouchListener优先级很高,可以通过这个来告诉父布局,不要拦截我的事件recyclerView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent motionEvent) { switch (motionEvent.getAction()){ case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: //这里有时要根据自己的场景去写自己的逻辑 view.getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: view.getParent().requestDisallowInterceptTouchEvent(false); break; } return true; } });
以上三种方式至于采用哪种要根据自己的实际场景。
三、针对一种方式进行详解
下面就针对第二种方式在自定义子RecyclerView的做事件拦截处理,因为这种方式正好适合项目解决冲突。
目标 :触摸子RecyclerView上下滑动时,子列表滑动,当列表滑动到顶部、底部或触摸点超出子RecyclerView上下边距时继续滑动,则父RecyclerView跟着滑动。
- MotionEvent.ACTION_DOWN
按下时记录按下的x,y值,并重置标记为;float x = ev.getX(); float y = ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mDownX = x; mDownY = y; lastY = y; disallowInterceptState = 0; getParent().requestDisallowInterceptTouchEvent(true); break;
- MotionEvent.ACTION_MOVE
手指滑动时,通过计算在x,y轴方向移动的距离,判断哪个方向先移动超过给的距离来判断移动的方向,若是x轴方向则不拦截(因为ViewPager横线滑动)次数将标记位设置为2(disallowInterceptState = 2
),若y方向则告诉父View不要拦截并将标记位设置为1(disallowInterceptState = 1
);
继续move时,不断检查是否到View的上下边缘和列表是否滑动到顶部或底部,当满足条件时将标记位设置为2,(disallowInterceptState = 2
)告诉父View可以拦截事件了。if (disallowInterceptState == 0) { float absX = Math.abs(x - mDownX); float absY = Math.abs(y - mDownY); if ((absX > 5f || absY > 5f)) { if (absX < absY) { disallowInterceptState = 1; } else { disallowInterceptState = 2; } } } if (getParent() != null && disallowInterceptState != 0) { //y坐标边界检测 boolean bl = y < 0 || y > getMeasuredHeight(); disallowInterceptState = bl ? 2 : disallowInterceptState; //若滑动到顶部 && 继续下滑动,则释放拦截事件 if((isScrollTop() && lastY < y) || (isScrollBottom() && lastY > y)){ disallowInterceptState = 2; } //检查滑动到底部或顶部 getParent().requestDisallowInterceptTouchEvent(disallowInterceptState == 1); } lastY = y;
- MotionEvent.ACTION_UP和MotionEvent.ACTION_CANCEL
这两个事件不需要做其他处理,恢复父view可以拦截事件//父层ViewGroup不要拦截点击事件 getParent().requestDisallowInterceptTouchEvent(false);
完整代码ChildRecyclerView.java
public class ChildRecyclerView extends RecyclerView {
public ChildRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
private float mDownX, mDownY,lastY;
private int disallowInterceptState = 0;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
float x = ev.getX();
float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = x;
mDownY = y;
lastY = y;
disallowInterceptState = 0;
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (disallowInterceptState == 0) {
float absX = Math.abs(x - mDownX);
float absY = Math.abs(y - mDownY);
if ((absX > 5f || absY > 5f)) {
if (absX < absY) {
disallowInterceptState = 1;
} else {
disallowInterceptState = 2;
}
}
}
if (getParent() != null && disallowInterceptState != 0) {
//y坐标边界检测
boolean bl = y < 0 || y > getMeasuredHeight();
disallowInterceptState = bl ? 2 : disallowInterceptState;
//若滑动到顶部 && 继续下滑动,则释放拦截事件
if((isScrollTop() && lastY < y) || (isScrollBottom() && lastY > y)){
disallowInterceptState = 2;
}
//检查滑动到底部或顶部
getParent().requestDisallowInterceptTouchEvent(disallowInterceptState == 1);
}
lastY = y;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
//父层ViewGroup不要拦截点击事件
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
return super.dispatchTouchEvent(ev);
}
/**
* 滑动到底部检查
* @return true滑动到底部,false没有到底
*/
private boolean isScrollBottom(){
return !canScrollVertically(1);
}
/**
* 滑动到顶部检查
* @return true滑动到顶部,false没有到顶
*/
private boolean isScrollTop(){
return !canScrollVertically(-1);
}
}
最后附上效果图
希望能帮助到大家。
每日一句:要想练就绝世武功 就要忍受常人难忍受的痛。