Android 解決豎向RecyclerView嵌套橫向RecyclerView時的滑動衝突

解決了什麼問題?

  1. 豎向RecyclerView嵌套橫向RecyclerView時的滑動衝突怎麼解決?
  2. 豎向RecyclerView嵌套橫向RecyclerView時以45度分開處理?

問題描述

我們寫瀑布流是,如果豎向RecyclerView嵌套橫向RecyclerView,當滑動橫向RecyclerView時,豎向的RecyclerView會抖動。這是爲什麼呢?要分析這個問題我們首先需要了解事件分發機制。如果你已經熟知這一部分可以跳過。

什麼是事件分發?

簡單來說,事件分發就是用戶手點擊屏幕之後,點擊信息的傳遞過程。

誰處理事件分發?

答案是Activity裏的PhoneWindow,要了解PhoneWindow就要先看看Activity的構成。

  • Activity
    • PhoneWindow
      • DecorView
        • TitleView
        • ContentView

以上就是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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章