RecycleView 實現複雜首頁佈局三種方式

做過電商類應用的朋友可能都會遇到一個比較頭疼的問題:複雜的首頁佈局如何實現。參考百度糯米,美團,bilibili等應用,都會發現其首頁的佈局相對複雜,例如下圖bilibili的首頁(第二張是demo實現的效果圖),可以看到在同一個頁面中先是有列表佈局出現,然後出現了2列的網格佈局,接着3列的網格佈局,最後還出現了瀑布流式佈局:

這裏寫圖片描述 這裏寫圖片描述

這樣的效果該怎麼做呢?是使用LinearLayoutManager、GridLayoutManager還是StaggeredGridLayoutManager?還是根本不是使用的RecycleView,是用ScrollView硬佈局實現的?或者使用了多個RecycleView進行嵌套,來實現3種混合佈局的排版的?

下面我們一條一條進行梳理:

  • 首先,我們發現頁面的長度是無限長度的,可以不斷下拉刷新,所有排除ScrollView的可能,基本斷定是使用的是RecyclerView
  • 我們注意到同一個頁面中出現了3中混合佈局的排版,有可能是使用RecycleView進行了2級嵌套,在線性RecycleView中嵌套了網格和瀑布的RecycleView?
  • 如果沒有進行嵌套的話,有沒有辦法用一種自定義的佈局管理器實現這3種效果呢?
  • 會不會是使用了某些3方控件?

針對以上3點疑問,我分別提供3種對應的解決方案來實現上圖的效果:

方案一:RecyclerView的2級嵌套

看到同一個滾動控件中出現了3種混合佈局,多數人第一映像就是進行嵌套。 
如果進行嵌套的話,嵌套什麼?從效果圖來看,上圖的一個欄目中的視圖數量似乎是固定的,這意味着可以使用RelativeLayout等佈局進行硬排版。確實如果真是固定的這樣做當然更好,但是注意到點擊每個欄目上的刷新按鈕的時候,偶爾會出現兩個視圖交換位置的動畫,這是RecyclerView特有的,而且也沒有人告訴我每個欄目中的視圖數量就是固定,萬一哪天又多了一排呢,所以我們還是嵌套RecyclerView,具體嵌套規則如下圖:

這裏寫圖片描述

其實最外層的RecyclerView1換成ScrollView也可以,只要把內部的RecyclerView依次拼接起來就可以,實現起來也更加簡單,實現方式千千萬,自己選個喜歡的而已,我只是爲了便於拓展,萬一需要動態的增加欄目呢。

重新getItemViewType方法爲每個position位置的item設置不同的type類型,然後在onCreateViewHolder創建對應的Holder,最後在onBindViewHolder爲不同類型的item設置不同的Fully佈局的子RecyclerView就行了。

上關鍵代碼(有刪減):

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (holder instanceof MyViewHolder1){
            holder.child_recyclerView.setLayoutManager(new FullyLinearLayoutManager(context));
        }else if (holder instanceof MyViewHolder2){
            holder.child_recyclerView.setLayoutManager(new FullyGridLayoutManager(context, 2, GridLayoutManager.VERTICAL, false));
        }else if (holder instanceof MyViewHolder3){
            holder.child_recyclerView.setLayoutManager(new FullyGridLayoutManager(context, 3, GridLayoutManager.VERTICAL, false));
        }else if (holder instanceof MyViewHolder4){
            holder.child_recyclerView.setLayoutManager(new FullyGridLayoutManager(context, 3, GridLayoutManager.VERTICAL, false));
        }
    }gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
       @Override
       public int getSpanSize(int position) {
          return gridManager.getSpanCount();
       }
} @Override
    public void onAttachedToRecyclerView(final RecyclerView recyclerView) {
        super.onAttachedToRecyclerView(recyclerView);

        RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
        if(manager instanceof GridLayoutManager) {
            final GridLayoutManager gridManager = ((GridLayoutManager) manager);
            gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    int type = getItemViewType(position);
                    switch (type){
                        case TYPE_SLIDER:
                        case TYPE_TYPE2_HEAD:
                        case TYPE_TYPE3_HEAD:
                            return 6;
                        case TYPE_TYPE2:
                            return 3;
                        case TYPE_TYPE3:
                            return 2;
                        default:
                            return 3;
                    }
                }
            });
        }
    }

這種方式我不多介紹,因爲在前一篇也說過,google不建議對RecyclerView、ListView進行嵌套,嵌套使用的話那麼視圖複用機制就等於白費了,

依然會在第一次加載時初始化所有的item,如果數據量過大則會產生性能障礙,所以我也遵循google的建議不推薦使用嵌套解決問題,我更加推薦下

面一種方案。

方案二:使用特殊的(自定義)佈局管理器

這種方案是我認爲最爲優秀的做法,它完全符合Google制定的標準:使用佈局管理器來管理佈局。我們繼續觀察首頁佈局的圖示,我們真的要爲了實現這種混合佈局自己去寫一個佈局管理器嗎?我們發現上面出現了列表、網格、瀑布流3種交叉混排的混合佈局。我們先把瀑布流放在一邊,仔細想想如果我們把網格的列數設置爲1列,那不就是一個列表佈局嗎,也就是說我們使用網格佈局管理器就可以做出列表的樣式,所以說雖然是說用自定義佈局管理器,但實際上不需要我們自定義,GridLayoutManager爲我們提供了動態改變每個item所佔列數的方法:

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (holder instanceof MyViewHolder1){
            holder.child_recyclerView.setLayoutManager(new FullyLinearLayoutManager(context));
        }else if (holder instanceof MyViewHolder2){
            holder.child_recyclerView.setLayoutManager(new FullyGridLayoutManager(context, 2, GridLayoutManager.VERTICAL, false));
        }else if (holder instanceof MyViewHolder3){
            holder.child_recyclerView.setLayoutManager(new FullyGridLayoutManager(context, 3, GridLayoutManager.VERTICAL, false));
        }else if (holder instanceof MyViewHolder4){
            holder.child_recyclerView.setLayoutManager(new FullyGridLayoutManager(context, 3, GridLayoutManager.VERTICAL, false));
        }
    }gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
       @Override
       public int getSpanSize(int position) {
          return gridManager.getSpanCount();
       }
} @Override
    public void onAttachedToRecyclerView(final RecyclerView recyclerView) {
        super.onAttachedToRecyclerView(recyclerView);

        RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
        if(manager instanceof GridLayoutManager) {
            final GridLayoutManager gridManager = ((GridLayoutManager) manager);
            gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    int type = getItemViewType(position);
                    switch (type){
                        case TYPE_SLIDER:
                        case TYPE_TYPE2_HEAD:
                        case TYPE_TYPE3_HEAD:
                            return 6;
                        case TYPE_TYPE2:
                            return 3;
                        case TYPE_TYPE3:
                            return 2;
                        default:
                            return 3;
                    }
                }
            });
        }
    }

getSpanSize方法,返回值就表示當前item佔多少列,例如如果我們列數設置的爲3列,返回3就表示鋪滿,也就是和列表一樣了。

如圖所示,我們給RecyclerView設置一個列數爲6的GridLayoutManager,然後再動態地爲不同部位的item分別設置SpanSize爲6(鋪滿)、3(1/2)、2(1/3)就行了

這裏寫圖片描述

設置一個列數爲6的GridLayoutManager:

recyclerView.setLayoutManager(new GridLayoutManager(recyclerView.getContext(),6, GridLayoutManager.VERTICAL, false));

在onAttachedToRecyclerView方法中動態爲不同position設置不同的SpanSize:

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (holder instanceof MyViewHolder1){
            holder.child_recyclerView.setLayoutManager(new FullyLinearLayoutManager(context));
        }else if (holder instanceof MyViewHolder2){
            holder.child_recyclerView.setLayoutManager(new FullyGridLayoutManager(context, 2, GridLayoutManager.VERTICAL, false));
        }else if (holder instanceof MyViewHolder3){
            holder.child_recyclerView.setLayoutManager(new FullyGridLayoutManager(context, 3, GridLayoutManager.VERTICAL, false));
        }else if (holder instanceof MyViewHolder4){
            holder.child_recyclerView.setLayoutManager(new FullyGridLayoutManager(context, 3, GridLayoutManager.VERTICAL, false));
        }
    }gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
       @Override
       public int getSpanSize(int position) {
          return gridManager.getSpanCount();
       }
} @Override
    public void onAttachedToRecyclerView(final RecyclerView recyclerView) {
        super.onAttachedToRecyclerView(recyclerView);

        RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
        if(manager instanceof GridLayoutManager) {
            final GridLayoutManager gridManager = ((GridLayoutManager) manager);
            gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    int type = getItemViewType(position);
                    switch (type){
                        case TYPE_SLIDER:
                        case TYPE_TYPE2_HEAD:
                        case TYPE_TYPE3_HEAD:
                            return 6;
                        case TYPE_TYPE2:
                            return 3;
                        case TYPE_TYPE3:
                            return 2;
                        default:
                            return 3;
                    }
                }
            });
        }
    }

那麼有朋友就要問了,說好的瀑布流呢? 

我查閱了StaggeredGridLayoutManager,發現它並沒有提供動態設置所佔列的方法,只是在StaggeredGridLayoutManager.LayoutParams中提供了這樣一個方法:

LayoutParams.setFullSpan(true);

作用是把當前item的寬度設爲full(填滿),也就是說如果使用StaggeredGridLayoutManager要麼不設置,要麼就只能填滿,

所以無法完成圖上的效果,我們也並不是非要完全仿照它,bilibili在最近一次更新後也放棄使用瀑布流式的佈局了,統一爲列表和網格式混排。

當然如果要實現圖上的效果也不是沒有辦法,只需要換一種方式,改一下item,把設置爲FullSpan的item設置爲一個多個視圖組合的複合item就行了,放個圖,代碼就不上了:

這裏寫圖片描述


哪種方案最好?

上面的2種方案中,無疑第二種是最佳的解決方案,沒有引入過多的依賴,完全使用RecyclerView佈局管理器的特性實現,從性能上來說也最佳。但是TwowayView能做到更多其他的支持,比如網格佈局等(具體參見github : twoway-view),也不失爲一種好的方式,而第一種方案我也就不多說了,不推薦,這種嵌套不僅讓代碼邏輯變得複雜凌亂(demo裏寫嵌套的時候我差點沒寫吐),同時還有性能障礙,所以給出一個結論:2>1

下載

最後,附上demo下載: 
demo下載地址

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