解決了什麼問題?
- 豎向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