問題描述
我們寫瀑布流是,如果豎向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); } }