Android 給RecyclerView新增頭部和尾部

DonKingLiang發表於2017-11-13

之前我在GitHub上開源了一個可以實現RecyclerView列表分組的通用Adapter: GroupedRecyclerViewAdapter。也在部落格上寫了一篇專門介紹它的實現和使用的文章:《Android 可分組的RecyclerViewAdapter》。有一些朋友在看了我的博文和使用我的開源庫後,會主動的給我反饋一些問題和提出一些建議。我很高興,這使我覺得我所做的這些不僅可以提升自己,同時也能幫助到別人(雖然我很少寫部落格和開源東西)。前幾天就有朋友向我反饋,說他不僅需要一個分組的列表,同時還希望能給列表新增頭部和尾部。其實GroupedRecyclerViewAdapter的每個分組都是可以設定頭部和尾部的。如果只是給一個普通的列表新增頭部和尾部,只需要用GroupedRecyclerViewAdapter實現只有一個分組的列表就可以。但是他希望的是可以實現多個分組的同時,給整個大列表也設定頭部和尾部,這樣的需求是我以前從來沒有想過的,但我還是給出了自己的建議:列表的第一個分組只要頭部,不要尾部和子項,把它當做整個大列表的頭部,尾部的實現也一樣。這樣也能實現他的需求,但就是處理邏輯複雜了一點。其實我在設計GroupedRecyclerViewAdapter的時候,為了能讓它有更好的擴充套件性和能方便實現更多的複雜佈局,所以給它的頭部、尾部和子項都支援了多種型別的ViewTtype,有興趣的朋友歡迎去看一下。

ListView新增頭部和尾部的實現原理

在我們以前使用ListView的時候,ListView為我們提供了新增頭部(addHeaderView())和尾部(addFooterView())的方法,讓我們可以很方便的給列表新增頭部和尾部,而且頭部和尾部跟我們自己的ListView Adapter完全沒有任何關係。那麼ListView是如何做到的呢。讓我們一起開啟ListView的原始碼,看一下它是如何給列表新增頭部(addHeaderView())的。(新增尾部的原理是一樣的,這裡就不單獨說了)

    public void addHeaderView(View v) {
        addHeaderView(v, null, true);
    }

    public void addHeaderView(View v, Object data, boolean isSelectable) {
        final FixedViewInfo info = new FixedViewInfo();
        info.view = v;
        info.data = data;
        info.isSelectable = isSelectable;
        mHeaderViewInfos.add(info);
        mAreAllItemsSelectable &= isSelectable;

        // 將ListView的Adapter包裝成HeaderViewListAdapter。
        if (mAdapter != null) {
            if (!(mAdapter instanceof HeaderViewListAdapter)) {
                mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
            }

            if (mDataSetObserver != null) {
                mDataSetObserver.onChanged();
            }
        }
    }複製程式碼

上面就是ListView新增頭部的方法的原始碼,其中它會判斷當前ListView的Adapter是不是HeaderViewListAdapter,如果不是,這把當前的Adapter包裝成HeaderViewListAdapter。所以HeaderViewListAdapter就是實現ListView新增頭部的關鍵,那麼我們就去看一下HeaderViewListAdapter到底是個什麼東西。

    public class HeaderViewListAdapter implements WrapperListAdapter, Filterable{
}複製程式碼

HeaderViewListAdapter實現了WrapperListAdapter介面,而WrapperListAdapter介面是ListAdapter的子介面,所以HeaderViewListAdapter就是ListAdapter的一個實現類,跟普通的ListView Adapter沒有太大區別。HeaderViewListAdapter接收一個普通的ListView Adapter和ListView頭部列表和尾部列表,並且對它們統一管理。下面分析一下它的核心程式碼。

    //構造方法:接收從外部傳進來的頭部列表資訊和尾部列表資訊,還有被包裝的普通ListAdapter。
    public HeaderViewListAdapter(ArrayList<ListView.FixedViewInfo> headerViewInfos,
                                 ArrayList<ListView.FixedViewInfo> footerViewInfos,
                                 ListAdapter adapter) {

    }

    //返回整個列表的item個數,其實就是普通Adapter的item個數加上頭尾部的個數。
    public int getCount() {
        if (mAdapter != null) {
            return getFootersCount() + getHeadersCount() + mAdapter.getCount();
        } else {
            return getFootersCount() + getHeadersCount();
        }
    }

    /返回當前列表項的ViewType。
    public int getItemViewType(int position) {
        int numHeaders = getHeadersCount();
        if (mAdapter != null && position >= numHeaders) {
            int adjPosition = position - numHeaders;
            int adapterCount = mAdapter.getCount();
            //如果當前列表項是普通的列表項,則交由mAdapter處理。
            //傳給mAdapter的position需要除掉頭尾部的處理。
            if (adjPosition < adapterCount) {
                return mAdapter.getItemViewType(adjPosition);
            }
        }
        //返回當前列表項時頭部或者尾部
        return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
    }

    public View getView(int position, View convertView, ViewGroup parent) {
        //如果當前列表項是頭部,則返回對應的頭部佈局。
        int numHeaders = getHeadersCount();
        if (position < numHeaders) {
            return mHeaderViewInfos.get(position).view;
        }

        //如果當前列表項是普通的列表項,則交由mAdapter處理。
        //傳給mAdapter的position需要除掉頭尾部的處理。
        final int adjPosition = position - numHeaders;
        int adapterCount = 0;
        if (mAdapter != null) {
            adapterCount = mAdapter.getCount();
            if (adjPosition < adapterCount) {
                return mAdapter.getView(adjPosition, convertView, parent);
            }
        }

        //如果當前列表項是尾部,則返回對應的尾部佈局。
        return mFooterViewInfos.get(adjPosition - adapterCount).view;
    }複製程式碼

這就是HeaderViewListAdapter的核心程式碼,我們可以看到,它的程式碼非常的簡單,就是在getCount()、getItemViewType()、getView()中做了一些處理。在getCount()中它返回的個數里加上了頭部和尾部的個數。在getItemViewType()、getView()中,它判斷如果當前列表項是頭部或者尾部的時候自己處理,否則就交由被包裝的普通Adapter處理。所以HeaderViewListAdapter的主要作用就是管理ListView的頭部和尾部的。

HeaderViewListAdapter的設計非常的巧妙,只需要把我們設定給ListView的Adapter包裝一下,就可以讓我們的ListView具有了新增頭部和尾部的功能,而且絲毫不會影響到我們原來的Adapter。甚至於我們根本就不知道在我們給ListView新增頭部的時候,ListView已經將我們原來的Adapter包裝成HeaderViewListAdapter,我們也無需關心他的實現邏輯。

根據HeaderViewListAdapter的設計思路,我們是不是也可以給我們的RecyclerView.Adapter實現一個包裝類,只要對我們自己的Adapter包裝一下,就可以讓我們的列表具有了新增頭部和尾部的功能呢?帶著這樣的想法,於是我就自己動手寫了一個專門用來包裝RecyclerView的Adapter的包裝類:HeaderViewAdapter。

HeaderViewAdapter的程式碼實現

HeaderViewAdapter的設計思路和實現的效果跟HeaderViewListAdapter是完全一樣的,在程式碼的實現上會有所不同,畢竟ListView的Adapter和RecyclerView的Adapter是完全不同的兩個東西。

public class HeaderViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    //被包裝的Adapter。
    private RecyclerView.Adapter mAdapter;

    //用於存放HeaderView
    private final List<FixedViewInfo> mHeaderViewInfos = new ArrayList<>();

    //用於存放FooterView
    private final List<FixedViewInfo> mFooterViewInfos = new ArrayList<>();

    //用於監聽被包裝的Adapter的資料變化的監聽器。它將被包裝的Adapter的資料變化對映成HeaderViewAdapter的變化。
    private RecyclerView.AdapterDataObserver mObserver = new RecyclerView.AdapterDataObserver() {
                //這裡是具體的程式碼實現,因為篇幅的關係,在這裡就不放出來了。
    };

    public HeaderViewAdapter(RecyclerView.Adapter adapter) {
        this.mAdapter = adapter;
        if (mAdapter != null) {
            //註冊mAdapter的資料變化監聽
            mAdapter.registerAdapterDataObserver(mObserver);
        }
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // 根據viewType查詢對應的HeaderView 或 FooterView。如果沒有找到則表示該viewType是普通的列表項。
        View view = findViewForInfos(viewType);
        if (view != null) {
            return new ViewHolder(view);
        } else {
            //交由mAdapter處理。
            return mAdapter.onCreateViewHolder(parent, viewType);
        }
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        // 如果是HeaderView 或者是 FooterView則不繫結資料。
        // 因為HeaderView和FooterView是由外部傳進來的,它們不由列表去更新。
        if (isHeader(position) || isFooter(position)) {
            return;
        }

        //將列表實際的position調整成mAdapter對應的position。
        //交由mAdapter處理。
        int adjPosition = position - getHeadersCount();
        mAdapter.onBindViewHolder(holder, adjPosition);
    }

    @Override
    public int getItemCount() {
        return mHeaderViewInfos.size() + mFooterViewInfos.size()
                + (mAdapter == null ? 0 : mAdapter.getItemCount());
    }

    @Override
    public int getItemViewType(int position) {
        //如果當前item是HeaderView,則返回HeaderView對應的itemViewType。
        if (isHeader(position)) {
            return mHeaderViewInfos.get(position).itemViewType;
        }

        //如果當前item是HeaderView,則返回HeaderView對應的itemViewType。
        if (isFooter(position)) {
            return mFooterViewInfos.get(position - mHeaderViewInfos.size() - mAdapter.getItemCount()).itemViewType;
        }

        //將列表實際的position調整成mAdapter對應的position。
        //交由mAdapter處理。
        int adjPosition = position - getHeadersCount();
        return mAdapter.getItemViewType(adjPosition);
    }

    /**
     * 判斷當前位置是否是頭部View。
     *
     * @param position 這裡的position是整個列表(包含HeaderView和FooterView)的position。
     * @return
     */
    public boolean isHeader(int position) {
        return position < getHeadersCount();
    }

    /**
     * 判斷當前位置是否是尾部View。
     *
     * @param position 這裡的position是整個列表(包含HeaderView和FooterView)的position。
     * @return
     */
    public boolean isFooter(int position) {
        return getItemCount() - position <= getFootersCount();
    }

    /**
     * 獲取HeaderView的個數
     *
     * @return
     */
    public int getHeadersCount() {
        return mHeaderViewInfos.size();
    }

    /**
     * 獲取FooterView的個數
     *
     * @return
     */
    public int getFootersCount() {
        return mFooterViewInfos.size();
    }

    /**
     * 新增HeaderView
     *
     * @param view
     */
    public void addHeaderView(View view) {
        addHeaderView(view, generateUniqueViewType());
    }

    private void addHeaderView(View view, int viewType) {
        //包裝HeaderView資料並新增到列表
        FixedViewInfo info = new FixedViewInfo();
        info.view = view;
        info.itemViewType = viewType;
        mHeaderViewInfos.add(info);
        notifyDataSetChanged();
    }

    /**
     * 新增FooterView
     *
     * @param view
     */
    public void addFooterView(View view) {
        addFooterView(view, generateUniqueViewType());
    }

    private void addFooterView(View view, int viewType) {
        // 包裝FooterView資料並新增到列表
        FixedViewInfo info = new FixedViewInfo();
        info.view = view;
        info.itemViewType = viewType;
        mFooterViewInfos.add(info);
        notifyDataSetChanged();
    }

    /**
     * 生成一個唯一的數,用於標識HeaderView或FooterView的type型別,並且保證型別不會重複。
     *
     * @return
     */
    private int generateUniqueViewType() {
        int count = getItemCount();
        while (true) {
            //生成一個隨機數。
            int viewType = (int) (Math.random() * Integer.MAX_VALUE) + 1;

            //判斷該viewType是否已使用。
            boolean isExist = false;
            for (int i = 0; i < count; i++) {
                if (viewType == getItemViewType(i)) {
                    isExist = true;
                    break;
                }
            }

            //判斷該viewType還沒被使用,則返回。否則進行下一次迴圈,重新生成隨機數。
            if (!isExist) {
                return viewType;
            }
        }
    }

    /**
     * 根據viewType查詢對應的HeaderView 或 FooterView。沒有找到則返回null。
     *
     * @param viewType 查詢的viewType
     * @return
     */
    private View findViewForInfos(int viewType) {
        for (FixedViewInfo info : mHeaderViewInfos) {
            if (info.itemViewType == viewType) {
                return info.view;
            }
        }

        for (FixedViewInfo info : mFooterViewInfos) {
            if (info.itemViewType == viewType) {
                return info.view;
            }
        }

        return null;
    }

    /**
     * 用於包裝HeaderView和FooterView的資料類
     */
    private class FixedViewInfo {
        //儲存HeaderView或FooterView
        View view;

        //儲存HeaderView或FooterView對應的viewType。
        int itemViewType;
    }

    private static class ViewHolder extends RecyclerView.ViewHolder {
        ViewHolder(View itemView) {
            super(itemView);
        }
    }
}複製程式碼

為了讓大家看程式碼方便一點,我刪除了一些不是很重要的程式碼,而且把程式碼的實現細節講解寫到了每個方法的註釋上,相信大家都能很容易的看懂。如果大家想看完整的程式碼,請移步我的GitHub

HeaderViewAdapter的使用

我已經把HeaderViewAdapter和相關的類打包成一個引用庫放到GitHub上,歡迎大家使用和star。

在ListView的設計上,對HeaderViewListAdapter的所有操作的是由ListView自己完成的。但是我們無法把對HeaderViewAdapter的操作交由RecyclerView來處理,所以需要自己對HeaderViewAdapter進行操作(包裝、新增頭部和尾部等),其實這也很簡單,幾句程式碼的事情而已。而且它可以適用於任何的RecyclerView,沒有任何使用上的限制。

    //需要包裝的adapter
    LinearAdapter adapter = new LinearAdapter(this);

    //對adapter進行包裝。
    HeaderViewAdapter headerViewAdapter = new HeaderViewAdapter(adapter);

    //新增HeaderView和FooterView
    headerViewAdapter.addHeaderView(headerView);
    headerViewAdapter.addFooterView(footerView);

    //設定Adapter
    recyclerView.setAdapter(headerViewAdapter);複製程式碼

無論我們的RecyclerView使用什麼LayoutManager,HeaderViewAdapter都需要保證列表的頭部和尾部能佔滿一行,否則佈局就會很難看。使用LinearLayoutManager的時候不需要做特殊的處理,HeaderViewAdapter也已經幫我們處理了StaggeredGridLayoutManager的情況。至於GridLayoutManager的情況,我在HeaderViewAdapter的庫裡提供了一個HeaderViewGridLayoutManager的子類。所以大家在使用GridLayoutManager的時候,應該使用HeaderViewGridLayoutManager。

    recyclerView.setLayoutManager(new HeaderViewGridLayoutManager(this, 2, headerViewAdapter));複製程式碼

為了讓我們的RecyclerView新增頭部和尾部的時候,更接近於ListView的體驗。所以我在庫裡提供了一個RecyclerView子類:HeaderRecyclerView。HeaderRecyclerView封裝了對HeaderViewAdapter的所以操作,這使我們只需要操作HeaderRecyclerView,而無需直接跟HeaderViewAdapter打交道,這使得我們使用HeaderRecyclerView的時候就如同以前使用ListView一樣。

    HeaderRecyclerView rvList = (HeaderRecyclerView) findViewById(R.id.rv_list);
    //這是普通的adapter
    GridAdapter adapter = new GridAdapter(this);
    rvList.setLayoutManager(new GridLayoutManager(this, 2));
    //直接設定普通的adapter,不需要直接進行包裝。
    rvList.setAdapter(adapter);

    //新增HeaderView和FooterView。直接操作HeaderRecyclerView。
    rvList.addHeaderView(headerView);
    rvList.addFooterView(footerView);複製程式碼

效果圖:

LinearList.gif
LinearList.gif

GridList.gif
GridList.gif

傳送門:github.com/donkinglian…

相關文章