我们写瀑布流是,如果竖向RecyclerView嵌套横向RecyclerView,当滑动横向RecyclerView时,竖向的RecyclerView会抖动。这是为什么呢?要分析这个问题我们首先需要了解事件分发机制。如果你已经熟知这一部分可以跳过。
简单来说,事件分发就是用户手点击屏幕之后,点击信息的传递过程。
答案是Activity里的PhoneWindow,要了解PhoneWindow就要先看看Activity的构成。
以上就是Activity的构成结构图,要知道PhoneWindow是属于Activity下的一层视图即可。
要了解事件分发我们要首先看一段伪代码。
public boolen dispatchTouchEvent(MotionEvent ev){
boolen result = false;
if(onInterceptTouchEvent(ev)){
result = onTouchEvent(ev);
}else{
result = child.dispatchTouchEvent(ev);
}
}
return true:表示该View内部消化掉了所有事件
return false:表示事件在本层不再继续进行分发,并交由上层控件的onTouchEvent方法进行消费
return super.dispatchTouchEvent(ev):默认事件将分发给本层的事件拦截onInterceptTouchEvent方法进行处理
return true:表示将事件进行拦截,并将拦截到的事件交由本层控件的onTouchEvent进行处理
return false:表示不对事件进行拦截,事件得以成功分发到子View
return super.onInterceptTouchEvent(ev):默认表示不拦截该事件,并将事件传递给下一层View的dispatchTouchEvent
return true:表示onTouchEvent处理完事件后消费了此次事件
return fasle:表示不响应事件,那么该事件将会不断向上层View的onTouchEvent方法传递,直到某个View的onTouchEvent方法返回true
return super.dispatchTouchEvent(ev):表示不响应事件,结果与return false一样
了解了事件分发,我们来分析这个问题,如图所示
在滑动横向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;
}
问题就在于只要滑动的距离绝对值大于阈值即可。结合我们的例子,外面的纵向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;
}
我给它起了一个名字叫BetterGestureRecyclerView