1. 程式人生 > >Android 仿京東,淘寶RecyclerView巢狀ViewPager巢狀RecyclerView商品展示

Android 仿京東,淘寶RecyclerView巢狀ViewPager巢狀RecyclerView商品展示

最近看到京東,淘寶都有RecyclerView巢狀ViewPager巢狀RecyclerView商品展示的效果,效果挺好,廢話不多說先看效果圖:

GIF.gif

技能點:
1.Android事件分發機制等

需求點:
1.列表巢狀,內層的列表可以左右切換
2.ViewPager可以點選和滑動切換

最近在淘寶京東看到類似的效果,有時間就寫了一下,效果實現了,但是感覺解決問題的思路和程式碼有很多瑕疵,寫出來拋磚引玉,希望大佬們不吝賜教,寫的不好不喜勿噴!

下面進入正題,先看下佈局結構:

screen.png

就是標題所說的佈局結構 RecyclerView+ViewPager+RecyclerView

`

很多同學看到這裡肯定想到要處理滑動衝突,沒錯,我們簡單分析一下好擼程式碼(雖然是擼好的程式碼)

  1. 橫向滑動
  • 橫向滑動很簡單,RecyclerView不需要處理,ViewPager處理
  1. 縱向滑動
  • 縱向滑動就稍微複雜點,本文的解決滑動衝突主要就就是解決外層RecyclerView以及內層RecyclerView的滑動衝突,仔細看下互動效果,不難發現我們需要用Tab是否吸頂作為判斷的節點來將滑動事件交給外層或內層RecyclerView處理. 即: 1.Tab未吸頂時外層RecyclerView處理滑動事件,2.Tab吸頂時內層RecyclerView處理滑動事件. 這裡解釋一下,原來的方案是吸頂,後來我想了一下如果這個ViewPager下面沒有跟多其它的樣式的話,可以不用吸頂的(不能再有了,互動處理也太麻煩,有的話排版應該也不好看),\color{red}{所以下面的吸頂都是假的,只是看起來是吸頂效果}

大概就是這樣,思路很清晰,這裡先提幾個接下來遇到的問題:

  • RecyclerView巢狀ViewPager時ViewPager的高度為0
  • 滑動衝突
  • 操作步驟:滑動到Tab吸頂->滑動內層RecyclerView至中間->切換一個Tab(內層RecyclerView的狀態已經滑動到頂部,就是初始狀態)->這時候將Tab滑動到非吸頂->切換到最初內層RecyClerView滑動到中間的Tab,這時候展示的就是Tab未吸頂,內層RecyclerView不在頂部的尷尬局面.說了這麼多應該需要一張gif解釋一下上圖:

    GIF1.gif

對於上圖所提到的情況,這個時候使用者手指縱向滑動紅色區域,滑動事件交給誰都不合適

.那先說下淘寶和京東採取的方式:

  1. 淘寶和京東部分頁面切換ViewPager時候重新拉取資料(可能沒有重新拉資料,只是notify了一下)將RecyclerView直接展示到初始狀態
  2. 京東的部分介面(京東->我的->下拉->為你推薦)處理方式為:當Tab為非吸頂狀態時候切換ViewPager,外層RecyclerView滑動到Tab吸頂
  3. demo因為用的是假資料,所以沒做處理,但是程式碼中有在tab非吸頂狀態時候,外層RecyclerView優先處理滑動事件的程式碼

個人感覺第一種處理方式比較好一點,demo的程式碼如下(需要請自行修改,PagerFragment.java)

                        if(! ((MainActivity)getActivity()).isStick){
                            ((MainActivity)getActivity()).adjustScroll(true);
                            return false;
                        }

下面說下實現方式,以及問題的解決(佈局等細節就不貼出來了,詳情見demo):

  1. 外部的RecyclerView為自定義的View繼承自RecyclerView重寫onInterceptTouchEvent方法
    處理滑動事件:

   private float downX ;    //按下時 的X座標
    private float downY ;    //按下時 的Y座標
    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        float x= e.getX();
        float y = e.getY();
        switch (e.getAction()){
            case MotionEvent.ACTION_DOWN:
                //將按下時的座標儲存
                downX = x;
                downY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                //獲取到距離差
                float dx= x-downX;
                float dy = y-downY;
                //通過距離差判斷方向
                int orientation = getOrientation(dx, dy);
                switch (orientation) {
                        //左右滑動交給ViewPager處理
                    case 'r':
                        setNeedIntercept(false);
                        break;
                    //左右滑動交給ViewPager處理
                    case 'l':
                        setNeedIntercept(false);
                        break;
                }
                return isNeedIntercept;
        }
        return super.onInterceptTouchEvent(e);
    }

    public void setNeedIntercept(boolean needIntercept) {
        isNeedIntercept = needIntercept;
    }

    private int getOrientation(float dx, float dy) {
        if (Math.abs(dx)>Math.abs(dy)){
            //X軸移動
            return dx>0?'r':'l';//右,左
        }else{
            //Y軸移動
            return dy>0?'b':'t';//下//上
        }
    }

isNeedIntercept為是否攔截滑動事件,自己處理.並提供了一個setNeedIntercept方法供外部呼叫.程式碼可以看出,橫向的滑動直接放行,讓ViewPager處理,向上滑動時候如果tab吸頂了且已經滑動到底部,交給內部的RecyclerView處理,否則自己處理.

我們對內層的RecyclerView進行處理,重寫其onTouchEvent方法

     @Override
    public boolean onTouchEvent(MotionEvent e) {
        float x= e.getX();
        float y = e.getY();
        switch (e.getAction()){
            case MotionEvent.ACTION_DOWN:
                //將按下時的座標儲存
                downX = x;
                downY = y;
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                        //獲取到距離差
                        float dx= x-downX;
                        float dy = y-downY;
               
                        //通過距離差判斷方向
                        int orientation = getOrientation(dx, dy);
                        int[] location={0,0};
                        getLocationOnScreen(location);
                        switch (orientation) {
                            case 'b':
                                //內層RecyclerView下拉到最頂部時候不再處理事件
                                if(!canScrollVertically(-1)){
                                    getParent().requestDisallowInterceptTouchEvent(false);
                                    if(needIntercepectListener!=null){
                                        needIntercepectListener.needIntercepect(false);
                                    }
                                }else{
                                    getParent().requestDisallowInterceptTouchEvent(true);
                                    if(needIntercepectListener!=null){
                                        needIntercepectListener.needIntercepect(true);
                                    }
                                }
                                break;
                            case 't':
                            
                                if(location[1]<=maxY){
                                    getParent().requestDisallowInterceptTouchEvent(true);
                                    if(needIntercepectListener!=null){
                                        needIntercepectListener.needIntercepect(true);
                                      
                                    }
                                }else{
                                    getParent().requestDisallowInterceptTouchEvent(false);
                                    if(needIntercepectListener!=null){
                                        needIntercepectListener.needIntercepect(false);
                                        return true;
                                    }
                                }
                                break;
                            case 'r':
                                getParent().requestDisallowInterceptTouchEvent(false);
                                break;
                            //左右滑動交給ViewPager處理
                            case 'l':
                                getParent().requestDisallowInterceptTouchEvent(false);
                                break;
                        }
                        break;
        }
        return super.onTouchEvent(e);
    }



    private int getOrientation(float dx, float dy) {
        if (Math.abs(dx)>Math.abs(dy)){
            //X軸移動
            return dx>0?'r':'l';//右,左
        }else{
            //Y軸移動
            return dy>0?'b':'t';//下//上
        }
    }

    public void setMaxY(int height) {
        this.maxY=height;
    }

    public interface NeedIntercepectListener{
        void needIntercepect(boolean needIntercepect);
    }
    public void setNeedIntercepectListener(NeedIntercepectListener needIntercepectListener) {
        this.needIntercepectListener = needIntercepectListener;
    }

其中的回撥是為了告訴外層的RecyclerView需不需要攔截事件.

滑動衝突到這裡基本上處理完了,下面說下吸頂的問題,其實只是思路的問題,這裡採取的方式是將TabLayout和ViewPager當做一個外層RecyclerView的最後一個item,並且高度為螢幕高度-狀態列高度,這樣當外層RecyclerView滑動到底部,Tab看上去是吸頂的.

簡單說下:這個demo之前是按真正的吸頂做的,所以文章改動過,哪裡說得不清楚的請直接看demo,主要是處理滑動事件衝突,難度不大,純屬拋磚引玉.

最後暴露一個問題,在外層RecyclerView滑動到底部時,需要將觸控事件交給內層的RecyclerView處理時,按照Demo裡的處理方式,手指擡起之後重新滑動,內層RecyclerView才能拿到事件,原因是Demo判斷外層RecyclerView是否滑動到底部的程式碼寫在onInterceptTouchEvent裡面,這個方法並不會實時呼叫,試過將判斷寫在onTouchEvent裡面,實時判斷再呼叫onInterceptTouchEvent,但是好像因為內層的RecyclerView並沒有消費掉事件,所以這麼做並沒有效果,並沒有實時的將觸控事件交給內層RecyclerView處理,這裡嘗試了很多方式,都不太理想,希望有思路的大佬給指點一下,效果如下:

GIF2.gif



作者:大名鼎鼎劉小廚
連結:https://www.jianshu.com/p/a5100ac471ae
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。