Android:解決ViewPager和(RecyclerView、HorizontalScrollView)內部橫向滾動控件的觸摸滑動衝突

其實ViewPager對於觸摸事件的分發已經做得非常好了,HorizontalScrollView以及使用了橫向LinearLayoutManager的RecyclerView或者某些第三方banner輪播控件,基本沒什麼問題,能滾動到最後的,纔會觸發ViewPager的橫向切換滑動。

但是在某些情況下,比如我這邊使用場景是多個菜單欄,使用了第三方類似ViewPager的LayoutManager:PagerGridLayoutManager(https://xiaozhuanlan.com/topic/5841730926) 這個功能確實厲害。解決了List數據需要分頁滑動顯示問題。使用了這個的話,觸摸就不太靈活了,必須是完全橫向才能觸發RecyclerView,不然觸摸就會被ViewPager消費掉。

現在想要實現的是:觸摸在菜單欄的RecyclerView上時,不會觸發ViewPager的橫向滾動。

 

在這之前,先看看這個問題普遍的解決辦法:

1、把ViewPager設置成不響應滑動觸摸,它就是一個純粹的容器,也很容易實現:不攔截觸摸,不消費觸摸(public boolean onTouchEvent(MotionEvent ev) { return false; }    public boolean onInterceptTouchEvent(MotionEvent ev) { return false; }   ) 很遺憾,這個辦法被產品否決了。

2、使用NestedScrolling,ViewPager實現NestedScrollingParent。但是這個實現起來是有2個難點:NestedScrolling的2個接口Parent和Child是說,Child要滑動的時候,都會把自己的行動告訴Parent,Parent來判斷是否需要消費,消費多少觸摸距離。但是ViewPager是直接就攔截了觸摸(之所以這樣說,看底下),根本到不了底下的View。因此ViewPager的onInterceptTouchEvent需要大改,然後在onNestedPreScroll來判斷是否消費滑動;第二個難點是對於我來說的,我的菜單欄是作爲一個header放在另一個垂直滑動的RecyclerView內的,沒試過套2層實現NestedScrolling,應該會有很多問題。

3、重寫ViewPager的觸摸了

 

本篇算是採用了第三種方法,但是比較簡單。

理解這篇文章的前提,是需要對觸摸事件分發有一點小小的瞭解。自上而下 自下而上    當前View onInterceptTouchEvent返回false的時候,事件繼續向下傳遞,返回true,事件到當前view的onTouchEvent處理,onTouchEvent返回false則往上返回。因此假如一個ViewGroup包裹一個View 然後不設置onClick之類的,觸摸View,事件從activity -> ViewGoup -> View ->  ViewGoup ->activity  這塊不深究的話比較簡單。

 

先寫一下開始的思路:開始猜測的是,底層View的問題,也就是上面的RecyclerView。當然,其他控件都沒問題,單單使用了PagerGridLayoutManager就有問題,肯定是PagerGridLayoutManager和ViewPager有衝突。因此我的目光看到了最下層的RecyclerView(下面用rvMenu代替)上,我的想法是rvMenu沒完全消費完觸摸事件,然後就到了上層的ViewPager上,因此寫了個TouchView用來包裹rvMenu:

public class TouchView extends LinearLayout {
    public TouchView(Context context) {
        super(context);
    }

    public TouchView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public TouchView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return true;
    }
}

不攔截觸摸事件,因此觸摸事件會繼續往下傳遞,onTouchEvent返回true 把觸摸事件都消費掉,這樣ViewPager就接收不到觸摸事件了(說一句,不要去動RecyclerView的觸摸事件,因爲它還是要傳遞給子View的,不能簡單的返回true或者false)。想法很美好,結果並沒有什麼改變。因此可以得出上面說的結論:ViewPager是直接就攔截了觸摸然後進行滑動

 

現在目光就轉到了ViewPager上了:現在的思路是,當我觸摸在rvMenu上時,onInterceptTouchEvent返回false,觸摸其他位置時,默認不作處理,那麼整體框架就出來了:

public class MyViewPager extends ViewPager {

    public MyViewPager(Context context) {
        super(context);
    }

    public MyViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
       
        if (是rvMenu的話) { 
            return false;
        }
        return super.onInterceptTouchEvent(ev);
    }
}

其實文章到這裏就算結束了,判斷返回false的時機,看具體情況。假如你是需要某個特定的ViewPager下的包裹的東西不響應的話,比如說在CFragment的時候橫向滑動不響應,就可以這樣寫:

public class MyViewPager extends ViewPager {

    public MyViewPager(Context context) {
        super(context);
    }

    public MyViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Fragment fragment = ((FragmentPagerAdapter) getAdapter()).getItem(getCurrentItem());
        if (fragment instanceof CFragment) { 
            return false;
        }
        return super.onInterceptTouchEvent(ev);
    }
}

我的情況是子View的子View,因此採用的是,MotionEvent的getRawX()和getRawY()來對比rvMenu的位置,如果是處於rvMenu的位置的話,返回false:

public class MyViewPager extends ViewPager {

    public MyViewPager(Context context) {
        super(context);
    }

    public MyViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        View v =  ((ViewAdapter) getAdapter()).getItem(getCurrentItem());
        if (v instanceof MainFeedView) {
            if (((MainFeedView) v).isOnTouch((int)ev.getRawX(), (int)ev.getRawY())) {
                return false;
            } else {
                return super.onInterceptTouchEvent(ev);
            }
        }
        return super.onInterceptTouchEvent(ev);
    }
}


//MainFeedView裏的isOnTouch方法:
public boolean isOnTouch(int x, int y) {
        return isTouchPointInView(rvMenu, x, y);
    }


//(x,y)是否在view的區域內
    public static boolean isTouchPointInView(View view, int x, int y) {
        if (view == null) {
            return false;
        }
        int[] location = new int[2];
        view.getLocationOnScreen(location);
        int left = location[0];
        int top = location[1];
        int right = left + view.getMeasuredWidth();
        int bottom = top + view.getMeasuredHeight();
        //view.isClickable() &&
        if (y >= top && y <= bottom && x >= left
                && x <= right) {
            return true;
        }
        return false;
    }

 

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