1. 程式人生 > >一步步教你實現完整的複雜列表佈局

一步步教你實現完整的複雜列表佈局

本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出

引子:我們在工作中遇到最多的檢視場景恐怕就是各種樣式的列表了,這也是由手機螢幕有限的尺寸決定的,隨著需求的日益豐滿,我們會發現列表的樣式也隨之做著各種各樣的變更:樣式越來越多了,佈局越來越複雜了,如果我們前面的佈局是單純將各種ViewGroup拼接到一塊的,那改動起來就費事了,暫且不說資料量大引起的卡頓問題,面臨的工作量絕不是修改佈局檔案就能搞定的,資料的繫結、事件觸發的設定、滑動的處理、手勢衝突的解決…甚至還可能要加上些高階UI特效。開啟淘寶或京東,我們能看到這樣的佈局樣式:

圖1:
圖1

圖2:
圖2

圖3:
圖3

圖4:
圖4

圖5:
圖5

圖6:
圖6

相信做過電商類app的朋友在產品拿到我們面前這樣的頁面時都糾結過如何去實現它,大概有這樣幾種思路:
1、 老老實實寫佈局,UI有多少內容統統手寫出來。呵呵。
2、 利用滾動控制元件做巢狀,例如圖1可以利用ScrollView巢狀多個GridView實現,圖2、4、5可以利用ListView巢狀GridView實現;
3、 利用RecyclerView的多級巢狀實現,例如實現這樣的佈局:
複雜佈局

當資料量較大、分屏頁數較多的時候,2和3會出現明顯的卡頓,這是因為cpu需要同時處理各個滑動佈局的內部item位置關係以及資料的賦值,這樣一來就容易出現cpu和gpu的計算與展示出現不同步的現象,導致螢幕顯示丟幀,造成視覺卡頓的現象,尤其是當item的佈局又很複雜時更容易出現此情況,甚至可能出現oom。
4、 使用RecyclerView實現全佈局,用一個RecyclerView實現複雜的佈局列表,這一種是完全符合谷歌的設計標準的,同時充分利用了RecyclerView雙快取的原理,下面我通過一個很典型的例子,帶著大家一步步實現它,並通過該例子,解析以上幾種常見的佈局樣式,相信看完後你可能跟我有同樣的感覺:絕大部分的複雜列表都是有規可循的。

該篇將會依次介紹到:

1. RecyclerView的雙快取技術簡介

2. 複雜佈局的典型樣式;

3. 實現典型複雜佈局的列表介面卡;

4. 實現RecyclerView的上拉載入功能;

5. 分割線的原理介紹,實現複雜佈局的分割線;

6. 新增空佈局的實現

7. 逐個分析以上電商app出現的頁面佈局,提供具體的實現思路。

說明:本文旨在為實現複雜列表佈局提供一種完整的思路,並沒有做過多的封裝,希望能起到拋磚引玉的作用,文中所涉及程式碼已上傳至GitHub,文末給出連結。另外,我打算一篇文章講完所有內容,不再做切割,如果有對RecyclerView不瞭解的同學,建議先去查閱相關資料。

一、 RecyclerView的雙快取技術簡單介紹:

RecyclerView內部維護了一個二級快取(算上使用者設定的,實際上擁有三級快取),這些快取是由RecyclerView的一個final型別的內部類所管理的,實際上由其以下快取變數決定:
快取變數
快取與複用的原理,看下 Google IO 視訊中的一張截圖:
快取關係圖

我們看到,當ViewHolder滑出頁面時,會暫時存放到Cache中,而從Cache中移除的holder,會存放到RecyclerViewPool的迴圈快取池之中,預設情況下,Cache快取2個holder,RecyclerViewPool快取5個holder,另外不同的viewType的快取互相沒有影響。

二、 複雜佈局的典型樣式:

實際上,以上列舉的佈局樣式,可以大致歸納為下圖所示:
複雜佈局歸納樣式
看著好複雜的樣子,舉個例子:
這裡寫圖片描述

我們約定:
列表的最頂部的佈局叫做Header,例如常見的輪播圖;
列表中間區域我們稱之為分組;
分組的標題部分我們稱之為SectionHeader;
分組的內容項我們稱之為SectionBody;
分組的結尾稱之為SectionFooter;
列表的結束佈局稱之為Footer;

三、 實現典型複雜佈局的列表介面卡:

先看下我們要實現的效果:
這裡寫圖片描述
首先定義一個抽象類SectionedRecyclerViewAdapter,繼承RecyclerView.Adapter,
接著對資料分組,定義四個陣列,分別記錄每項分組的頭部資料的section的位置,分組內的每一項的position的位置:

    //用來儲存分組section位置
    private int[] sectionForPosition = null;

    //用來儲存分組內的每項的position位置
    private int[] positionWithinSection = null;

    //用來記錄每個位置是否是一個組內Header
    private boolean[] isHeader = null;

    //用來記錄每個位置是否是一個組內Footer
    private boolean[] isFooter = null;

    //item的總數,注意,是總數,包含所有項
    private int count = 0;

例如有這樣一種資料結構:
這裡寫圖片描述
其中的年級個班級可分別表示為section和position;
接著準備遊標,標記各組的view:

    //用來標記每個分組的Header
    protected static final int TYPE_SECTION_HEADER = -1;

    //用來標記每個分組的Footer
    protected static final int TYPE_SECTION_FOOTER = -2;

    //用來標記每個分組的內容
    protected static final int TYPE_ITEM = -3;

    //用來標記整個列表的Header
    protected static final int TYPE_HEADER = 0;  //頂部HeaderView

    //用來標記整個列表的Footer
    protected static final int TYPE_FOOTER = 1;  //底部FooterView

    //上拉載入更多
    public static final int PULLUP_LOAD_MORE = 0;

    //正在載入中
    public static final int LOADING_MORE = 1;

    //載入完成
    public static final int LOADING_FINISH = 2;

    //空佈局
    public static final int TYPE_EMPTY = -4;

    //上拉載入預設狀態--預設為-1
    public int load_more_status = -1;

原始碼裡我儘可能的都加上了註釋,這裡我只擷取關鍵部分,
實際上最關鍵的部分是做各個item所在位置關係的計算:
第1步:計算出item的總數量,這裡定義了一個抽象方法,用來標識當前的分組是否含有SectionFooter,有的話,遍歷時要多加1;
第2步:得到item的總數量後,初始化幾個陣列:初始化與position相對應的section陣列,初始化與section相對應的position的陣列,初始化當前位置是否是一個Header的陣列,初始化當前位置是否是一個Footer的陣列;
第3步:通過計算每個item的位置資訊,將上一步初始化後的陣列填充資料,最終這幾個陣列儲存了每個位置的item的狀態資訊,即:是否是header,是否是footer,所在的position是多少,所在的section是多少:

private void setupPosition() {
        count = countItems();//計算出item的總數量
        setupArrays(count);//得到item的總數量後,初始化幾個陣列:初始化與position相對應的section陣列,初始化與section相對應的position
        // 的陣列,初始化當前位置是否是一個Header的陣列,初始化當前位置是否是一個Footer的陣列
        calculatePositions();//通過計算每個item的位置資訊,將上一步初始化後的陣列填充資料,最終這幾個陣列儲存了每個位置的item
        // 的狀態資訊,即:是否是header,是否是footer,所在的position是多少,所在的section是多少
    }

    /**
     * 計算item的總數量
     *
     * @return
     */
    private int countItems() {
        int count = 0;
        int sections = getSectionCount();

        for (int i = 0; i < sections; i++) {
            count += 1 + getItemCountForSection(i) + (hasFooterInSection(i) ? 1 : 0);
        }
        return count;
    }

    /**
     * 通過item的總數量,初始化幾個陣列:初始化與position相對應的section陣列,
     * 初始化與section相對應的position的陣列,初始化當前位置是否是一個Header的陣列,
     * 初始化當前位置是否是一個Footer的陣列
     *
     * @param count
     */
    private void setupArrays(int count) {
        sectionForPosition = new int[count];
        positionWithinSection = new int[count];
        isHeader = new boolean[count];
        isFooter = new boolean[count];
    }

    /**
     * 通過計算每個item的位置資訊,將上一步初始化後的陣列填充資料,
     * 最終這幾個陣列儲存了每個位置的item的狀態資訊,即:是否是header,是否是footer,
     * 所在的position是多少,所在的section是多少
     */
    private void calculatePositions() {
        int sections = getSectionCount();
        int index = 0;

        for (int i = 0; i < sections; i++) {
            setupItems(index, true, false, i, 0);
            index++;

            for (int j = 0; j < getItemCountForSection(i); j++) {
                setupItems(index, false, false, i, j);
                index++;
            }

            if (hasFooterInSection(i)) {
                setupItems(index, false, true, i, 0);
                index++;
            }
        }
    }

    /**
     * 儲存每個位置對應的資料資訊
     *
     * @param index    從0開始的每個最小單位所在的位置,從0開始,到count結束
     * @param isHeader 所在index位置的item是否是header
     * @param isFooter 所在index位置的item是否是footer
     * @param section  所在index位置的item對應的section
     * @param position 所在index位置的item對應的position
     */
    private void setupItems(int index, boolean isHeader, boolean isFooter, int section, int
            position) {
        this.isHeader[index] = isHeader;
        this.isFooter[index] = isFooter;
        sectionForPosition[index] = section;
        positionWithinSection[index] = position;
    }

RecyclerView有一個內部類AdapterDataObserver,看起名稱就知道是用來監控adapter資料集變化的,我們自定義一個內部類,繼承它,複寫onChanged()方法,當資料集合放生變化時,重新計算各ItemView之間的位置關係:

//定義一個內部類,每當資料集合發生改變時,設定控制元件的位置資訊
    class SectionDataObserver extends RecyclerView.AdapterDataObserver {
        @Override
        public void onChanged() {
            setupPosition();
            checkEmpty();//檢查資料是否為空,設定空佈局
        }
        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            checkEmpty();//檢查資料是否為空,設定空佈局
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            checkEmpty();//檢查資料是否為空,設定空佈局
        }
    }

接著複寫onCreateViewHolder,繫結佈局型別,這裡定義了6種類型的佈局:

@Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        RecyclerView.ViewHolder viewHolder;
        if (viewType == TYPE_EMPTY) {
            viewHolder = new EmptyViewHolder(emptyView);
        } else {
            if (isSectionHeaderViewType(viewType)) {
                viewHolder = onCreateSectionHeaderViewHolder(parent, viewType);
            } else if (isSectionFooterViewType(viewType)) {
                viewHolder = onCreateSectionFooterViewHolder(parent, viewType);
            } else if (isFooterViewType(viewType)) {
                viewHolder = onCreateFooterViewHolder(parent, viewType);
            } else if (isHeaderViewType(viewType)) {
                viewHolder = onCreateHeaderViewHolder(parent, viewType);
            } else {
                viewHolder = onCreateItemViewHolder(parent, viewType);
            }
        }
        return viewHolder;
    }

接著實現onBindViewHolder,這裡做了不同情況的區分:當整個列表擁有頭佈局的時候是一種情況,沒有頭佈局的時候是一種情況:

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (emptyViewVisible) {//此時資料集為空,需要設定空佈局
        } else {
            setViewHolder(holder, position);
        }
    }

    private void setViewHolder(RecyclerView.ViewHolder holder, final int position) {
        if (hasHeader()) {//如果整個列表有header
            if (position == 0) {
                onBindHeaderViewHolder((RH) holder);
            } else if (position + 1 < getItemCount()) {
                final int section = sectionForPosition[position - 1];
                int index = positionWithinSection[position - 1];
                if (isSectionHeaderPosition(position - 1)) {//當前位置是分組header
                    onBindSectionHeaderViewHolder((H) holder, section);
                    holder.itemView.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            onSectionHeaderClickListener.onSectionHeaderClick(section);
                        }
                    });

                } else if (isSectionFooterPosition(position - 1)) {//當前位置是分組的footer

                    onBindSectionFooterViewHolder((F) holder, section);
                    holder.itemView.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            if (onSectionFooterClickListener != null) {
                                onSectionFooterClickListener.onSectionFooterClick(section);
                            }
                        }
                    });

                } else {//當前位置是組內item
                    onBindItemViewHolder((VH) holder, section, index);

                    holder.itemView.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            onItemClickListener.onItemClick(section, position - 1);
                        }
                    });

                    holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
                        @Override
                        public boolean onLongClick(View v) {
                            if (onItemLongClickListener != null) {
                                onItemLongClickListener.onItemLongClick(section, position - 1);
                            }
                            return true;
                        }
                    });

                }
            } else {//當前位置是整個列表的footer
                onBindFooterViewHolder((FO) holder);
            }
        } else {//整個列表沒有Header
            if (position + 1 < getItemCount()) {
                final int section = sectionForPosition[position];
                int index = positionWithinSection[position];
                if (isSectionHeaderPosition(position)) {//當前位置是分組Header
                    onBindSectionHeaderViewHolder((H) holder, section);
                    holder.itemView.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            if (onSectionHeaderClickListener != null) {
                                onSectionHeaderClickListener.onSectionHeaderClick(section);
                            }
                        }
                    });

                } else if (isSectionFooterPosition(position)) {//當前位置是分組footer

                    onBindSectionFooterViewHolder((F) holder, section);
                    holder.itemView.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            if (onSectionFooterClickListener != null) {
                                onSectionFooterClickListener.onSectionFooterClick(section);
                            }
                        }
                    });

                } else {//當前位置是分組的item
                    onBindItemViewHolder((VH) holder, section, index);
                    holder.itemView.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            if (onItemClickListener != null) {
                                onItemClickListener.onItemClick(section, position);
                            }
                        }
                    });

                    holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
                        @Override
                        public boolean onLongClick(View v) {
                            if (onItemLongClickListener != null) {
                                onItemLongClickListener.onItemLongClick(section, position);
                            }
                            return true;
                        }
                    });

                }
            } else {//當前位置是整個列表的footer
                onBindFooterViewHolder((FO) holder);
            }
        }
    }

接著複寫getItemViewType,告知RecyclerView在各個位置的item是屬於哪一個佈局型別的:

@Override
    public int getItemViewType(int position) {
        if (sectionForPosition == null) {
            setupPosition();
        }
        if (emptyViewVisible) {
            return TYPE_EMPTY;
        } else {
            if (hasHeader()) {
                if (position == 0) {
                    return getHeaderViewType();
                } else if (position + 1 < getItemCount()) {
                    int section = sectionForPosition[position - 1];
                    int index = positionWithinSection[position - 1];
                    if (isSectionHeaderPosition(position - 1)) {
                        return getSectionHeaderViewType(section);
                    } else if (isSectionFooterPosition(position - 1)) {
                        return getSectionFooterViewType(section);
                    } else {
                        return getSectionItemViewType(section, index);
                    }
                }
                return getFooterViewType();
            } else {
                if (position + 1 < getItemCount()) {
                    int section = sectionForPosition[position];
                    int index = positionWithinSection[position];
                    if (isSectionHeaderPosition(position)) {
                        return getSectionHeaderViewType(section);
                    } else if (isSectionFooterPosition(position)) {
                        return getSectionFooterViewType(section);
                    } else {
                        return getSectionItemViewType(section, index);
                    }
                }
                return getFooterViewType();
            }
        }
    }

這樣我們的基本的多佈局的adapter基類就算完成了,裡面我定義了分項item的點選事件和取資料的方法;
使用很簡單,我們定義好各項的佈局:Header的佈局、Footer的佈局、SectionHeader的佈局、SectionFooter的佈局、上拉載入的佈局,接著實現各自的ViewHolder:

public class FooterHolder extends RecyclerView.ViewHolder {

    public TextView tvFooter;

    public FooterHolder(View itemView) {
        super(itemView);
        initView();
    }

    private void initView() {
        tvFooter = (TextView) itemView.findViewById(R.id.tv_footer);
    }
}

,然後就可以定義我們具體的介面卡,讓它繼承自寫好的SectionedRecyclerViewAdapter,在裡面完成資料的繫結與展示,在這裡,有個地方需要注意的是,需要動態設定SectionBody的每個item長和寬,並設定其左右邊距,需要做一個計算,看圖:
這裡寫圖片描述

程式碼設定:

@Override
    protected void onBindItemViewHolder(EvaluateSectionBodyHolder holder, int section, int position) {
        int screenWidth = DisplayUtil.getScreenWidthPixels((Activity)mContext);
        int imgWidth = (screenWidth - DisplayUtil.dp2px(mContext, 40)) / 3;
        ViewGroup.MarginLayoutParams params = null;
        if (holder.llRoot.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
            params = (ViewGroup.MarginLayoutParams) holder.llRoot.getLayoutParams();
        } else {
            params = new ViewGroup.MarginLayoutParams(holder.llRoot.getLayoutParams());
        }
        params.width = imgWidth;
        params.height = imgWidth;
        if (position % 3 == 0) {
            params.leftMargin = DisplayUtil.dp2px(mContext, 10);
        } else if (position % 3 == 1) {
            params.leftMargin = DisplayUtil.dp2px(mContext, 20/3);
        } else {
            params.leftMargin = DisplayUtil.dp2px(mContext, 10/3);
        }
        holder.llRoot.setLayoutParams(params);
    }

接著,每行的列數,實際上是由GridLayoutManager.SpanSizeLookup這個類去控制的,我們繼承它,實現控制我們任何地方要展示的列數:

public class SectionedSpanSizeLookup extends GridLayoutManager.SpanSizeLookup {

    protected SectionedRecyclerViewAdapter<?, ?, ?, ?, ?> adapter = null;
    protected GridLayoutManager layoutManager = null;

    public SectionedSpanSizeLookup(SectionedRecyclerViewAdapter<?, ?, ?, ?, ?> adapter, GridLayoutManager layoutManager) {
        this.adapter = adapter;
        this.layoutManager = layoutManager;
    }

    @Override
    public int getSpanSize(int position) {
        if (adapter.hasHeader()) {
            if (position == 0) {
                return layoutManager.getSpanCount();
            } else if (position + 1 < adapter.getItemCount()) {
                if (adapter.isSectionHeaderPosition(position -1) || adapter.isSectionFooterPosition(position -1)) {
                    return layoutManager.getSpanCount();
                } else {
                    return 1;
                }
            } else {
                return layoutManager.getSpanCount();
            }
        } else {
            if (position + 1 < adapter.getItemCount()) {
                if (adapter.isSectionHeaderPosition(position) || adapter.isSectionFooterPosition(position)) {
                    return layoutManager.getSpanCount();
                } else {
                    return 1;
                }
            } else {
                return layoutManager.getSpanCount();
            }
        }
    }
}

四、 實現RecyclerView的上拉載入功能:

我們需要監聽RecyclerView的OnScrollListener,定義一個類,繼承自它,這裡我們需要做的是:
1、 當滾動狀態為SCROLL_STATE_IDLE時,判斷當前item的總數是否填充滿了一屏,如果沒滿,也就沒有上拉載入了;
2、 當可見item的最後一個可見的item的位置與item的總數一致時,進行下一步;
3、 加一個識別符號isLoading,為true表示正在請求,請求結束後置為false,防止多次請求;
這裡有一個細節,就是滑動邊界的容差值,當childView邊界完全顯示在介面中時才會檢測成功.這就導致了一個可能的情況是隻差一點點滑動到邊界時,也不會檢測成功而出發上拉載入的回撥,所以要求很高的靈敏度,故加上上下兩個容差值,我們認為,當滑動到接近邊界時,就認為需要進行上拉載入了,關鍵程式碼如下:

/**
     * 檢查是否滿一屏
     *
     * @param recyclerView
     * @return
     */
    public boolean isFullAScreen(RecyclerView recyclerView) {
        //獲取item總個數,一般用mAdapter.getItemCount(),用mRecyclerView.getLayoutManager().getItemCount()也可以
        //獲取當前可見的item view的個數,這個數字是不固定的,隨著recycleview的滑動會改變,
        // 比如有的頁面顯示出了6個view,那這個數字就是6。此時滑一下,第一個view出去了一半,後邊又加進來半個view,此時getChildCount()
        // 就是7。所以這裡可見item view的個數,露出一半也算一個。
        int visiableItemCount = recyclerView.getChildCount();
        if (visiableItemCount > 0) {
            View lastChildView = recyclerView.getChildAt(visiableItemCount - 1);
            //獲取第一個childView
            View firstChildView = recyclerView.getChildAt(0);
            int top = firstChildView.getTop();
            int bottom = lastChildView.getBottom();
            //recycleView顯示itemView的有效區域的bottom座標Y
            int bottomEdge = recyclerView.getHeight() - recyclerView.getPaddingBottom() + bottomOffset;
            //recycleView顯示itemView的有效區域的top座標Y
            int topEdge = recyclerView.getPaddingTop() + topOffset;
            //第一個view的頂部小於top邊界值,說明第一個view已經部分或者完全移出了介面
            //最後一個view的底部小於bottom邊界值,說明最後一個view已經完全顯示在介面
            //若滿足這兩個條件,說明所有子view已經填充滿了recycleView,recycleView可以"真正地"滑動
            if (bottom <= bottomEdge && top < topEdge) {
                //滿屏的recyceView
                return true;
            }
            return false;
        } else {
            return false;
        }
    }
@Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        if (isFullAScreen(recyclerView)) {
            //查詢最後一個可見的item的position
            lastItemPosition = gridLayoutManager.findLastVisibleItemPosition();
            if (newState == RecyclerView.SCROLL_STATE_IDLE && lastItemPosition + 1 ==
                    gridLayoutManager.getItemCount()) {
                if (!isLoading) {
                    onLoadMore();
                }
            }
        }
    }

五、 分割線的原理介紹,實現複雜佈局的分割線:

要實現分割線,需要自定義類繼承自RecyclerView.ItemDecoration,ItemDecoration有三個方法提供給開發者拓展,依次為:

getItemoffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state ):outRect為包裹在itemView外層View的座標引數,如下圖,例如:設定outRect.set(0,0,0,0);表示itemView的四周沒有任何的分割空間存在,set的四個引數分別表示外圍View距離itemView左邊、上方、右邊、下方四個方向的間隔距離,尤其需要注意的是,在此處設定了outRect的四個方向的引數之後,會預設將這個間距設定到itemView的四個方向的padding上,由此我們可以知道,getItemoffsets這個方法是我們必須要複寫且實現的。

onDraw(Canvas c, RecyclerView parent, RecyclerView.State state):外圍View的繪製將被itemView遮蓋。

onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state):外圍View將繪製在itemView之上,即遮蓋itemView,當我們繪製一些特殊需求時,此方法很是受用,例如:為每個item繪製一個角標,表示其狀態,例如很多商品都有熱賣或者優惠券的角標,利用畫筆將bitmap繪製出即可:
這裡寫圖片描述

我先把我們最終要實現的效果圖貼出來,看下我們要繪製的分割線的模樣和位置:

這裡寫圖片描述

我們看到,只有每個分組的頂部存在分割線,並且列表的第一個的頂部是不需要分割線的,實現的思路也很簡單,我們只需要計算出每個分組Header所在的位置,然在在Header的頂部設定好外圍空間即可:

 @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State
            state) {
        super.getItemOffsets(outRect, view, parent, state);
        int totalCount = parent.getAdapter().getItemCount();
        int itemPosition = parent.getChildAdapterPosition(view);
        if (isDraw(parent, view, totalCount)) {
            if (itemPosition == 0) {
                outRect.set(0, 0, 0, 0);
            } else {
                outRect.set(0, mDividerHeight, 0, 0);
            }
        }
    }

    /**
     * 是否可以繪製分割線
     * @param parent     當前的RecyclerView
     * @param itemView   當前的內容項
     * @param totalCount 介面卡的item總數,可能大於RecyclerView的item總數
     * @return
     */
    private boolean isDraw(RecyclerView parent, View itemView, int totalCount) {
        int itemPosition = parent.getChildAdapterPosition(itemView);
        if (totalCount > 1 && itemPosition < totalCount - 1) {//要除去footer佔有的一個位置
            if (parent.getAdapter() instanceof SectionedRecyclerViewAdapter) {
                if (((SectionedRecyclerViewAdapter) parent.getAdapter()).isSectionHeaderPosition
                        (itemPosition)) {
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        drawHorizontal(c, parent);
    }

    /**
     * 繪製水平的分割線
     * @param c
     * @param parent
     */
    private void drawHorizontal(Canvas c, RecyclerView parent) {

        int totalCount = parent.getAdapter().getItemCount();

        //獲取當前可見的item的數量,半個也算
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            //獲取當前可見的view
            View child = parent.getChildAt(i);
            if (isDraw(parent, child, totalCount)) {
                RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
                int left = child.getLeft() - params.leftMargin;//元件在容器X軸上的起點,需要注意,如果使用者設定了left方向的Margin值,需要在取得itemViewleft屬性後,將該margin抵消掉,因為,使用者設定margin的意圖明顯不是想讓分割線覆蓋掉的
                int right = child.getRight() + params.rightMargin ;
                int top = child.getTop() - mDividerHeight - params.bottomMargin;//元件在容器Y軸上的起點
                int bottom = top + mDividerHeight;
                if (mDividerDrawable != null) {
                    mDividerDrawable.setBounds(left, top, right, bottom);
                    mDividerDrawable.draw(c);
                }
                if (mDividerPaint != null) {
                    c.drawRect(left, top, right, bottom, mDividerPaint);
                }
            }
        }
    }

需要注意,如果使用者設定了各方向的Margin值,需要在取得itemView的各方向的margin屬性後,將該margin抵消掉,因為,使用者設定margin的意圖明顯不是想讓分割線覆蓋掉的。
最後補充的是,各子View的點選事件,實際上,在SectionedRecyclerViewAdapter之中已經預定義了點選回撥的介面,我們依然可以通過EventBus這種廣播框架便捷的實現我們的效果,不贅述。
以上便是實現一個典型的複雜佈局的全部思路過程,原始碼在最後放出,基於此,下面挨個突破各種不規則的複雜佈局。

六、 新增空佈局

對於空佈局,我們希望在使用的時候只需要一行程式碼setEmtyView(view),就行了,adapter無資料時自動呼叫空佈局,看下實現思路:
首先在SectionedRecyclerViewAdapter中新增兩個私有變數:

private View emptyView;
    private boolean emptyViewVisible;

    public void setEmptyView(View emptyView) {
        this.emptyView = emptyView;
    }

一個是空佈局View,一個是記錄空佈局是否顯示,然後定義一個方法checkEmpty(),當資料集合發生改變時,檢查是否是空佈局:

private void checkEmpty() {
        if (emptyView != null) {
            if (hasHeader()) {
                emptyViewVisible = getItemCount() == 2;
            } else {
                emptyViewVisible = getItemCount() == 1;
            }
            emptyView.setVisibility(emptyViewVisible ? View.VISIBLE : View.GONE);
        }
    }

最後再修改onCreateViewHolder()和onBindViewHolder()方法,新增空佈局的情況即可,使用的時候,只需要adapter.setEmptyView(view)一行程式碼。

七、 逐個分析以上電商app出現的頁面佈局,提供具體的實現思路

先看淘寶的首頁,也就是上面圖1,整個滾動檢視,頂部是一個廣告輪播,接著是一個分類的網格檢視,再接著是類似公告的廣告顯示區域,下面是一條分割線,再往下又是一個網格檢視,廣告輪播我們可以當做整個列表的頂部Header,廣告公告的檢視當做每一個分組的footer,不需要顯示的做隱藏。當然分類區域每行的列數和分割線下面的推薦商品區域的每行列數有差異的,這個在SpanSizeLookup這個繼承類中去控制即可。我們又看到,不僅列數不一致,連樣式都變了!實際上,有兩種方案可以控制樣式的變動,一種是將所有的樣式都寫好到一個佈局裡面,控制其顯示與隱藏(可以使用ViewStub做隱藏與顯示),考慮到效能問題,不大推薦這種方法,另外一種是為不同的分組加一個型別判斷,例如section=0的分組是分類的分組型別,我們定義一個SECTION_TYPE_0的常量來標識它,section=1的分組是商品推薦分組型別,我們再定義一個常量SECTION_TYPE_1的常量來標識它,以此類推,然後修改我們寫好的SectionedRecyclerViewAdapter這個類,新增泛型型別,然後分別在複寫方法onCreateViewHolder、onBindViewHolder、和getItemViewType裡作區分,最後在我們繼承SpanSizeLookup的類中,按照定義好的常量去做區分即可,思路有了,實現起來並不複雜。實際上,甚至連分割線以上的部分,即:輪播、分類、廣告公告都可以成為列表的Header,只不過這樣的話,分類需要我們單獨去完成佈局的構建,單省去了分組佈局多型別的步驟。
圖2、3、4、5和我們的demo一樣,按思路實現即可。
接著看京東的分類,這是一個非典型的分類佈局,不過依然可以找到規則,思路是,將分組的標題、輸入價格的兩個控制元件、以及下方的分割線合併到一起,作為每個分組的Header便可輕鬆解決,在需要的位置控制相關控制元件的隱藏與顯示即可。畫出來是這樣的:
這裡寫圖片描述

八、 小結:加點題外話吧,實際上,技術的積累沒有什麼捷徑可言,點點滴滴都是自己一步一個腳印走過來的,在學習新的東西的時候,我們除了要抱著務實本分的基本原則外,我覺得還要有敢於鑽研、不怕爭辯的的態度。最後,路漫漫其修遠兮,希望各位都能在後半年有長足的進步,完善自己的一套知識體系。

相關推薦

步步實現完整複雜列表佈局

本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出 引子:我們在工作中遇到最多的檢視場景恐怕就是各種樣式的列表了,這也是由手機螢幕有限的尺寸決定的,隨著需求的日益豐滿,我們會發現列表的樣式也隨之做著各種各樣的變更:樣式越來越多了,佈局越來越複雜

Android開發丶步步實現okhttp帶進度的列表下載檔案功能

大家好,我又回來了! 標題好像又起的不知所云,但是貌似也想不起更好的標題,話不多少,先來張效果圖 根據上圖就很明顯標題的含義了,每個列表標籤都有一個下載的按鈕,點選以下載對應的檔案,如果已下載則顯示“已下載”,反之顯示“點選下載”。 首先我們使用okhttp框架下載

IIC詳解,包括原理、過程,最後步步實現IIC

IIC詳解 1、I2C匯流排具有兩根雙向訊號線,一根是資料線SDA,另一根是時鐘線SCL   2、IIC總線上可以掛很多裝置:多個主裝置,多個從裝置(外圍 裝置)。上圖中主裝置是兩個微控制器,剩下的都是從裝置。  3、多主機會產生匯流排裁決問題。當多個主機同時想佔用匯

步步搭建一個完整的前端專案(基於vue、element-ui、webpack)

準備工作 需要先安裝node環境,官方地址:https://nodejs.org 開始搭建 Windows下不要使用git自帶的mintty執行命令,切換選項時會失效的。覺得cmd難看的話用PowerShell會好一些,只是好一些,呵呵。 安裝vue npm instal

步步輕鬆學樸素貝葉斯模型實現篇2

導讀:樸素貝葉斯模型是機器學習常用的模型演算法之一,其在文字分類方面簡單易行,且取得不錯的分類效果。所以很受歡迎,對於樸素貝葉斯的學習,本文首先介紹理論知識即樸素貝葉斯相關概念和公式推導,為了加深理解,採用一個維基百科上面性別分類例子進行形式化描述。然後通過程式設計實現樸素貝葉斯分類演算法,並在遮蔽社

演算法-步步如何用c語言實現堆排序(非遞迴)

看了左神的堆排序,覺得思路很清晰,比常見的遞迴的堆排序要更容易理解,所以自己整理了一下筆記,帶大家一步步實現堆排序演算法 首先介紹什麼是大根堆:每一個子樹的最大值都是子樹的頭結點,即根結點是所有結點的最大值 堆排序是基於陣列和二叉樹思想實現的(二叉樹是腦補結構,實際是陣列) 堆排序過程 1、陣列建

機器學習:步步理解反向傳播方法

神經網絡 方法 數學 https 以及 看到了 兩個 簡單的 down http://www.360doc.com/content/17/0209/17/10724725_627833018.shtml 數學完全看不懂 看到了這篇通過示例給出反向傳播的博文A Step by

步步創建自己的數字貨幣(代幣)進行ICO

允許 總量 ted exe init allow transfer ner 定義 本文從技術角度詳細介紹如何基於以太坊ERC20創建代幣的流程. 寫在前面 本文所講的代幣是使用以太坊智能合約創建,閱讀本文前,你應該對以太坊、智能合約有所了解,如果你還不了解,建議你先看以太坊

步步開發、部署第一個去中心化應用(Dapp) - 寵物商店

區塊鏈今天我們來編寫一個完整的去中心化(區塊鏈)應用(Dapps), 本文可以和編寫智能合約結合起來看。 寫在前面 閱讀本文前,你應該對以太坊、智能合約有所了解,如果你還不了解,建議你先看以太坊是什麽除此之外,你最好還了解一些HTML及JavaScript知識。 本文通過實例教大家來開發去中心化應用,應用效果

科普貼 | 以太坊代幣錢包MyEtherWallet使用教程,步步玩轉MEW

按鈕 wid isp 查詢 到你 pan fail VC oam MyEtherWallet 是一個以太坊的網頁錢包,使用非常簡單,打開網頁就可以使用,源代碼開源,不會在服務器上存儲用戶的錢包信息如私鑰和密碼。支持 Ledger Wallet、TREZOR 等硬件錢包

步步如何打造一個網站克隆工具仿站

obj cell ins 地址 line load mail als () 前兩天朋友叫我模仿一個網站,剛剛開始,我一個頁面一個頁面查看源碼並復制和保存,花了我很多時間,一個字“累”,為了減輕工作量,我寫了個網站“克隆工具”,一鍵克隆,比起人工操作, 效率提高了200%以上

【Android】從無到有:手把手步步使用最簡單的Fragment(三)

轉載請註明出處,原文連結:https://blog.csdn.net/u013642500/article/details/80585416 【本文適用讀者】         用程式碼建立並使用了 Fragment,新增 Fragment 之

【Android】從無到有:手把手步步使用最簡單的Fragment(二)

轉載請註明出處,原文連結:https://blog.csdn.net/u013642500/article/details/80579389 【本文適用讀者】         targetSdkVersion 版本大於等於 21,即 app 即將有可能

【Android】從無到有:手把手步步使用最簡單的 Fragment(

轉載請註明出處,原文連結:https://blog.csdn.net/u013642500/article/details/80515227 【本文適用讀者】         知道 Fragment 是什麼,不知

【Android】從無到有:手把手步步構建並使用RecyclerView

轉載請註明出處,原文連結:https://blog.csdn.net/u013642500/article/details/80480906 【AS版本】 【新增依賴】 1、開啟 Project Structural。(可點選圖示 ,也可以在File選單中開啟,也可以按 Ct

20181117——步步開發、部署第一個去中心化應用(Dapp) - 寵物商店

DApp是Decentralized Application 分散式應用 npm Node Package Manager. 包管理工具 用來下載安裝升級解除安裝安裝包 完了沒發出來,結果快取都沒了。 用MetaMask測試私有網路 從Ganache建立的賬戶中選擇一個匯入

步步使用rem適配不同螢幕的移動裝置

本文轉載自:https://www.cnblogs.com/dannyxie/p/6640903.html 感謝分享 1.先說說幾個前端常用的幾個單位的概論: 1、px (pixel,畫素):是一個虛擬長度單位,是計算機系統的數字化影象長度單位,如果px要換算成物理長度,需要指定精度

實現阿里巴巴的Sophix熱修復(

1.0 整合準備 gradle遠端倉庫依賴, 開啟專案找到app的build.gradle檔案,新增如下配置: 新增maven倉庫地址: repositories { maven { url "http://maven.ali

步步學會browserify

本文來自網易雲社群作者:孫聖翔注意文章需要邊看邊練習,不然你可能忘得速度比看的還快。Browserifybrowserify的官網是http://browserify.org/,他的用途是將前端用到的眾多資源(css,img,js,...) 打包成一個js檔案的技術。比如在h

步步輕鬆學關聯規則Apriori演算法

摘要:先驗演算法(Apriori Algorithm)是關聯規則學習的經典演算法之一,常常應用在商業等諸多領域。本文首先介紹什麼是Apriori演算法,與其相關的基本術語,之後對演算法原理進行多方面剖析,其中包括思路、原理、優缺點、流程步驟和應用場景。接著再通過一個實際案例進行語言描述性逐步剖析。