Android滑动冲突:RecyclerView嵌套RecyclerView

bug场景:

项目中有一个页面有点类似于淘宝首页,中间有比较多的重复item,为了减少布局嵌套,引入了vlayout框架来处理,在开发新需求的时候,因为业务场景的原因必须内嵌了一个ViewPager,而ViewPager中的一个局部又得是RecyclerView,此时发现内部的RecyclerView无法滑动,事件都被外部的RecyclerView拦截了。

bug分析:

注:为了更简要的说明,我们将外部的RecyclerView叫做RvParent,内部的RecyclerView叫做RvSon。

技术要点:

要想解决这个问题,必须有Android的触摸事件原理的基础,对于Android的事件分发网上已经有很多成熟的文章了,我这里就不再赘述了,如不清楚,请自行查阅。

技术难点:
  • 1.当Touch事件处于MOVE时,如何将RvSon的MOVE传递给RvParent,以及如果将RvParent的MOVE传递给RvSon,并且体验良好。
  • 2.RecyclerView的mScrollState处理不当会导致特殊场景下有bug。
拆分问题:

我们将问题拆分为俩点:

  • 1.手指的落点在RvSon区域内:
    a.当手指向上滑动时:
    1>如果RvSon可以向上滚动,则滚动RvSon,当手指滚动RvSon时,如果RvSon已经滚动到最底部,此时手指还向上滑动,那么交给RvParent处理。
    2>如果RvSon不可向上滚动,则滚动RvParent,当用户向上滚动RvParent时,如果用户突然向下滚动,此时判断RvSon是否可以向下滚动,如果可以:交给RvSon处理,如果不可,则还由RvParent处理。
    b.当手指向下滑动时:原理同上。

  • 2.手指的落点子SonRv区域外:滚动事件由ParentRv全权处理。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
HanldeEvent如下:
void setIntercept(boolean intercept);
boolean getIntercept();
void setChildView(View childView);
void setCurPosY(float curPosY);
void actionIntercept(MotionEvent event);

RvParent如下:
public class NestedRecyclerView extends RecyclerView implements HanldeEvent {
/**
* 当手指的落点在RvSon区域内时赋值,当手指离开屏幕时置空,用来判断事件由谁处理。
* */
private View mChildView;
/**
* RvParent用户判断是否拦截
*/
private boolean isIntercept;

private float mPosY;
private float mCurPosY;

private int mTouchStop;

public NestedRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
final ViewConfiguration vc = ViewConfiguration.get(context);
//根据自己需求可调整大小
mTouchStop = Util.px2dp(context, vc.getScaledTouchSlop());
}


@Override
public void setIntercept(boolean isIntercept) {
this.isIntercept = isIntercept;
}

@Override
public boolean getIntercept() {
return isIntercept;
}

@Override
public void setChildView(View childView) {
mChildView = childView;
}

@Override
public void setCurPosY(float curPosY) {
mCurPosY = curPosY;
}

@Override
public void actionIntercept(MotionEvent event) {
onInterceptTouchEvent(event);
}

@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
// 去掉默认行为,使得每个事件都会经过这个Layout
}

@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
if (mChildView == null) {
return super.onInterceptTouchEvent(e);
}
if (isIntercept) {
return super.onInterceptTouchEvent(e);
}
return false;
}

@Override
public boolean onTouchEvent(MotionEvent e) {
if (mChildView == null) {
return super.onTouchEvent(e);
}
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
mCurPosY = e.getRawY();
break;
case MotionEvent.ACTION_MOVE:
mPosY = mCurPosY;
mCurPosY = e.getRawY();
if (mCurPosY - mPosY > mTouchStop) {
//当RvParent向上滑动时,如果手指的落点在RvSon区域内,且RvSon可以向上滑动,重新dispatch一次down事件,使得列表可以继续滚动
if (mChildView != null && mChildView.canScrollVertically(-1)) {
aginDispatch(e);
}
} else if (mCurPosY - mPosY < -mTouchStop) {
//原理同上
if (mChildView != null && mChildView.canScrollVertically(1)) {
aginDispatch(e);
}
}
break;
case MotionEvent.ACTION_UP:
isIntercept = true;
setChildView(null);
break;
default:
break;

}
return super.onTouchEvent(e);
}

private void aginDispatch(MotionEvent e) {
setIntercept(false);
e.setAction(MotionEvent.ACTION_DOWN);
dispatchTouchEvent(e);
}
}

RvSon如下:
public class NestedChildRecyclerView extends RecyclerView {
private HanldeEvent mHanldeEvent;
private float mCurPosY;
private float mPosY;
private int mTouchStop;

public NestedChildRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
final ViewConfiguration vc = ViewConfiguration.get(context);
//根据自己需求可调整大小
mTouchStop = Util.px2dp(context, vc.getScaledTouchSlop());
setLayoutManager(new LinearLayoutManager(getContext()));
//用于监听RecyclerView能否上下滑动
initListener();
}


@Override
public void onScrollStateChanged(int state) {
super.onScrollStateChanged(state);
//当子RvSon停止滚动时,将RvParent的mScrollState也置成SCROLL_STATE_IDLE状态,否则。。。在特殊场景下有bug。
if (state == SCROLL_STATE_IDLE && mHanldeEvent instanceof RecyclerView) {
RecyclerView recyclerView = (RecyclerView) mHanldeEvent;
recyclerView.stopScroll();
}
}

@Override
public boolean onTouchEvent(MotionEvent e) {
if (mHanldeEvent == null) {
return false;
}
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
mCurPosY = e.getRawY();
mHanldeEvent.setChildView(NestedChildRecyclerView.this);
mHanldeEvent.setCurPosY(mCurPosY);
break;
case MotionEvent.ACTION_MOVE:
mPosY = mCurPosY;
mCurPosY = e.getRawY();
//向上滑动
if (mCurPosY - mPosY > mTouchStop) {
if (canScrollVertically(-1)) {
mHanldeEvent.setIntercept(false);
}
}
//向下滑动
else if (mCurPosY - mPosY < -mTouchStop) {
if (canScrollVertically(1)) {
mHanldeEvent.setIntercept(false);
}
}
break;
case MotionEvent.ACTION_UP:
mHanldeEvent.setChildView(null);
break;
case MotionEvent.ACTION_CANCEL:
//如果去掉,滑动事件从RvSon过度到RvParent会出现不平滑。 
if (mHanldeEvent.getIntercept()) {
e.setAction(MotionEvent.ACTION_DOWN);
mHanldeEvent.actionIntercept(e);
}
break;
default:
break;
}
return super.onTouchEvent(e);
}

private void initListener() {
addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (mHanldeEvent == null) {
return;
}
//滑动到底部
if (!canScrollVertically(1)) {
mHanldeEvent.setIntercept(true);
}
//滑动到顶部
else if (!canScrollVertically(-1)) {
mHanldeEvent.setIntercept(true);
}
}
});
}


public void setHanldeEvent(HanldeEvent hanldeEvent) {
mHanldeEvent = hanldeEvent;
}

@Override
public boolean onInterceptHoverEvent(MotionEvent event) {
return true;
}
}

源码点击下载