Android 仿京東,淘寶RecyclerView嵌套ViewPager嵌套RecyclerView商品展示

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

GIF.gif

技能點:
1.Android事件分發(fā)機制等

需求點:
1.列表嵌套,內(nèi)層的列表可以左右切換
2.ViewPager可以點擊和滑動切換

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

下面進入正題,先看下布局結(jié)構(gòu):
screen.png

就是標(biāo)題所說的布局結(jié)構(gòu) RecyclerView+ViewPager+RecyclerView`

很多同學(xué)看到這里肯定想到要處理滑動沖突,沒錯,我們簡單分析一下好擼代碼(雖然是擼好的代碼)
  1. 橫向滑動
  • 橫向滑動很簡單,RecyclerView不需要處理,ViewPager處理

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

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

  • RecyclerView嵌套ViewPager時ViewPager的高度為0
  • 滑動沖突
  • 操作步驟:滑動到Tab吸頂->滑動內(nèi)層RecyclerView至中間->切換一個Tab(內(nèi)層RecyclerView的狀態(tài)已經(jīng)滑動到頂部,就是初始狀態(tài))->這時候?qū)ab滑動到非吸頂->切換到最初內(nèi)層RecyClerView滑動到中間的Tab,這時候展示的就是Tab未吸頂,內(nèi)層RecyclerView不在頂部的尷尬局面.說了這么多應(yīng)該需要一張gif解釋一下上圖:
    GIF1.gif
對于上圖所提到的情況,這個時候用戶手指縱向滑動紅色區(qū)域,滑動事件交給誰都不合適

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

  1. 淘寶和京東部分頁面切換ViewPager時候重新拉取數(shù)據(jù)(可能沒有重新拉數(shù)據(jù),只是notify了一下)將RecyclerView直接展示到初始狀態(tài)
  2. 京東的部分界面(京東->我的->下拉->為你推薦)處理方式為:當(dāng)Tab為非吸頂狀態(tài)時候切換ViewPager,外層RecyclerView滑動到Tab吸頂
  3. demo因為用的是假數(shù)據(jù),所以沒做處理,但是代碼中有在tab非吸頂狀態(tài)時候,外層RecyclerView優(yōu)先處理滑動事件的代碼
個人感覺第一種處理方式比較好一點,demo的代碼如下(需要請自行修改,PagerFragment.java)
                        if(! ((MainActivity)getActivity()).isStick){
                            ((MainActivity)getActivity()).adjustScroll(true);
                            return false;
                        }

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

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

   private float downX ;    //按下時 的X坐標(biāo)
    private float downY ;    //按下時 的Y坐標(biāo)
    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        float x= e.getX();
        float y = e.getY();
        switch (e.getAction()){
            case MotionEvent.ACTION_DOWN:
                //將按下時的坐標(biāo)存儲
                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方法供外部調(diào)用.代碼可以看出,橫向的滑動直接放行,讓ViewPager處理,向上滑動時候如果tab吸頂了且已經(jīng)滑動到底部,交給內(nèi)部的RecyclerView處理,否則自己處理.

我們對內(nèi)層的RecyclerView進行處理,重寫其onTouchEvent方法

     @Override
    public boolean onTouchEvent(MotionEvent e) {
        float x= e.getX();
        float y = e.getY();
        switch (e.getAction()){
            case MotionEvent.ACTION_DOWN:
                //將按下時的坐標(biāo)存儲
                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':
                                //內(nèi)層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;
    }

其中的回調(diào)是為了告訴外層的RecyclerView需不需要攔截事件.

滑動沖突到這里基本上處理完了,下面說下吸頂?shù)膯栴},其實只是思路的問題,這里采取的方式是將TabLayout和ViewPager當(dāng)做一個外層RecyclerView的最后一個item,并且高度為屏幕高度-狀態(tài)欄高度,這樣當(dāng)外層RecyclerView滑動到底部,Tab看上去是吸頂?shù)?

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

最后暴露一個問題,在外層RecyclerView滑動到底部時,需要將觸摸事件交給內(nèi)層的RecyclerView處理時,按照Demo里的處理方式,手指抬起之后重新滑動,內(nèi)層RecyclerView才能拿到事件,原因是Demo判斷外層RecyclerView是否滑動到底部的代碼寫在onInterceptTouchEvent里面,這個方法并不會實時調(diào)用,試過將判斷寫在onTouchEvent里面,實時判斷再調(diào)用onInterceptTouchEvent,但是好像因為內(nèi)層的RecyclerView并沒有消費掉事件,所以這么做并沒有效果,并沒有實時的將觸摸事件交給內(nèi)層RecyclerView處理,這里嘗試了很多方式,都不太理想,希望有思路的大佬給指點一下,效果如下:
GIF2.gif

項目地址:Github

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末秃殉,一起剝皮案震驚了整個濱河市本涕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌钦无,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡常挚,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門稽物,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人折欠,你說我怎么就攤上這事贝或『鸸” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵咪奖,是天一觀的道長盗忱。 經(jīng)常有香客問我,道長羊赵,這世上最難降的妖魔是什么趟佃? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮昧捷,結(jié)果婚禮上闲昭,老公的妹妹穿的比我還像新娘。我一直安慰自己靡挥,他們只是感情好序矩,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著跋破,像睡著了一般簸淀。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上毒返,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天租幕,我揣著相機與錄音,去河邊找鬼拧簸。 笑死令蛉,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的狡恬。 我是一名探鬼主播珠叔,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼弟劲!你這毒婦竟也來了祷安?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤兔乞,失蹤者是張志新(化名)和其女友劉穎汇鞭,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體庸追,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡霍骄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了淡溯。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片读整。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖咱娶,靈堂內(nèi)的尸體忽然破棺而出米间,到底是詐尸還是另有隱情强品,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布屈糊,位于F島的核電站的榛,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏逻锐。R本人自食惡果不足惜夫晌,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望昧诱。 院中可真熱鬧晓淀,春花似錦、人聲如沸鳄哭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽妆丘。三九已至锄俄,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間勺拣,已是汗流浹背奶赠。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留药有,地道東北人毅戈。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像愤惰,于是被迫代替她去往敵國和親苇经。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容