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

問題描述

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

    }
}

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