Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 【Android】讓HeaderView也參與回收復用機制,自我感覺是優雅的為 RecyclerView 添加 HeaderView (FooterView)的解決方案

【Android】讓HeaderView也參與回收復用機制,自我感覺是優雅的為 RecyclerView 添加 HeaderView (FooterView)的解決方案

編輯:關於Android編程

本文站在巨人的肩膀上 自我感覺又進了一步而成。

基於翔神的大作基礎之上寫的一個為RecyclerView添加HeaderView FooterView 的另一種解決方案,

上次翔神發表這篇文章時,我就提了個問題:說headerView和FooterView都是強引用在Adapter中的,這樣即使他所屬的ViewHolder被回收復用,但是View本身的實例還是在被強引用,內存空間也無法釋放的。 這樣做雖然速度沒任何問題,(甚至還有小小提升,但是HeaderView過大內存空間就會吃緊了吧) 所以我想了好久 改寫了一下,換了種思路,給RecyclerView提供數據和布局,UI的創建 和 數據的綁定分開來做,都交由Adapter維護。

牆裂建議大家先閱讀翔神文章後 再立刻閱讀此文,威力翻倍。這樣對本文使用到的一些吊炸天的東西就不會陌生了,例如通用的CommonAdapter和ViewHolder。

敲黑板,如果只是伸手黨,建議直接看 【2 使用方法】,並直接到文末下載鏈接裡的工程,拷貝recyclerview包下的幾個文件即可使用。

工程裡已經參考解決,HeaderView適配GridLayoutManager 和StaggeredGridLayoutManager。

========================================================================

【1 引言】

眾所周知,RecyclerView已經是主流,ListView已經成為過去式,而兩者之間有些許的不同,其中比較重要的一點就是ListView自帶addHeaderView,addFooterView方法,而RecyclerView並沒有提供。So,我們開發者要自己想辦法實現這個功能。

市面上大多為RecyclerView添加HeaderView的方案,都是在使用RecyclerView的類中(Activity Fragment)裡構建一個View,並綁定好數據,然後通過XXXAdapter提供的addHeaderView方法,將這個View set進Adapter裡。

Adapter內部使用ArrayList、或者翔神使用的是SparseArray存儲這個View,並為HeaderView FooterView分配不同的itemViewType,然後Adapter在onCreateViewHolder和onBindViewHolder方法裡,根據ViewType的不同來判斷這是HeaderView 還是普通item。

這種方法目前為止我只發現一個弊端(也是本文改進的地方),就是這個HeaderView由於在Adapter裡是被ArrayList、SparseArray強引用的,就算其所屬的RecyclerView.ViewHolder被回收服用了,但是這個View會因為被ArrayList等強引用著,依然停留在內存中。所以該HeaderView並沒有被回收,只是被復用了。而普通的item都只有數據和layoutId被保存在Adapter中,並沒有View的實例。

一般情況下 這並沒有任何問題,因為普通項目的HeaderView也不大,但是若HeaderView過於龐大,(就像我司的項目,動辄HeaderView就三個屏幕長度,三屏之後才是普通的item),在這個頁面已經往下滑了很多距離,浏覽了很多內容,HeaderView早已不可見,此時按照RecyclerView的思路,這個龐大的HeaderView所屬的VIewHolder應該被系統回收復用,以騰出空間,ok,那麼RecyclerView做了它該做的事,回收了這個HeaderView寄身的VIewHolder給其他類型的Item使用了,可惜上文提到,此時HeaderView被強引用住,被回收復用的只是其所屬的那個ViewHolder,這個龐大的VIew所占的內存空間依然沒有被釋放。

其實我們仔細想一想,RecyclerView Adapter裡是不保存View對象的,它保存的只是數據和layout,而我們也應該遵循此原則 為其添加HeaderView(FooterView)。

(題外話,和ListView相比,RecyclerView更是進一步的 將 UI的創建 和數據的綁定 分成了兩步,(oncreateViewHolder,onBindViewHolder))

敲黑板,本文就參考翔神的裝飾者模式,為RecyclerView 添加 HeaderView(FooterView),並且將HeaderView的UI創建,和數據綁定強制分開,令HeaderView實例在Adapter中不再被強引用,讓HeaderView和普通的ItemView沒有兩樣~。

先上預覽動圖:

\

 

========================================================================

【2 使用方法】

 

//HeaderView使用方法小窺: 以下為Rv添加兩個HeaderView
mHeaderAdapter = new HeaderRecyclerAndFooterWrapperAdapter(mAdapter) {
    @Override
    protected void onBindHeaderHolder(ViewHolder holder, int headerPos, int layoutId, Object o) {
        switch (layoutId) {
            case R.layout.item_header_1:
                TestHeader1 header1 = (TestHeader1) o;
                holder.setText(R.id.tv, header1.getText());
                break;
            case R.layout.item_header_2:
                TestHeader2 header2 = (TestHeader2) o;
                holder.setText(R.id.tv1, header2.getTxt1());
                holder.setText(R.id.tv2, header2.getTxt2());
                break;
            default:
                break;
        }
    }
};
mHeaderAdapter.addHeaderView(R.layout.item_header_1,new TestHeader1("第一個HeaderView"));
mHeaderAdapter.addHeaderView(R.layout.item_header_2,new TestHeader2("第二個","HeaderView"));
mRv.setAdapter(mHeaderAdapter);
粗略這麼一看,我擦 什麼辣雞,比翔神那個真是差十萬八千裡,人家只要4行代碼就加一個HeaderView,而且還不用實現父類Adapter的方法,你這還要switch case 看起來就一坨好麻煩的樣子,走了走了。

 

客官留步留步,如果客官有這種想法,先冷靜一下,裡聽我港。

這個寫法猛地看起來是略復雜了一些,但是它強制的讓我們將UI的創建和數據的綁定分開了,我們重寫的onBindHeaderHolder()方法,就是數據的綁定過程, 試想一下,基本上每個帶HeaderView的頁面都有下拉刷新功能,如果你使用傳統方法添加HeaderView,那麼你必須要持有HeaderView的引用才能在數據刷新時改變頭部數據,而且那些煩人的set方法一樣是要寫一遍,你可能需要將 寫在Activity(Fragment)裡的 創建HeaderView時的set數據方法抽成一個函數,再調用一遍。所以工作量是一點沒減少的。

所以我們這種做法,你的工作量也是一點沒增加滴!反而還是方便滴!優雅滴!

(躲開丟過來的雞蛋)廢話不多說,用法已經看到,下面看我們是怎麼實現的。 如果伸手黨看到這裡覺得已經夠了,那麼就可以去文末直接下載源碼copy使用了,裡面使用的幾個類版權大多歸翔神所有。

========================================================================

【三,實現】

直接貼出核心代碼:

 

private static final int BASE_ITEM_TYPE_HEADER = 1000000;//headerview的viewtype基准值
//存放HeaderViews的layoudID和data,key是viewType,value 是 layoudID和data,
// 在createViewHOlder裡根據layoutId創建UI,
// 在onbindViewHOlder裡依據這個data渲染UI,同時也將layoutId回傳出去用於判斷何種Header
private SparseArrayCompat mHeaderDatas = new SparseArrayCompat();
@Override
public int getItemViewType(int position) {
    if (isHeaderViewPos(position)) {
        return mHeaderDatas.keyAt(position);
    }
    return super.getItemViewType(position - getHeaderViewCount());
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

    if (mHeaderDatas.get(viewType) != null) {//不為空,說明是headerview
        return ViewHolder.get(parent.getContext(), null, parent, mHeaderDatas.get(viewType).keyAt(0), -1);
    } 
    return mInnerAdapter.onCreateViewHolder(parent, viewType);
}

protected abstract void onBindHeaderHolder(ViewHolder holder, int headerPos, int layoutId, Object o);//多回傳一個layoutId出去,用於判斷是第幾個headerview

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    if (isHeaderViewPos(position)) {
        int layoutId = mHeaderDatas.get(getItemViewType(position)).keyAt(0);
        onBindHeaderHolder((ViewHolder) holder, position, layoutId, mHeaderDatas.get(getItemViewType(position)).get(layoutId));
        return;
    }
    //舉例子,2個header,0 1是頭,2是開始,2-2 = 0
    mInnerAdapter.onBindViewHolder(holder, position - getHeaderViewCount());
}
/**
 * 添加HeaderView
 *
 * @param layoutId headerView 的LayoutId
 * @param data     headerView 的data(可能多種不同類型的header 只能用Object了)
 */
public void addHeaderView(int layoutId, Object data) {
    //mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, v);
    SparseArrayCompat headerContainer = new SparseArrayCompat();
    headerContainer.put(layoutId, data);
    mHeaderDatas.put(mHeaderDatas.size() + BASE_ITEM_TYPE_HEADER, headerContainer);
}
首先,定義一個很大的int,作為HeaderView的viewType的基准值(我這裡是100W),

 

 

然後定義一個SparseArray ,在這個二維的SparseArray中,

外層的SparseArray的Key用來存放HeaderView的ViewType,value存放的是這個ViewType對應的HeaderView的布局layoutId 和 數據data,

即內層SparseArray的key是layoutId,value是Object類型的數據data(因為每個HeaderView的數據類型不同,所以這裡我只能想到用Obejct類型)。

 

在getItemViewType()方法中,我們先根據postion判斷是否是HeaderView,如果是,那麼返回該HeaderView的viewtype(SparseArray的key)。

在onCreateViewHolder()方法裡,根據ViewType判斷是否是HeaderView,如果是HeaderView ,那麼創建一個該HeaderView的ViewHolder(我這裡使用的是翔神的通用ViewHolder)。

在onBindViewHolder()方法中,先根據postion判斷是否是HeaderView,如果是,先從mHeaderDatas裡取出這個HeaderView的layoutId,並將這個HeaderView的數據也一並取出,回調一個 abstract 的 onBindHeaderHolder()的方法,將這些參數都傳入,交由子類去自由處理。 子類在這個方法裡 完成數據的綁定即可。

========================================================================

【四,完整代碼】

這份代碼FooterView並沒有用此方法實現,是“強引用VIew方法實現的”。

理由:

1 因為FooterView往往是一個LoadMore相關的提示控件,內存占用很有限。

2 LoadMore相關提示的控件 是需要強引用在Fragment Activity 或者相關類中,即使我在Adapter類裡將其引用釋放,這個View在內存的空間依然是無法被釋放的。

3 兩種實現方法都放上來,大家可以根據本文描述的方法,自行嘗試將FooterView也改寫,可以和我討論,稍後我也會附加上我修改的版本。

 

package com.example.headerrv.recyclerview;

import android.support.v4.util.SparseArrayCompat;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.view.View;
import android.view.ViewGroup;

/**
 * 介紹:一個給RecyclerView添加HeaderView FooterView的裝飾Adapter類
 * 重點哦~ RecyclerView的HeaderView將可以被系統回收,不像老版的HeaderView是一個強引用在內存裡
 * 作者:zhangxutong
 * 郵箱:[email protected]
 * 時間: 2016/8/2.
 */
public abstract class HeaderRecyclerAndFooterWrapperAdapter extends RecyclerView.Adapter {
    private static final int BASE_ITEM_TYPE_HEADER = 1000000;//headerview的viewtype基准值
    private static final int BASE_ITEM_TYPE_FOOTER = 2000000;//footerView的ViewType基准值

    //存放HeaderViews的layoudID和data,key是viewType,value 是 layoudID和data,
    // 在createViewHOlder裡根據layoutId創建UI,
    // 在onbindViewHOlder裡依據這個data渲染UI,同時也將layoutId回傳出去用於判斷何種Header
    private SparseArrayCompat mHeaderDatas = new SparseArrayCompat();
    private SparseArrayCompat mFooterViews = new SparseArrayCompat<>();//存放FooterViews,key是viewType

    protected RecyclerView.Adapter mInnerAdapter;//內部的的普通Adapter

    public HeaderRecyclerAndFooterWrapperAdapter(RecyclerView.Adapter mInnerAdapter) {
        this.mInnerAdapter = mInnerAdapter;
    }

    public int getHeaderViewCount() {
        return mHeaderDatas.size();
    }

    public int getFooterViewCount() {
        return mFooterViews.size();
    }

    private int getInnerItemCount() {
        return mInnerAdapter != null ? mInnerAdapter.getItemCount() : 0;
    }

    /**
     * 傳入position 判斷是否是headerview
     *
     * @param position
     * @return
     */
    public boolean isHeaderViewPos(int position) {// 舉例, 2 個頭,pos 0 1,true, 2+ false
        return getHeaderViewCount() > position;
    }

    /**
     * 傳入postion判斷是否是footerview
     *
     * @param position
     * @return
     */
    public boolean isFooterViewPos(int position) {//舉例, 2個頭,2個inner,pos 0 1 2 3 ,false,4+true
        return position >= getHeaderViewCount() + getInnerItemCount();
    }

    /**
     * 添加HeaderView
     *
     * @param layoutId headerView 的LayoutId
     * @param data     headerView 的data(可能多種不同類型的header 只能用Object了)
     */
    public void addHeaderView(int layoutId, Object data) {
        //mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, v);
        SparseArrayCompat headerContainer = new SparseArrayCompat();
        headerContainer.put(layoutId, data);
        mHeaderDatas.put(mHeaderDatas.size() + BASE_ITEM_TYPE_HEADER, headerContainer);
    }

    /**
     * 設置(更新)某個layoutId的HeaderView的數據
     *
     * @param layoutId
     * @param data
     */
    public void setHeaderView(int layoutId, Object data) {
        boolean isFinded = false;
        for (int i = 0; i < mHeaderDatas.size(); i++) {
            SparseArrayCompat sparse = mHeaderDatas.valueAt(i);
            if (layoutId == sparse.keyAt(0)) {
                sparse.setValueAt(0, data);
                isFinded = true;
            }
        }
        if (!isFinded) {//沒發現 說明是addHeaderView
            addHeaderView(layoutId, data);
        }
    }


    /**
     * 設置某個位置的HeaderView
     *
     * @param headerPos 從0開始,如果pos過大 就是addHeaderview
     * @param layoutId
     * @param data
     */
    public void setHeaderView(int headerPos, int layoutId, Object data) {
        if (mHeaderDatas.size() > headerPos) {
            SparseArrayCompat headerContainer = new SparseArrayCompat();
            headerContainer.put(layoutId, data);
            mHeaderDatas.setValueAt(headerPos, headerContainer);
        } else if (mHeaderDatas.size() == headerPos) {//調用addHeaderView
            addHeaderView(layoutId, data);
        } else {
            //
            addHeaderView(layoutId, data);
        }
    }

    /**
     * 添加FooterView
     *
     * @param v
     */
    public void addFooterView(View v) {
        mFooterViews.put(mFooterViews.size() + BASE_ITEM_TYPE_FOOTER, v);
    }

    /**
     * 清空HeaderView數據
     */
    public void clearHeaderView() {
        mHeaderDatas.clear();
    }

    public void clearFooterView() {
        mFooterViews.clear();
    }


    public void setFooterView(View v) {
        clearFooterView();
        addFooterView(v);
    }

    @Override
    public int getItemViewType(int position) {
        if (isHeaderViewPos(position)) {
            return mHeaderDatas.keyAt(position);
        } else if (isFooterViewPos(position)) {//舉例:header 2, innter 2, 0123都不是,4才是,4-2-2 = 0,ok。
            return mFooterViews.keyAt(position - getHeaderViewCount() - getInnerItemCount());
        }
        return super.getItemViewType(position - getHeaderViewCount());
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        if (mHeaderDatas.get(viewType) != null) {//不為空,說明是headerview
            //return new ViewHolder(parent.getContext(), mHeaderViews.get(viewType));
            //return createHeader(parent, mHeaderViews.indexOfKey(viewType)); 第一種方法是讓子類實現這個方法 構建ViewHolder
            return ViewHolder.get(parent.getContext(), null, parent, mHeaderDatas.get(viewType).keyAt(0), -1);
        } else if (mFooterViews.get(viewType) != null) {//不為空,說明是footerview
            return new ViewHolder(parent.getContext(), mFooterViews.get(viewType));
        }
        return mInnerAdapter.onCreateViewHolder(parent, viewType);
    }

    //protected abstract RecyclerView.ViewHolder createHeader(ViewGroup parent, int headerPos);

    protected abstract void onBindHeaderHolder(ViewHolder holder, int headerPos, int layoutId, Object o);//多回傳一個layoutId出去,用於判斷是第幾個headerview

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (isHeaderViewPos(position)) {
            int layoutId = mHeaderDatas.get(getItemViewType(position)).keyAt(0);
            onBindHeaderHolder((ViewHolder) holder, position, layoutId, mHeaderDatas.get(getItemViewType(position)).get(layoutId));
            return;
        } else if (isFooterViewPos(position)) {
            return;
        }
        //舉例子,2個header,0 1是頭,2是開始,2-2 = 0
        mInnerAdapter.onBindViewHolder(holder, position - getHeaderViewCount());
    }


    @Override
    public int getItemCount() {
        return getInnerItemCount() + getHeaderViewCount() + getFooterViewCount();
    }

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        mInnerAdapter.onAttachedToRecyclerView(recyclerView);
        //為了兼容GridLayout
        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        if (layoutManager instanceof GridLayoutManager) {
            final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
            final GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup();

            gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    int viewType = getItemViewType(position);
                    if (mHeaderDatas.get(viewType) != null) {
                        return gridLayoutManager.getSpanCount();
                    } else if (mFooterViews.get(viewType) != null) {
                        return gridLayoutManager.getSpanCount();
                    }
                    if (spanSizeLookup != null)
                        return spanSizeLookup.getSpanSize(position);
                    return 1;
                }
            });
            gridLayoutManager.setSpanCount(gridLayoutManager.getSpanCount());
        }

    }

    @Override
    public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
        mInnerAdapter.onViewAttachedToWindow(holder);
        int position = holder.getLayoutPosition();
        if (isHeaderViewPos(position) || isFooterViewPos(position)) {
            ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();

            if (lp != null
                    && lp instanceof StaggeredGridLayoutManager.LayoutParams) {

                StaggeredGridLayoutManager.LayoutParams p =
                        (StaggeredGridLayoutManager.LayoutParams) lp;

                p.setFullSpan(true);
            }
        }
    }
}
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved