真实项目运用-RecyclerView封装

作者: 程序员Anthony | 来源:发表于2016-09-14 14:17 被阅读14881次

    原文链接:从零开始搭建android框架系列
    项目地址:MVPCommon

    前言

    很久没有发表从零开始搭建android框架系列这个系列的文章了 。由于最近工作确实有点忙碌,也在脚踏实地的花时间研究android方面自己很多不懂的东西。但是写博客确实是一个坚持不懈和自我提高的过程,也希望在保持文章更新的同时能够保持文章的质量 。之前翻译了一些文章,有兴趣的小伙伴可以去看一下。今天这篇文章来谈一谈RecyclerView的封装,对RecyclerView的一些使用点进行总结,以及如何将RecyclerView的adapter进一步简化。平时开发使用的RecyclerView Adapter是来自鸿洋大神的为RecyclerView打造通用Adapter 让RecyclerView更加好用以及对应的github项目baseAdapter github.但是有个问题是他这篇文章写的时间比较早,项目一直在维护,所以本篇文章也算是对整个项目的思路的再梳理。
    刚好解决了昨天在鸿洋博客下看到的这个小问题。哈哈。希望对大家有帮助。

    项目结构

    首先看看我的项目结构,项目分为common 和module模块,这里对之前整个项目的框架进行了改造,没有了之前的library,取而代之的是将所有公用组件放在了common包中,这是每个项目都可以copy使用的部分。在module包中就是具体每个项目的每个模块。比如这个示例项目中,包含


    整体结构

    recyclerView组件作为每个项目中都可以使用的组件,这里放在common-widgets-recyclerview包下。

    这里可以看到的recyclerView组件这里添加了adapter,base,divider,section,utils,wrapper包。下面来进行深入的讲解以及怎样在项目开发中进行使用吧。

    recyclerView公用组件

    RecyclerView基础

    RecyclerView is a more advanced and flexible version of ListView. This widget is a container for large sets of views that can be recycled and scrolled very efficiently. Use the RecyclerView widget when you have lists with elements that change dynamically.
    RecyclerView 是Android L版本中新添加的一个用来取代ListView的SDK,它的灵活性与可替代性比listview更好。RecyclerView与ListView原理是类似的:都是仅仅维护少量的View并且可以展示大量的数据集。在RecyclerView标准化了ViewHolder类似于ListView中convertView用来做视图缓存。

    请直接参考 Android RecyclerView 使用完全解析 体验艺术般的控件

    ViewHolder

    ViewHolder是google在优化ListView性能的技巧上就提到的,虽然google并没有强制使用,但事实上它已经成为ListView的编写规范。在RecyclerView上,ViewHolder的使用成为了一种强制手段了。接下来对封装的ViewHolder进行分析:
    首先来看看ViewHolder的用法,这是一个简单的获取String数组并展现到TextView上。通过数组的大小来创建item的数量。

    public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
        public String[] datas = null;
        public MyAdapter(String[] datas) {
            this.datas = datas;
        }
        //创建新View,被LayoutManager所调用
        @Override
        public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
            View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item,viewGroup,false);
            ViewHolder vh = new ViewHolder(view);
            return vh;
        }
        //将数据与界面进行绑定的操作
        @Override
        public void onBindViewHolder(ViewHolder viewHolder, int position) {
            viewHolder.mTextView.setText(datas[position]);
        }
        //获取数据的数量
        @Override
        public int getItemCount() {
            return datas.length;
        }
        //自定义的ViewHolder,持有每个Item的的所有界面元素
        public static class ViewHolder extends RecyclerView.ViewHolder {
            public TextView mTextView;
            public ViewHolder(View view){
            super(view);
                mTextView = (TextView) view.findViewById(R.id.text);
            }
        }
    }
    

    当然这里只是简单的一个TextView,但是当数据多起来之后,很多TextView,ImageView,以及代码段

    View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item,viewGroup,false); 
    ViewHolder vh = new ViewHolder(view);
    

    都可以进行稍微修改。现在ViewHolder修改如下:

    
    
    public class ViewHolder extends RecyclerView.ViewHolder {
        private SparseArray<View> mViews;
        private View mConvertView;
        private Context mContext;
    
        ImageLoaderUtil imageLoaderUtil;
    
    
        public ViewHolder(Context context, View itemView) {
            super(itemView);
            mContext = context;
            mConvertView = itemView;
            mViews = new SparseArray<>();
            imageLoaderUtil = new ImageLoaderUtil();
        }
    
    
        public static ViewHolder createViewHolder(Context context, View itemView) {
            return new ViewHolder(context, itemView);
        }
    
        public static ViewHolder createViewHolder(Context context,
                                                  ViewGroup parent, int layoutId) {
            View itemView = LayoutInflater.from(context).inflate(layoutId, parent,
                    false);
            return new ViewHolder(context, itemView);
        }
    
    
        /**
         * 通过viewId获取控件
         *
         * @param viewId
         * @return
         */
        public <T extends View> T getView(int viewId) {
            View view = mViews.get(viewId);
            if (view == null) {
                view = mConvertView.findViewById(viewId);
                mViews.put(viewId, view);
            }
            return (T) view;
        }
    
        public View getConvertView() {
            return mConvertView;
        }
    
    
        /****以下为辅助方法*****/
    
        /**
         * 设置TextView的值
         *
         * @param viewId
         * @param text
         * @return
         */
        public ViewHolder setText(int viewId, String text) {
            TextView tv = getView(viewId);
            tv.setText(text);
            return this;
        }
    
        public ViewHolder setImageResource(int viewId, int resId) {
            ImageView view = getView(viewId);
            view.setImageResource(resId);
            return this;
        }
    
        public ViewHolder setImageUrl(int viewId, String url) {
            ImageView view = getView(viewId);
            ImageLoader.Builder builder = new ImageLoader.Builder();
            ImageLoader img = builder.url(url)
                    .imgView(view).strategy(ImageLoaderUtil.LOAD_STRATEGY_ONLY_WIFI).build();
            imageLoaderUtil.loadImage(mContext, img);
            return this;
        }
    
        public ViewHolder setImageBitmap(int viewId, Bitmap bitmap) {
            ImageView view = getView(viewId);
            view.setImageBitmap(bitmap);
            return this;
        }
    
        public ViewHolder setImageDrawable(int viewId, Drawable drawable) {
            ImageView view = getView(viewId);
            view.setImageDrawable(drawable);
            return this;
        }
    ......
    ......
    
        /**
         * 关于事件的
         */
        public ViewHolder setOnClickListener(int viewId,
                                             View.OnClickListener listener) {
            View view = getView(viewId);
            view.setOnClickListener(listener);
            return this;
        }
    
    ......
    
    
    }
    
    

    这里需要关注的是getView方法,直接返回当前view的类型。
    所以我们可以在使用viewholder的时候

    holder.setText(R.id.text_view, "text");
    

    就完成了textView的setText操作。而没有进行类型转换。当然这里省去了findViewById的步骤的同时,使用private SparseArray<View> mViews 进行所有view的保存,也就是在牺牲一定内存性能的情况下,确保了代码的整洁性。还需要关注上面的

        public ViewHolder setImageUrl(int viewId, String url) {
            ImageView view = getView(viewId);
            ImageLoader.Builder builder = new ImageLoader.Builder();
            ImageLoader img = builder.url(url)
                    .imgView(view).strategy(ImageLoaderUtil.LOAD_STRATEGY_ONLY_WIFI).build();
            imageLoaderUtil.loadImage(mContext, img);
            return this;
        }
    

    这里只需要传入ImageView的id,和url就可以解析网络图片并完成加载。使用的是Glide进行图片加载。具体可以参考之前的文章网络图片加载的封装.这样封装还有一个好处是当你遇到奇葩的服务器返回字段,比如说我们前段时间遇到的每次返回的textView的text都是有问题的,需要我们自己处理,比如说时间需要截取并返回刚刚,几小时前,我们都可以在ViewHolder进行统一的处理,而不需要在每个数据获取的时候进行处理。

    多Item布局实现

    这也是我们使用RecyclerView和ListView中过程中经常遇到的问题。看看网易新闻的列表样式,顶部大图,标题+三张图片,标题+左侧图片,视频样式,广告样式....... 这种情况我们怎么便捷快速处理呢?

    看看通常处理itemView的type类型不同的方法:

    public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
        class ViewHolder0 extends RecyclerView.ViewHolder {
            ...
        }
    
        class ViewHolder2 extends RecyclerView.ViewHolder {
            ...
        }
    
        @Override
        public int getItemViewType(int position) {
            // Just as an example, return 0 or 2 depending on position
            // Note that unlike in ListView adapters, types don't have to be contiguous
            return position % 2 * 2;
        }
    
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
             switch (viewType) {
                 case 0: return new ViewHolder0(...);
                 case 2: return new ViewHolder2(...);
                 ...
             }
        }
    }
    

    这里对方法getItemViewType()进行重写, 并且在 onCreateViewHolder()针对不同的viewType进行不同的ViewHolder创建。
    同时,这样使用不同的type 来处理不同的位置的数据,也能解决ListView中经常遇到的一个问题,那就是header和footer 的view的添加。我们只需要针对首尾位置进行itemViewType 的处理并且返回header和footer的view就行了。

    这里也对这种情况进行了处理的封装:
    看看实际项目中的效果,一个adapter就完成了所有的不同的item类型操作。
    整个类继承自MultiItemTypeAdapter<T>

    /**
     * Created by Anthony
     * <p/>
     * 
     */
    public class NewsMultiAdapter extends MultiItemTypeAdapter<NewsItem> {
    
        public NewsMultiAdapter(Context context) {
            super(context);
            addItemViewDelegate(new TodayTopicDelegate());// docType = 5,  今日头条样式
            addItemViewDelegate(new JustTitleDelegate());//   docType = 4,  纯文字样式
            addItemViewDelegate(new OtherTypeDelegate());//  docType = 0/1,  默认左侧图片 + 右侧标题,描述字段样式
    //        addItemViewDelegate(new BigPicTypeDelegate()); //docType = 2,  顶部标题 + 一张大横图样式
    
        }
    
    
        /*
         docType = 5,  今日头条样式
            docType = 0,  默认左侧图片 + 右侧标题,描述字段样式
            docType = 1,  顶部标题 + 三张图片样式
            docType = 2,  顶部标题 + 一张大横图样式
            docType = 3,  默认样式 + 图集图标 -->点击进入图集细览详情
                    docType = 4,  纯文字样式
            docType = 5,  今日头条样式
                    docType = 6,  专题样式
            */
        public class TodayTopicDelegate implements ItemViewDelegate<NewsItem> {
            @Override
            public int getItemViewLayoutId() {
                return R.layout.gz_tab1_item_today_topic;
            }
    
            @Override
            public boolean isForViewType(NewsItem item, int position) {
                return item.getType() == 5;
            }
    
            @Override
            public void convert(ViewHolder holder, NewsItem item, int position) {
                holder.setText(R.id.tv_title_center, item.getTitle());
                holder.setText(R.id.tv_news_date, item.getTime());
            }
        }
    
    
        public class OtherTypeDelegate implements ItemViewDelegate<NewsItem> {
    
            @Override
            public int getItemViewLayoutId() {
                return R.layout.gz_tab1_item_normal_news;
            }
    
            @Override
            public boolean isForViewType(NewsItem item, int position) {
                return item.getType() == 0;
            }
    
            @Override
            public void convert(ViewHolder holder, NewsItem item, int position) {
                holder.setText(R.id.tv_title_center, item.getTitle());
                holder.setText(R.id.tv_news_source, item.getSummary());
                holder.setText(R.id.tv_news_date, item.getTime());
    
                if (item.getImgs() != null) {
                    String url = item.getImgs().get(0);
                    holder.setImageUrlInGZ(R.id.img_news_image, url);
    
                }
            }
        }
    
        public class JustTitleDelegate implements ItemViewDelegate<NewsItem> {
    
            @Override
            public int getItemViewLayoutId() {
                return R.layout.gz_tab1_item_just_title;
            }
    
            @Override
            public boolean isForViewType(NewsItem item, int position) {
                return item.getType() == 4;
            }
    
            @Override
            public void convert(ViewHolder holder, NewsItem item, int position) {
                holder.setText(R.id.tv_title_center, item.getTitle());
                holder.setText(R.id.tv_news_date, item.getDate());
            }
        }
    

    来看看MultiItemTypeAdapter<T>

    /**
     * Created by zhy on 16/4/9.
     */
    public class MultiItemTypeAdapter<T> extends RecyclerView.Adapter<ViewHolder>
    {
        protected Context mContext;
        protected List<T> mDatas;
    
        protected ItemViewDelegateManager mItemViewDelegateManager;
        protected OnItemClickListener<T> mOnItemClickListener;
        public int offset = 0;
    
        public MultiItemTypeAdapter(Context context, List<T> datas)
        {
            mContext = context;
            mDatas = datas;
            mItemViewDelegateManager = new ItemViewDelegateManager();
        }
    
        public MultiItemTypeAdapter(Context context)
        {
            this(context, new ArrayList<T>());
        }
    
        @Override
        public int getItemViewType(int position)
        {
            if (!useItemViewDelegateManager()) return super.getItemViewType(position);
            return mItemViewDelegateManager.getItemViewType(mDatas.get(position), position);
        }
    
    
        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
        {
            int layoutId = mItemViewDelegateManager.getItemViewLayoutId(viewType);
            ViewHolder holder = ViewHolder.createViewHolder(mContext, parent, layoutId);
            setListener(parent, holder, viewType);
            return holder;
        }
    
        public void convert(ViewHolder holder, T t)
        {
            mItemViewDelegateManager.convert(holder, t, holder.getAdapterPosition());
        }
    
        protected boolean isEnabled(int viewType)
        {
            return true;
        }
    
    
        protected void setListener(final ViewGroup parent, final ViewHolder viewHolder, int viewType)
        {
            if (!isEnabled(viewType)) return;
            viewHolder.getConvertView().setOnClickListener(new View.OnClickListener()
            {
                @Override
                public void onClick(View v)
                {
                    if (mOnItemClickListener != null)
                    {
                        int position = viewHolder.getAdapterPosition();
                        mOnItemClickListener.onItemClick(v, viewHolder, mDatas.get(position - offset), position);
                    }
                }
            });
    
            viewHolder.getConvertView().setOnLongClickListener(new View.OnLongClickListener()
            {
                @Override
                public boolean onLongClick(View v)
                {
                    if (mOnItemClickListener != null)
                    {
                        int position = viewHolder.getAdapterPosition();
                        return mOnItemClickListener.onItemLongClick(v, viewHolder, mDatas.get(position - offset), position);
                    }
                    return false;
                }
            });
        }
    
        @Override
        public void onBindViewHolder(ViewHolder holder, int position)
        {
            convert(holder, mDatas.get(position));
        }
    
        @Override
        public int getItemCount()
        {
            int itemCount = mDatas.size();
            return itemCount;
        }
    
    
        public List<T> getDatas()
        {
            return mDatas;
        }
    
        public MultiItemTypeAdapter addItemViewDelegate(ItemViewDelegate<T> itemViewDelegate)
        {
            mItemViewDelegateManager.addDelegate(itemViewDelegate);
            return this;
        }
    
        public MultiItemTypeAdapter addItemViewDelegate(int viewType, ItemViewDelegate<T> itemViewDelegate)
        {
            mItemViewDelegateManager.addDelegate(viewType, itemViewDelegate);
            return this;
        }
    
        protected boolean useItemViewDelegateManager()
        {
            return mItemViewDelegateManager.getItemViewDelegateCount() > 0;
        }
    
        public interface OnItemClickListener<T>
        {
            void onItemClick(View view, RecyclerView.ViewHolder holder, T o, int position);
    
            boolean onItemLongClick(View view, RecyclerView.ViewHolder holder, T o, int position);
        }
    
        public void setOnItemClickListener(OnItemClickListener onItemClickListener)
        {
            this.mOnItemClickListener = onItemClickListener;
        }
    
        public void addDataAll(List data) {
            mDatas.addAll(data);
        }
    
        public void clearData() {
            mDatas.clear();
        }
    }
    
    

    MultiItemTypeAdapter<T>,这里主要完成了List形式添加数据,数据类型使用泛型操作,只需要在构造函数中,或者public方法 addDataAll就可以添加列表类型数据。利用ItemViewDelegateManager完成不同类型type的管理.而添加不同的type是对接口ItemViewDelegate的实现。ItemViewDelegateManager起到了一个中间管理者和代理者的作用。具体看下面的代码:

    /**
     * Created by zhy on 16/6/22.
     */
    public interface ItemViewDelegate<T>
    {
    
        int getItemViewLayoutId();
    
        boolean isForViewType(T item, int position);
    
        void convert(ViewHolder holder, T t, int position);
        
    }
    
    
    package com.app.gzgov.common.widgets.recyclerview.base;
    
    import android.support.v4.util.SparseArrayCompat;
    
    
    /**
     * Created by zhy on 16/6/22.
     */
    public class ItemViewDelegateManager<T>
    {
        SparseArrayCompat<ItemViewDelegate<T>> delegates = new SparseArrayCompat();
    
        public int getItemViewDelegateCount()
        {
            return delegates.size();
        }
    
        public ItemViewDelegateManager<T> addDelegate(ItemViewDelegate<T> delegate)
        {
            int viewType = delegates.size();
            if (delegate != null)
            {
                delegates.put(viewType, delegate);
                viewType++;
            }
            return this;
        }
    
        public ItemViewDelegateManager<T> addDelegate(int viewType, ItemViewDelegate<T> delegate)
        {
            if (delegates.get(viewType) != null)
            {
                throw new IllegalArgumentException(
                        "An ItemViewDelegate is already registered for the viewType = "
                                + viewType
                                + ". Already registered ItemViewDelegate is "
                                + delegates.get(viewType));
            }
            delegates.put(viewType, delegate);
            return this;
        }
    
        public ItemViewDelegateManager<T> removeDelegate(ItemViewDelegate<T> delegate)
        {
            if (delegate == null)
            {
                throw new NullPointerException("ItemViewDelegate is null");
            }
            int indexToRemove = delegates.indexOfValue(delegate);
    
            if (indexToRemove >= 0)
            {
                delegates.removeAt(indexToRemove);
            }
            return this;
        }
    
        public ItemViewDelegateManager<T> removeDelegate(int itemType)
        {
            int indexToRemove = delegates.indexOfKey(itemType);
    
            if (indexToRemove >= 0)
            {
                delegates.removeAt(indexToRemove);
            }
            return this;
        }
    
        public int getItemViewType(T item, int position)
        {
            int delegatesCount = delegates.size();
            for (int i = delegatesCount - 1; i >= 0; i--)
            {
                ItemViewDelegate<T> delegate = delegates.valueAt(i);
                if (delegate.isForViewType( item, position))
                {
                    return delegates.keyAt(i);
                }
            }
            throw new IllegalArgumentException(
                    "No ItemViewDelegate added that matches position=" + position + " in data source");
        }
    
        public void convert(ViewHolder holder, T item, int position)
        {
            int delegatesCount = delegates.size();
            for (int i = 0; i < delegatesCount; i++)
            {
                ItemViewDelegate<T> delegate = delegates.valueAt(i);
    
                if (delegate.isForViewType( item, position))
                {
                    delegate.convert(holder, item, position);
                    return;
                }
            }
            throw new IllegalArgumentException(
                    "No ItemViewDelegateManager added that matches position=" + position + " in data source");
        }
    
    
        public int getItemViewLayoutId(int viewType)
        {
            return delegates.get(viewType).getItemViewLayoutId();
        }
    
        public int getItemViewType(ItemViewDelegate itemViewDelegate)
        {
            return delegates.indexOfValue(itemViewDelegate);
        }
    }
    

    这里也就解决了多种itemViewType的问题。实现了方便的添加不同的类型的item数据。泛型数据降低了代码的耦合度。

    一种item布局的实现:

    这里提供一种item布局,就只需要对MultiItemTypeAdapter<T>进行限定一种layout类型。并且isForViewType方法返回true,代表着始终返回当前的layout。

    那么对于只有一种类型的列表数据

    /**
     * Created by zhy on 16/4/9.
     */
    public abstract class CommonAdapter<T> extends MultiItemTypeAdapter<T> {
        protected Context mContext;
        protected int mLayoutId;
        protected List<T> mDatas;
        protected LayoutInflater mInflater;
    
        public CommonAdapter(final Context context, final int layoutId) {
            this(context, layoutId, new ArrayList<T>());
        }
    
        public CommonAdapter(final Context context, final int layoutId, List<T> datas) {
            super(context, datas);
            mContext = context;
            mInflater = LayoutInflater.from(context);
            mLayoutId = layoutId;
            mDatas = datas;
    
            addItemViewDelegate(new ItemViewDelegate<T>() {
                @Override
                public int getItemViewLayoutId() {
                    return layoutId;
                }
    
                @Override
                public boolean isForViewType(T item, int position) {
                    return true;
                }
    
                @Override
                public void convert(ViewHolder holder, T t, int position) {
                    CommonAdapter.this.convert(holder, t, position);
                }
            });
        }
    
        protected abstract void convert(ViewHolder holder, T t, int position);
    }
    

    具体的新闻类型:

    
    public class NewsSingleAdapter extends CommonAdapter<NewsItem> {
        public NewsSingleAdapter(Context context) {
            super(context, R.layout.prj_list_item_news);
        }
    
        @Override
        protected void convert(ViewHolder holder, NewsItem item, int position) {
            holder.setText(R.id.tv_news_title, item.getTitle());
            holder.setText(R.id.tv_news_summary, item.getSummary());
            holder.setText(R.id.tv_news_date, item.getTime());
            holder.setImageUrl(R.id.img_news_image,item.getImgs().get(0));
        }
    }
    

    这里也就实现了单一的列表形式,比如网易新闻的专题样式:


    加载更多以及header 和footer的添加

    这里直接参加wrapper包中几个类,


    这里是对不同的item的type 类型进行控制,从而得到了不同的RecyclerView的样式。具体可以参考我的MVPCommon中的代码。
    /**
     * Created by zhy on 16/6/23.
     */
    public class HeaderAndFooterWrapper<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder>
    {
        private static final int BASE_ITEM_TYPE_HEADER = 100000;
        private static final int BASE_ITEM_TYPE_FOOTER = 200000;
    
        private SparseArrayCompat<View> mHeaderViews = new SparseArrayCompat<>();
        private SparseArrayCompat<View> mFootViews = new SparseArrayCompat<>();
    
        private RecyclerView.Adapter mInnerAdapter;
        private RecyclerView.Adapter mNotifyAdapter;
    
        public HeaderAndFooterWrapper(RecyclerView.Adapter adapter)
        {
            mInnerAdapter = adapter;
        }
    
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
        {
            if (mHeaderViews.get(viewType) != null)
            {
                ViewHolder holder = ViewHolder.createViewHolder(parent.getContext(), mHeaderViews.get(viewType));
                return holder;
    
            } else if (mFootViews.get(viewType) != null)
            {
                ViewHolder holder = ViewHolder.createViewHolder(parent.getContext(), mFootViews.get(viewType));
                return holder;
            }
            return mInnerAdapter.onCreateViewHolder(parent, viewType);
        }
    
        @Override
        public int getItemViewType(int position)
        {
            if (isHeaderViewPos(position))
            {
                return mHeaderViews.keyAt(position);
            } else if (isFooterViewPos(position))
            {
                return mFootViews.keyAt(position - getHeadersCount() - getRealItemCount());
            }
            return mInnerAdapter.getItemViewType(position - getHeadersCount());
        }
    
        private int getRealItemCount()
        {
            return mInnerAdapter.getItemCount();
        }
    
    
        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
        {
            if (isHeaderViewPos(position))
            {
                return;
            }
            if (isFooterViewPos(position))
            {
                return;
            }
            mInnerAdapter.onBindViewHolder(holder, position - getHeadersCount());
        }
    
        @Override
        public int getItemCount()
        {
            return getHeadersCount() + getFootersCount() + getRealItemCount();
        }
    
        @Override
        public void onAttachedToRecyclerView(RecyclerView recyclerView)
        {
            mNotifyAdapter = recyclerView.getAdapter();
            WrapperUtils.onAttachedToRecyclerView(mInnerAdapter, recyclerView, new WrapperUtils.SpanSizeCallback()
            {
                @Override
                public int getSpanSize(GridLayoutManager layoutManager, GridLayoutManager.SpanSizeLookup oldLookup, int position)
                {
                    int viewType = getItemViewType(position);
                    if (mHeaderViews.get(viewType) != null)
                    {
                        return layoutManager.getSpanCount();
                    } else if (mFootViews.get(viewType) != null)
                    {
                        return layoutManager.getSpanCount();
                    }
                    if (oldLookup != null)
                        return oldLookup.getSpanSize(position);
                    return 1;
                }
            });
        }
    
        @Override
        public void onViewAttachedToWindow(RecyclerView.ViewHolder holder)
        {
            mInnerAdapter.onViewAttachedToWindow(holder);
            int position = holder.getLayoutPosition();
            if (isHeaderViewPos(position) || isFooterViewPos(position))
            {
                WrapperUtils.setFullSpan(holder);
            }
        }
    
        private boolean isHeaderViewPos(int position)
        {
            return position < getHeadersCount();
        }
    
        private boolean isFooterViewPos(int position)
        {
            return position >= getHeadersCount() + getRealItemCount();
        }
    
        public void addHeaderView(View view)
        {
            int key = findHeaderKeyByView(view);
            if (key == -1) {
                mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view);
                if (mNotifyAdapter != null)
                    mNotifyAdapter.notifyDataSetChanged();
    
                if (mInnerAdapter instanceof MultiItemTypeAdapter) {
                    ((MultiItemTypeAdapter) mInnerAdapter).offset += 1;
                }
            }
        }
    
        public void addFootView(View view)
        {
            mFootViews.put(mFootViews.size() + BASE_ITEM_TYPE_FOOTER, view);
        }
    
        public int getHeadersCount()
        {
            return mHeaderViews.size();
        }
    
        public int getFootersCount()
        {
            return mFootViews.size();
        }
    
        public void deleteHeaderView(View view)
        {
    //        if (mHeaderViews.size() > position && position >=0 ) {
    //            View v = mHeaderViews.get(position + BASE_ITEM_TYPE_HEADER, null);
    //            if (v != null) {
    //                mHeaderViews.remove(position + BASE_ITEM_TYPE_HEADER);
    //                if (mInnerAdapter instanceof MultiItemTypeAdapter) {
    //                    ((MultiItemTypeAdapter) mInnerAdapter).offset -= 1;
    //                }
    //                if (mNotifyAdapter != null)
    //                    mNotifyAdapter.notifyDataSetChanged();
    //            }
    //        }
    
    //        for(int i=0; i<mHeaderViews.size(); i++) {
    //            int key = mHeaderViews.keyAt(i);
    //            if(mHeaderViews.get(key) == view) {
    //                mHeaderViews.remove(key);
    //                if (mInnerAdapter instanceof MultiItemTypeAdapter) {
    //                    ((MultiItemTypeAdapter) mInnerAdapter).offset -= 1;
    //                }
    //                if (mNotifyAdapter != null)
    //                    mNotifyAdapter.notifyDataSetChanged();
    //                break;
    //            }
    //        }
    
            int key = findHeaderKeyByView(view);
            if (key != -1) {
                mHeaderViews.remove(key);
                if (mInnerAdapter instanceof MultiItemTypeAdapter) {
                    ((MultiItemTypeAdapter) mInnerAdapter).offset -= 1;
                }
                if (mNotifyAdapter != null)
                    mNotifyAdapter.notifyDataSetChanged();
            }
        }
    
        public int findHeaderKeyByView(View view) {
            for(int i=0; i<mHeaderViews.size(); i++) {
                int key = mHeaderViews.keyAt(i);
                if(mHeaderViews.get(key) == view) {
                    return key;
                }
            }
    
            return -1;
        }
    }
    

    这里的header和footer没有个数的限制。

    添加section分区操作:

    现在需求又来了 。需要对RecyclerView中的item进行分区操作,就比如说微信以B开头的姓名都放在B这个分区下,以C开头的名字,都在C这个分区下。比如说京东的这个界面
    列表数据里面添加了分区。那么怎么操作呢?


    这里对开源库SectionedRecyclerViewAdapter做了集成。并且添加的上面的ViewHolder,简化onCreateViewHolder中的数据绑定操作。
    也就是代码中的recyclerview-section包中的部分。

    Section的使用:

    1. 创建自定义 Section 类集成自 StatelessSection
        public class WeiboGridSection extends StatelessSection {
            private final List<NewsItem>  list;
    
            public WeiboGridSection(List<NewsItem> list) {
                super(R.layout.grid_item);
                this.list = list;
            }
    
            @Override
            public int getContentItemsTotal() {
                return list.getItems().size();
            }
    
            @Override
            public ViewHolder getItemViewHolder(View view, int viewType) {
                return new ViewHolder(mContext, view);
            }
    
            @Override
            public void onBindItemViewHolder(ViewHolder holder, final int position) {
    
                    final NewsItem newsItem = list.get(position);
                    String itemImgUrl = newsItem.getImages().get(0);
                    final String name = newsItem.getTitle();
                    holder.setImageUrl(R.id.grid_item_iv, itemImgUrl);
                    holder.setText(R.id.grid_item_tv, name);
                    holder.getConvertView().setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            Intent intent = new Intent(getActivity(), WebViewCommentActivity.class);
                            intent.putExtra(WebViewCommentActivity.WEB_VIEW_ITEM, newsItem);
                            startActivity(intent);
                        }
                    });
                }
            }
            @Override
            public ViewHolder getHeaderViewHolder(Context context, View view) {
                return new ViewHolder(mContext, view);
            }
    
            @Override
            public void onBindHeaderViewHolder(ViewHolder holder) {
                holder.setText(R.id.section_header_tv, "微博关注");
                holder.setImageResource(R.id.section_header_iv, R.mipmap.wb_focus);
    
            }
    
        }
    
    
    

    2) 添加section到adapter,注意这里是SectionRVAdapter

    // Create an instance of SectionedRecyclerViewAdapter 
    SectionRVAdapter sectionAdapter = new SectionRVAdapter();
    
    // Add your Sections
    sectionAdapter.addSection(new MySection());
    
    // Set up your RecyclerView with the SectionRVAdapter
    RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerview);
    recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
    recyclerView.setAdapter(sectionAdapter);
    

    看看界面效果


    Section的代码实现

    整个代码由于是对RecyclerView.Adapter封装。所以需要关注的方法自然是getItemViewType,onBindViewHolder,createViewHoldergetItemCount四个方法,下面以这四个方法为切入点进行分析:
    初始化需要关注的是这里使用hashMap对section进行存储。也就实现了后面的根据section的添加顺序依次展示Section到RecyclerView中。


    onCreateViewHolder完成的是ViewHolder的创建,每一个section分为头部header,底部footer。以及中间部分,中间布局可以有Loading/Loaded/Failed三种状态分别对应加载,加载成功,失败界面。注意这里的状态都分别对应于每个section里面,而不是整个RecyclerView.

    也就是说一个RecyclerView可以由多个Section组成,一个Section最多只能有一个Header和Footer,Section由多个RecyclerView的item条目组成。每个Section中间可以有三种状态。Loading/Loaded/Failed三种状态分别对应加载,加载成功,失败界面。

    下面是来自SectionedRecyclerViewAdapter的界面效果

      @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            ViewHolder viewHolder = null;
            View view = null;
    
            for (Map.Entry<String, Integer> entry : sectionViewTypeNumbers.entrySet()) {
                if (viewType >= entry.getValue() && viewType < entry.getValue() + VIEW_TYPE_QTY) {
    
                    Section section = sections.get(entry.getKey());
                    int sectionViewType = viewType - entry.getValue();
    
                    switch (sectionViewType) {
                        case VIEW_TYPE_HEADER: {
                            Integer resId = section.getHeaderResourceId();
    
                            if (resId == null)
                                throw new NullPointerException("Missing 'header' resource id");
    
                            view = LayoutInflater.from(parent.getContext()).inflate(resId, parent, false);
                            // get the header viewholder from the section
                            viewHolder = section.getHeaderViewHolder(mContext,view);
                            break;
                        }
                        case VIEW_TYPE_FOOTER: {
                            Integer resId = section.getFooterResourceId();
    
                            if (resId == null)
                                throw new NullPointerException("Missing 'footer' resource id");
    
                            view = LayoutInflater.from(parent.getContext()).inflate(resId, parent, false);
                            // get the footer viewholder from the section
                            viewHolder = section.getFooterViewHolder(mContext,view);
                            break;
                        }
                        case VIEW_TYPE_ITEM_LOADED: {
                            view = LayoutInflater.from(parent.getContext()).inflate(section.getItemResourceId(), parent, false);
                            // get the item viewholder from the section
                            viewHolder = section.getItemViewHolder(view,viewType);
                            break;
                        }
                        case VIEW_TYPE_LOADING: {
                            Integer resId = section.getLoadingResourceId();
    
                            if (resId == null)
                                throw new NullPointerException("Missing 'loading state' resource id");
    
                            view = LayoutInflater.from(parent.getContext()).inflate(resId, parent, false);
                            // get the loading viewholder from the section
                            viewHolder = section.getLoadingViewHolder(mContext,view);
                            break;
                        }
                        case VIEW_TYPE_FAILED: {
                            Integer resId = section.getFailedResourceId();
    
                            if (resId == null)
                                throw new NullPointerException("Missing 'failed state' resource id");
    
                            view = LayoutInflater.from(parent.getContext()).inflate(resId, parent, false);
                            // get the failed load viewholder from the section
                            viewHolder = section.getFailedViewHolder(mContext,view);
                            break;
                        }
                        default:
                            throw new IllegalArgumentException("Invalid viewType");
                    }
                }
            }
    
            return viewHolder;
        }
    

    和前面MultiItemTypeAdapter<T>的实现一样,我们需要根据不同viewType创建不同的viewHolder.但是需要注意的是一个Section是由一组item组成的,所以一个section需要多次的调用onCreateViewHolder进行创建不同的类型的样式。

    接下来关注onBindViewHolder方法,通过
    int sectionTotal = section.getSectionItemsTotal();获取到了section的item的数量,并在下方针对每个section的头部header,底部footer,以及中间部分进行操作。并且调用onBindHeaderViewHolder(holder),
    onBindFooterViewHolder(holder)
    以及onBindContentViewHolder(holder, getSectionPosition(position))方法,这就是当我们实现Section代码的时候需要实现的方法。section.getSectionItemsTotal()也是我们实现Section的时候提供的section的item的个数。

        @Override
        public void onBindViewHolder(ViewHolder holder, int position) {
    
            int currentPos = 0;
    
            for (Map.Entry<String, Section> entry : sections.entrySet()) {
                Section section = entry.getValue();
    
                // ignore invisible sections
                if (!section.isVisible()) continue;
    
                int sectionTotal = section.getSectionItemsTotal();
    
                // check if position is in this section
                if (position >= currentPos && position <= (currentPos + sectionTotal - 1)) {
    
                    if (section.hasHeader()) {
                        if (position == currentPos) {
                            // delegate the binding to the section header
                            getSectionForPosition(position).onBindHeaderViewHolder(holder);
                            return;
                        }
                    }
    
                    if (section.hasFooter()) {
                        if (position == (currentPos + sectionTotal - 1)) {
                            // delegate the binding to the section header
                            getSectionForPosition(position).onBindFooterViewHolder(holder);
                            return;
                        }
                    }
    
                    // delegate the binding to the section content
                    getSectionForPosition(position).onBindContentViewHolder(holder, getSectionPosition(position));
                    return;
                }
    
                currentPos += sectionTotal;
            }
    
            throw new IndexOutOfBoundsException("Invalid position");
        }
    

    接下来关注getItemCount,代表整个RecyclerView的item的个数。当然是所有Section的item的总和,所以代码如下。


    最后需要关注的是方法getItemViewType,这里也就完成了每个Section的五种类型的int返回操作。
       @Override
        public int getItemViewType(int position) {
             /*
             Each Section has 5 "viewtypes":
             1) header
             2) footer
             3) items
             4) loading
             5) load failed
             */
            int currentPos = 0;
    
            for (Map.Entry<String, Section> entry : sections.entrySet()) {
                Section section = entry.getValue();
    
                // ignore invisible sections
                if (!section.isVisible()) continue;
    
                int sectionTotal = section.getSectionItemsTotal();
    
                // check if position is in this section
                if (position >= currentPos && position <= (currentPos + sectionTotal - 1)) {
    
                    int viewType = sectionViewTypeNumbers.get(entry.getKey());
    
                    if (section.hasHeader()) {
                        if (position == currentPos) {
                            return viewType;
                        }
                    }
    
                    if (section.hasFooter()) {
                        if (position == (currentPos + sectionTotal - 1)) {
                            return viewType + 1;
                        }
                    }
    
                    switch (section.getState()) {
                        case LOADED:
                            return viewType + 2;
                        case LOADING:
                            return viewType + 3;
                        case FAILED:
                            return viewType + 4;
                        default:
                            throw new IllegalStateException("Invalid state");
                    }
    
                }
    
                currentPos += sectionTotal;
            }
    
            throw new IndexOutOfBoundsException("Invalid position");
        }
    
    

    首先关注的是SectionRVAdapter,这里我并没有集成自上面的MultiItemTypeAdapter<T>,应为这里涉及到对RecyclerView.Adapter的封装操作。这里的弊端也就是每一个section甚至整个RecyclerView的itemView都是一个形式。但是多种形式的section我们可以转化为将section也视作一种MultiItemTypeAdapter<T>的item类型就能解决,所以这里也不算问题。这里是SectionRVAdapter的完整代码。至于Section类,主要是对几种方法和view状态,以及所有item的封装,这里不再赘述。直接看代码。

    最后针对最近项目中遇到的这个问题,针对不同的布局,比如说下面的这个既有Grid,又有linear的形式。由于之前的问题全部是针对一个RecyclerView的,而一个RecyclerView在调用recyclerView.setLayoutManager()方法的时候,就只能有一个布局方式。好吧,当初我就是为了解决这个问题,后来才发现需要用三个RecyclerView来解决。

    这篇博客就到这里,回过头去,去看看鸿洋写的为RecyclerView打造通用Adapter 让RecyclerView更加好用,以及开源库baseAdapter github相信你一目了然。
    这里就是对baseAdapter github引用到实际项目中以及引入开源库SectionedRecyclerViewAdapter作为实际开发的例子。

    注:这里不能提供实际项目代码,只能提供代码片段作为参考。目前暂未提供示例代码到github的项目中,只提供了recyclerView公用组件。
    项目地址:MVPCommon

    参考链接

    baseAdapter github

    为RecyclerView打造通用Adapter 让RecyclerView更加好用

    RecyclerView源码分析

    RecyclerView技术栈

    相关文章

      网友评论

      • RoyAlex:请问如何刷新数据呢
      • 无辜的小黄人:你最后的那个布局之后是用的三个RecyclerView还是一个RecyclerView呀
      • 河马爱生活:楼主,你好,在ItemViewDelegate 如何调用notifyDataSetChanged()方法
      • RoyAlex:很赞
      • eba6cd3f3618:分解的很详细
      • 追梦者king:楼主,你的工程里没有common-widgets-recyclerview这个recyclerview包 @CameloeAnthony,无法按你这个文章进行参考啊
        岳齐:@CameloeAnthony找到了 学习一下 谢谢
        程序员Anthony:@追梦者king 额 。我这两天把它全部移到ThirdPart-RVHelper里面了 。作为一个library。可以方便进行copy 使用了 。
      • 伪文艺代表:ItemViewDelegate接口中的isForViewType()是不是理解为当前的item的布局?
      • Terry:这几乎是hongyang大神的代码
        程序员Anthony:@Tatastar 对的 。在他的基础上结合了section 的操作
      • tpkeeper:大神,有没有对网络请求方面的封装?
        tpkeeper:@CameloeAnthony :yum:
        程序员Anthony:@tpkeeper 参考https://gank.io/post/56e80c2c677659311bed9841
        RxJava 与Retrofit 结合的最佳实践
      • ada572ea42e9:开发公司的项目任务的时候 几乎没有人使用别人封装的view 因为得不偿失 正像楼上所说 还得费劲去代码 而且有不可预期的bug
        程序员Anthony:@Eddie_ 确实,思路是分享了。但是很多公司还是要根据自己的业务情况去封装自己的view。我们是做新闻客户端的。所以封装的这些对于新闻客户端来说确实比较好用。
      • 秋兰兮青青:1.recycleview 已经很完美了,你这么一封装,多了一大堆东西,别人用了万一有问题还得去看你的各种封装代码

        2.虽说安卓机器配置越来越好,但是app的内存占用也是水涨船高,从开发效率上来讲,前提是你的封装足够健壮,bug很少很少,这样可以提高开发效率。 但从性能上来说,由于你是基类用了泛型,所以每次渲染列表的时候都要去反射,同时还增加了很多对象,这些对象,并不能被即时回收。 所以势必占用更多内存。

        以上只是我的看法。
        秋兰兮青青:@CameloeAnthony 挺好的
        程序员Anthony:用于自己的项目中,完全简化了开发 。博客文章提供参考性意见,希望看到的人有收获。同时也是对自己知识的一个归纳整理
        小王泽哥: @83d8172c0930 我没看完,但是简单的封装可以做。复杂的封装没必要,因为谁也不能保证整体的业务需求都用的到,像新闻这种咨询类的完全可以
      • 大米的木头:我保证,我收藏后会看的……等考完研
        老唱片之桥: @木头李二 哈哈,计算机专业?加油哦

      本文标题:真实项目运用-RecyclerView封装

      本文链接:https://www.haomeiwen.com/subject/xuzmsttx.html