修复RecyclerView嵌套滚动问题
原文:http://blog.chengyunfeng.com/?p=1017&utm_source=tuicool&utm_medium=referral
在 Android 应用中,大部分情况下都会使用一个垂直滚动的 View 来显示内容(比如 ListView、RecyclerView 等)。但是有时候你还希望垂直滚动的View 里面的内容可以水平滚动。如果直接在垂直滚动的 View 里面使用水平滚动的 View,则滚动操作并不是很流畅。
比如下图中的示例:
为什么会出现这个问题呢?
上图中的布局为一个 RecyclerView 使用的是垂直滚动的 LinearLayoutManager 布局管理器,而里面每个 Item 为另外一个 RecyclerView 使用的是水平滚动的 LinearLayoutManager。而在Android系统的事件分发中,即使最上层的 View 只能垂直滚动,当用户水平拖动的时候,最上层的 View 依然会拦截点击事件。下面是 RecyclerView.java 中 onInterceptTouchEvent 的相关代码:
Java
```
@Override
publicbooleanonInterceptTouchEvent(MotionEvente){
...
switch(action){
case MotionEvent.ACTION_DOWN:
...
case MotionEvent.ACTION_MOVE:{
...
if(mScrollState!=SCROLL_STATE_DRAGGING){
booleanstartScroll=false;
if(canScrollHorizontally&&Math.abs(dx)>mTouchSlop){
...
startScroll=true;
}
if(canScrollVertically&&Math.abs(dy)>mTouchSlop){
...
startScroll=true;
}
if(startScroll){
setScrollState(SCROLL_STATE_DRAGGING);
}
}
}break;
...
}
returnmScrollState==SCROLL_STATE_DRAGGING;
}
```
注意上面的 if 判断:
Java
if(canScrollVertically&&Math.abs(dy)>mTouchSlop){...}
RecyclerView 并没有判断用户拖动的角度, 只是用来判断拖动的距离是否大于滚动的最小尺寸。 如果是一个只能垂直滚动的 View,这样实现是没有问题的。如果我们在里面再放一个 水平滚动的 RecyclerView ,则就出现问题了。
可以通过如下的方式来修复该问题:
Java
if(canScrollVertically&&Math.abs(dy)>mTouchSlop&&(canScrollHorizontally||Math.abs(dy)>Math.abs(dx))){...}
下面是一个完整的实现BetterRecyclerView.java:
Java
```
public class BetterRecyclerView extends RecyclerView{
private static final int INVALID_POINTER=-1;
private int mScrollPointerId=INVALID_POINTER;
private int mInitialTouchX,mInitialTouchY;
private int mTouchSlop;
public BetterRecyclerView(Contextcontext){
this(context,null);
}
publicBetterRecyclerView(Contextcontext,@NullableAttributeSetattrs){
this(context,attrs,0);
}
public BetterRecyclerView(Contextcontext,@NullableAttributeSetattrs,intdefStyle){
super(context,attrs,defStyle);
final ViewConfigurationvc=ViewConfiguration.get(getContext());
mTouchSlop=vc.getScaledTouchSlop();
}
@Override
public void setScrollingTouchSlop(intslopConstant){
super.setScrollingTouchSlop(slopConstant);
final ViewConfigurationvc=ViewConfiguration.get(getContext());
switch(slopConstant){
case TOUCH_SLOP_DEFAULT:
mTouchSlop=vc.getScaledTouchSlop();
break;
case TOUCH_SLOP_PAGING:
mTouchSlop=ViewConfigurationCompat.getScaledPagingTouchSlop(vc);
break;
default:
break;
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvente){
final intaction=MotionEventCompat.getActionMasked(e);
final intactionIndex=MotionEventCompat.getActionIndex(e);
switch(action){
case MotionEvent.ACTION_DOWN:
mScrollPointerId=MotionEventCompat.getPointerId(e,0);
mInitialTouchX=(int)(e.getX()+0.5f);
mInitialTouchY=(int)(e.getY()+0.5f);
return super.onInterceptTouchEvent(e);
case MotionEventCompat.ACTION_POINTER_DOWN:
mScrollPointerId=MotionEventCompat.getPointerId(e,actionIndex);
mInitialTouchX=(int)(MotionEventCompat.getX(e,actionIndex)+0.5f);
mInitialTouchY=(int)(MotionEventCompat.getY(e,actionIndex)+0.5f);
return super.onInterceptTouchEvent(e);
case MotionEvent.ACTION_MOVE:{
finalintindex=MotionEventCompat.findPointerIndex(e,mScrollPointerId);
if(index<0){
return false;
}
final int x=(int)(MotionEventCompat.getX(e,index)+0.5f);
final int y=(int)(MotionEventCompat.getY(e,index)+0.5f);
if(getScrollState()!=SCROLL_STATE_DRAGGING){
final int dx=x-mInitialTouchX;
final int dy=y-mInitialTouchY;
final boolean canScrollHorizontally=getLayoutManager().canScrollHorizontally();
final boolean canScrollVertically=getLayoutManager().canScrollVertically();
boolean startScroll=false;
if(canScrollHorizontally&&Math.abs(dx)>mTouchSlop&&(Math.abs(dx)>=Math.abs(dy)||canScrollVertically)){
startScroll=true;
}
if(canScrollVertically&&Math.abs(dy)>mTouchSlop&&(Math.abs(dy)>=Math.abs(dx)||canScrollHorizontally)){
startScroll=true;
}
return startScroll&&super.onInterceptTouchEvent(e);
}
return super.onInterceptTouchEvent(e);
}
default:
return super.onInterceptTouchEvent(e);
}
}
}
```
其他问题
当用户快速滑动(fling)RecyclerView 的时候, RecyclerView 需要一段时间来确定其最终位置。 如果用户在快速滑动一个子的水平 RecyclerView,在子 RecyclerView 还在滑动的过程中,如果用户垂直滑动,则是无法垂直滑动的。原因是子 RecyclerView 依然处理了这个垂直滑动事件。
所以,在快速滑动后的滚动到静止的状态中,子 View 不应该响应滑动事件了,再次看看 RecyclerView 的 onInterceptTouchEvent() 代码:
Java
```
@Override
public boolean onInterceptTouchEvent(MotionEvente){
...
switch(action){
case MotionEvent.ACTION_DOWN:
...
if(mScrollState==SCROLL_STATE_SETTLING){
getParent().requestDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
}
...
}
return mScrollState==SCROLL_STATE_DRAGGING;
}
```
可以看到,当 RecyclerView 的状态为 SCROLL_STATE_SETTLING (快速滑动后到滑动静止之间的状态)时, RecyclerView 告诉父控件不要拦截事件。
同样的,如果只有一个方向固定,这样处理是没问题的。
针对我们这个嵌套的情况,父 RecyclerView 应该只拦截垂直滚动事件,所以可以这么修改父 RecyclerView:
Java
```
public class FeedRootRecyclerViewextendsBetterRecyclerView{
public FeedRootRecyclerView(Contextcontext){
this(context,null);
}
public FeedRootRecyclerView(Contextcontext,@NullableAttributeSetattrs){
this(context,attrs,0);
}
public FeedRootRecyclerView(Contextcontext,@NullableAttributeSetattrs,intdefStyle){
super(context,attrs,defStyle);
}
@Override
public void requestDisallowInterceptTouchEvent(booleandisallowIntercept){
/* do nothing */
}
}
```
下图为最终的结果:
如果感兴趣可以下载示例项目,注意示例项目中使用 kotlin,所以需要配置 kotlin 插件。
原文:http://nerds.headout.com/fix-horizontal-scrolling-in-your-android-app/
Read more:http://blog.chengyunfeng.com/?p=1017#ixzz4KD0L8Ny5