问题描述
我们写瀑布流是,如果竖向RecyclerView嵌套横向RecyclerView,当滑动横向RecyclerView时,竖向的RecyclerView会抖动。
事件分发总结
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滑动触发部分源码
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进行处理,反之亦然。
附上源码如下:
package com.newsweekly.livepi.mvp.ui.widget; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.ViewConfiguration; public class BetterRecyclerView extends RecyclerView { private int mScrollPointerId; private int mInitialTouchX, mInitialTouchY; private int mTouchSlop; public BetterRecyclerView (@NonNull Context context) { super(context); init(); } public BetterRecyclerView (@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } public BetterRecyclerView (@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } private void init() { ViewConfiguration vc = ViewConfiguration.get(getContext()); this.mTouchSlop = vc.getScaledTouchSlop(); } @Override public void setScrollingTouchSlop(int slopConstant) { ViewConfiguration vc = ViewConfiguration.get(this.getContext()); switch (slopConstant) { case 0: this.mTouchSlop = vc.getScaledTouchSlop(); case 1: this.mTouchSlop = vc.getScaledPagingTouchSlop(); break; default: Log.w("RecyclerView", "setScrollingTouchSlop(): bad argument constant " + slopConstant + "; using default value"); } super.setScrollingTouchSlop(slopConstant); } @Override public boolean onInterceptTouchEvent(MotionEvent e) { boolean canScrollHorizontally = getLayoutManager().canScrollHorizontally(); boolean canScrollVertically = getLayoutManager().canScrollVertically(); int action = e.getActionMasked(); int actionIndex = e.getActionIndex(); switch (action) { //ACTION_DOWN case 0: mScrollPointerId = e.getPointerId(0); this.mInitialTouchX = (int) (e.getX() + 0.5F); this.mInitialTouchY = (int) (e.getY() + 0.5F); return super.onInterceptTouchEvent(e); //ACTION_MOVE case 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 (getScrollState() != 1) { int dx = x - this.mInitialTouchX; int dy = y - this.mInitialTouchY; boolean startScroll = false; 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; } Log.d("MyRecyclerView", "canX:" + canScrollHorizontally + "--canY" + canScrollVertically + "--dx:" + dx + "--dy:" + dy + "--startScorll:" + startScroll + "--mTouchSlop" + mTouchSlop); return startScroll && super.onInterceptTouchEvent(e); } return super.onInterceptTouchEvent(e); //ACTION_POINTER_DOWN case 5: this.mScrollPointerId = e.getPointerId(actionIndex); this.mInitialTouchX = (int) (e.getX(actionIndex) + 0.5F); this.mInitialTouchY = (int) (e.getY(actionIndex) + 0.5F); return super.onInterceptTouchEvent(e); } return super.onInterceptTouchEvent(e); } }