Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all articles
Browse latest Browse all 5930

【Android】让HeaderView也参与回收复用机制,自我感觉是优雅的为 RecyclerView 添加 HeaderView (FooterView)的解决方案

$
0
0

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

基于翔神的大作基础之上写的一个为RecyclerView添加HeaderView FooterView 的另一种解决方案, 

翔神链接文首镇楼:http://blog.csdn.net/lmj623565791/article/details/51854533 

上次翔神发表这篇文章时,我就提了个问题:说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<SparseArrayCompat> mHeaderDatas = new SparseArrayCompat<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中,

外层的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
 * 邮箱:zhangxutong@imcoming.com
 * 时间: 2016/8/2.
 */
public abstract class HeaderRecyclerAndFooterWrapperAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    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<SparseArrayCompat> mHeaderDatas = new SparseArrayCompat<SparseArrayCompat>();
    private SparseArrayCompat<View> 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);
            }
        }
    }
}


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

源码链接:http://download.csdn.net/detail/zxt0601/9608941




作者:zxt0601 发表于2016/8/21 17:22:01 原文链接
阅读:33 评论:0 查看评论

Viewing all articles
Browse latest Browse all 5930

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>