解决了什么问题?
- 竖向RecyclerView嵌套横向RecyclerView时的滑动冲突怎么解决?
- 竖向RecyclerView嵌套横向RecyclerView时以45度分开处理?
问题描述
我们写瀑布流是,如果竖向RecyclerView嵌套横向RecyclerView,当滑动横向RecyclerView时,竖向的RecyclerView会抖动。这是为什么呢?要分析这个问题我们首先需要了解事件分发机制。如果你已经熟知这一部分可以跳过。
什么是事件分发?
简单来说,事件分发就是用户手点击屏幕之后,点击信息的传递过程。
谁处理事件分发?
答案是Activity里的PhoneWindow,要了解PhoneWindow就要先看看Activity的构成。
- Activity
- PhoneWindow
- DecorView
- TitleView
- ContentView
- DecorView
- PhoneWindow
以上就是Activity的构成结构图,要知道PhoneWindow是属于Activity下的一层视图即可。
怎么简单理解事件分发?
要了解事件分发我们要首先看一段伪代码。
public boolen dispatchTouchEvent(MotionEvent ev){
boolen result = false;
if(onInterceptTouchEvent(ev)){
result = onTouchEvent(ev);
}else{
result = child.dispatchTouchEvent(ev);
}
}
- onInterceptTouchEvent和OnTouchEvent都是在dispatchTouchEvent方法里调用的。如果在不重写dispatchTouchEvent的方法前提下,这段代码已经可以解释事件分发了,如果onInterceptTouchEvent(ev)返回true就调用ViewGroup自身的onTouchEvent方法,如果是false就调用子控件的dispatchTouchEvent方法。
- 如果重写了dispatchTouchEvent方法,显然以上代码的方法就不会执行,则事件会交给父View的onTouchEvent执行。
- 如果dispatchTouchEvent和onInterceptTouchEvent都不重写,则会向下传递到最后一个子View的onTouchEvent方法,这时事件就会向上传递给父控件的onTouchEvent方法,直到遇到第一个返回为true的控件后方法结束。
- 这里需要注意的是View中是没有onInterceptTouchEvent()方法的,只有ViewGroup才有。
事件分发总结
dispatchTouchEvent
return true:表示该View内部消化掉了所有事件
return false:表示事件在本层不再继续进行分发,并交由上层控件的onTouchEvent方法进行消费
return super.dispatchTouchEvent(ev):默认事件将分发给本层的事件拦截onInterceptTouchEvent方法进行处理
onInterceptTouchEvent
return true:表示将事件进行拦截,并将拦截到的事件交由本层控件的onTouchEvent进行处理
return false:表示不对事件进行拦截,事件得以成功分发到子View
return super.onInterceptTouchEvent(ev):默认表示不拦截该事件,并将事件传递给下一层View的dispatchTouchEvent
onTouchEvent
return true:表示onTouchEvent处理完事件后消费了此次事件
return fasle:表示不响应事件,那么该事件将会不断向上层View的onTouchEvent方法传递,直到某个View的onTouchEvent方法返回true
return super.dispatchTouchEvent(ev):表示不响应事件,结果与return false一样
问题分析
了解了事件分发,我们来分析这个问题,如图所示
在滑动横向RecyclerView时事件会从竖向的RecyclerView里传过来,当我们滑动的手势触发了竖向RecyclerView的滑动事件的时候,事件就会被拦截,这样横向的RecyclerView就不会滑动,而竖向的的RecyclerView就会上下抖动。了解了这个原因,我们再来看看触发RecyclerView的滑动事件的调节是什么?这就需要看RecyclerView的源码了,进入源码。
RecyclerView滑动触发部分源码
public boolean onInterceptTouchEvent(MotionEvent e) {
if (this.mLayoutFrozen) {
return false;
} else if (this.dispatchOnItemTouchIntercept(e)) {
this.cancelTouch();
return true;
} else if (this.mLayout == null) {
return false;
} else {
boolean canScrollHorizontally = this.mLayout.canScrollHorizontally();
boolean canScrollVertically = this.mLayout.canScrollVertically();
if (this.mVelocityTracker == null) {
this.mVelocityTracker = VelocityTracker.obtain();
}
this.mVelocityTracker.addMovement(e);
int action = e.getActionMasked();
int actionIndex = e.getActionIndex();
switch(action) {
case 0:
...
case 1:
...
//从这里开始
case 2://这里的2 为 ACTION_MOVE = 2
int index = e.findPointerIndex(this.mScrollPointerId);
if (index < 0) {
Log.e("RecyclerView", "Error processing scroll; pointer index for id " + this.mScrollPointerId + " not found. Did any MotionEvents get skipped?");
return false;
}
int x = (int)(e.getX(index) + 0.5F);
int y = (int)(e.getY(index) + 0.5F);
if (this.mScrollState != 1) {
int dx = x - this.mInitialTouchX;
int dy = y - this.mInitialTouchY;
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > this.mTouchSlop) {
this.mLastTouchX = x;
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > this.mTouchSlop) {
this.mLastTouchY = y;
startScroll = true;
}
if (startScroll) {
this.setScrollState(1);
}
}
break;
//到这里结束
case 3:
...
}
return this.mScrollState == 1;
}
}
看上面的RecyclerView源码可知,当
if (canScrollHorizontally && Math.abs(dx) > this.mTouchSlop) {
this.mLastTouchX = x;
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > this.mTouchSlop) {
this.mLastTouchY = y;
startScroll = true;
}
这两个条件成立时,startScroll就会被设置为true,然后调用this.setScrollState(1);
void setScrollState(int state) {
if (state != this.mScrollState) {//mScrollState默认值为0
this.mScrollState = state;
if (state != 2) {
this.stopScrollersInternal();
}
this.dispatchOnScrollStateChanged(state);
}
}
在这里把mScroState的默认值设置为了1,最后onInterceptTouchEvent返回了
return this.mScrollState == 1;
也就是true。了解了滑动触发的源码我们就在这里对RecyclerView进行修改即可。
如何修改
我们再来看看触发RecyclerView滑动方法的条件
if (canScrollHorizontally && Math.abs(dx) > this.mTouchSlop) {
this.mLastTouchX = x;
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > this.mTouchSlop) {
this.mLastTouchY = y;
startScroll = true;
}
条件1:当可以横向滑动时,且横向滑动距离的绝对值大于触发滑动的阈值mTouchSlop触发
条件2:当可以纵向滑动时,且纵向滑动距离的绝对值大于触发滑动的阈值mTouchSlop触发
问题在哪?
问题就在于只要滑动的距离绝对值大于阈值即可。结合我们的例子,外面的纵向RecyclerView接收到的滑动只要纵向滑动的距离分量绝对值大于阈值mTouchSlop就会触发第二个条件返回true,进行拦截。
即使用户横向滑动的距离分量大于纵向也不会交给横向的RecyclerView处理,这样就会发生纵向RecyclerView抖动的问题
如何解决
知道了问题所在,我们只要加上如下这个判断即可
if (canScrollHorizontally && Math.abs(dx) > this.mTouchSlop
&& Math.abs(dx) > Math.abs(dy)) {
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > this.mTouchSlop
&& Math.abs(dy) > Math.abs(dx)) {
startScroll = true;
}
横向滑动时判断横向的分量是否大于纵向的,反之亦然。这样就可以实现45度滑动的分隔,用户与水平夹角小于45度滑动时就会交给横向的RecyclerView进行处理,反之亦然。
源码
我给它起了一个名字叫BetterGestureRecyclerView