CRecyclerView使用指南--HeaderView、EmptyView、FootView

前言:这两天借鉴了几位大牛的文章,自己撸了一个极度简便的RecyclerView的控件,包括了HeaderView、EmptyView、FootView三个功能,核心类只有一个,如下。按照普通的RecyclerView使用即可.
改版:1、头部、脚部的计数方式改为按可见状态下的计算。
2、增加了内部类WrapContentLinearLayoutManager。
修改:增加Item点击事件

一、核心类介绍

package com.zuji.entrance.widget;

import android.content.Context;
import android.support.v4.util.SparseArrayCompat;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

/**
 * Created by fangyc on 2018/7/9.
 */

public class CRecyclerView extends RecyclerView {
    HeaderAndFooterWrapAdapter mHeaderAndFooterWrapper;//与头部、脚部相关
    private View emptyView;  //与空布局相关
    //数据观察者,与空布局相关
    final private AdapterDataObserver observer = new AdapterDataObserver() {
        @Override
        public void onChanged() {
            checkIfEmpty();
        }

        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            checkIfEmpty();
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            checkIfEmpty();
        }
    };

    public CRecyclerView(Context context) {
        this(context, null);
    }

    public CRecyclerView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CRecyclerView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    /**
     * 添加头部
     *
     * @param view
     */
    public void addHeaderView(View view) {
        if (mHeaderAndFooterWrapper == null)
            throw new UnsupportedOperationException("设置头部前,请先setAdapter!");
        mHeaderAndFooterWrapper.addHeaderView(view);
    }

    /**
     * 添加底部
     *
     * @param view
     */
    public void addFootView(View view) {
        if (mHeaderAndFooterWrapper == null)
            throw new UnsupportedOperationException("设置底部前,请先setAdapter!");
        mHeaderAndFooterWrapper.addFootView(view);
    }

    /**
     * 移除头部,0<=position<=头部的个数
     *
     * @param position
     */
    public void removeHeaderView(int position) {
        mHeaderAndFooterWrapper.removeHeaderView(position);
    }

    /**
     * 移除底部,0<=position<=底部的个数
     *
     * @param position
     */
    public void removeFootView(int position) {
        mHeaderAndFooterWrapper.removeFootView(position);
    }

    /**
     * 获取头部
     *
     * @param position
     * @return
     */
    public View getHeaderView(int position) {
        return mHeaderAndFooterWrapper.getHeaderView(position);
    }

    /**
     * 获取脚部
     *
     * @param position
     * @return
     */
    public View getFootView(int position) {
        return mHeaderAndFooterWrapper.getFootView(position);
    }

    /**
     * 设置头部可见状态
     *
     * @param position
     * @param visibility
     */
    public void setHeaderViewVisibility(int position, int visibility) {
        mHeaderAndFooterWrapper.setHeaderViewVisibility(position, visibility);
    }

    /**
     * 设置脚部可见状态
     *
     * @param position
     * @param visibility
     */
    public void setFootViewVisibility(int position, int visibility) {
        mHeaderAndFooterWrapper.setFootViewVisibility(position, visibility);
    }

/*----以上为关于headerView和footView的部分--------------------------------------------------------------------------------------*/

/*----以下为关于emptyView的部分--------------------------------------------------------------------------------------*/

    /**
     * 检查数据是否为空,是否展示emptyView
     */
    private void checkIfEmpty() {
        if (emptyView != null && getAdapter() != null) {
            final boolean emptyViewVisible = getAdapter().getItemCount() == 0;
            emptyView.setVisibility(emptyViewVisible ? VISIBLE : GONE);
            setVisibility(emptyViewVisible ? GONE : VISIBLE);
        }
    }

    //设置没有内容时,提示用户的空布局
    public void setEmptyView(View emptyView) {
        this.emptyView = emptyView;
//        checkIfEmpty();
    }

    /**
     * 返回空布局
     *
     * @return
     */
    public View getEmptyView() {
        return this.emptyView;
    }

    @Override
    public void setAdapter(Adapter adapter) {
        final Adapter oldAdapter = getAdapter();
        if (oldAdapter != null) {
            oldAdapter.unregisterAdapterDataObserver(observer);
        }
        mHeaderAndFooterWrapper = new HeaderAndFooterWrapAdapter(adapter);
        super.setAdapter(mHeaderAndFooterWrapper);
        if (adapter != null) {
//            adapter.registerAdapterDataObserver(observer);
            mHeaderAndFooterWrapper.registerAdapterDataObserver(observer);
        }
        checkIfEmpty();
//        observer.onChanged();
    }

    public void notifyDataSetChanged(){
        final Adapter adapter = getAdapter();
        if (adapter != null) {
            adapter.notifyDataSetChanged();
        }
    }

    /**
     * 装饰者模式,
     * 用于处理headerView和FootView,
     * 内部类,也可以将该类独立出来.
     *
     * @param <T>
     */
    private class HeaderAndFooterWrapAdapter<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;

        public HeaderAndFooterWrapAdapter(RecyclerView.Adapter adapter) {
            mInnerAdapter = adapter;
        }

        private boolean isHeaderViewPos(int position) {
            return position < getVisibleHeadersCount();
        }

        private boolean isFooterViewPos(int position) {
            return position >= getVisibleHeadersCount() + getRealItemCount();
        }


        public void addHeaderView(View view) {
            mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view);
        }

        public void addFootView(View view) {
            mFootViews.put(mFootViews.size() + BASE_ITEM_TYPE_FOOTER, view);
        }

        public void removeHeaderView(int position) {
            mHeaderViews.remove(position + BASE_ITEM_TYPE_HEADER);
        }

        public void removeFootView(int position) {
            mFootViews.remove(position + BASE_ITEM_TYPE_FOOTER);
        }

        public View getHeaderView(int position) {
            return mHeaderViews.get(position + BASE_ITEM_TYPE_HEADER, null);
        }

        public View getFootView(int position) {
            return mFootViews.get(position + BASE_ITEM_TYPE_FOOTER, null);
        }

        public void setHeaderViewVisibility(int position, int visibility) {
            if (mFootViews.get(position + BASE_ITEM_TYPE_HEADER) != null) {
                mFootViews.get(position + BASE_ITEM_TYPE_HEADER).setVisibility(visibility);
            }
        }

        public void setFootViewVisibility(int position, int visibility) {
            if (mFootViews.get(position + BASE_ITEM_TYPE_FOOTER) != null) {
                mFootViews.get(position + BASE_ITEM_TYPE_FOOTER).setVisibility(visibility);
            }
        }

//        public int getHeadersCount() {
//            return mHeaderViews.size();
//        }
//
//        public int getFootersCount() {
//            return mFootViews.size();
//        }

        /**
         * 获取可见的头部条数
         *
         * @return
         */
        private int getVisibleHeadersCount() {
            int size = mHeaderViews.size();
            if (size <= 0) return size;
            int visiSize = 0;
            for (int i = 0; i < size; i++) {
                View view = mHeaderViews.get(i + BASE_ITEM_TYPE_HEADER);
                if (view.getVisibility() == View.VISIBLE) visiSize++;
            }
            return visiSize;
        }

        /**
         * 获取可见的脚部条数
         *
         * @return
         */
        private int getVisibleFootersCount() {
            int size = mFootViews.size();
            if (size <= 0) return size;
            int visiSize = 0;
            for (int i = 0; i < size; i++) {
                View view = mFootViews.get(i + BASE_ITEM_TYPE_FOOTER);
                if (view.getVisibility() == View.VISIBLE) visiSize++;
            }
            return visiSize;
        }

        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            if (mHeaderViews.get(viewType) != null) {
                return new SimpleViewHolder(mHeaderViews.get(viewType));

            } else if (mFootViews.get(viewType) != null) {
                return new SimpleViewHolder(mFootViews.get(viewType));
            }
            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 - getVisibleHeadersCount() - getRealItemCount());
            }
            return mInnerAdapter.getItemViewType(position - getVisibleHeadersCount());
        }

        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 - getVisibleHeadersCount());
        }

        @Override
        public int getItemCount() {
//            return getHeadersCount() + getFootersCount() + getRealItemCount();
            return getVisibleHeadersCount() + getVisibleFootersCount() + getRealItemCount();

        }

        private class SimpleViewHolder extends RecyclerView.ViewHolder {
            public SimpleViewHolder(View itemView) {
                super(itemView);
            }
        }

        /**
         * 针对GridLayoutManager
         *
         * @param recyclerView
         */
        @Override
        public void onAttachedToRecyclerView(RecyclerView recyclerView) {
            mInnerAdapter.onAttachedToRecyclerView(recyclerView);

            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 (mHeaderViews.get(viewType) != null) {
                            return gridLayoutManager.getSpanCount();
                        } else if (mFootViews.get(viewType) != null) {
                            return gridLayoutManager.getSpanCount();
                        }
                        if (spanSizeLookup != null)
                            return spanSizeLookup.getSpanSize(position);
                        return 1;
                    }
                });
                gridLayoutManager.setSpanCount(gridLayoutManager.getSpanCount());
            }
        }

        /**
         * 对于StaggeredGridLayoutManager
         *
         * @param holder
         */
        @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);
                }
            }
        }
    }

    /**
     * 据说
     * RecyclerView Bug:IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter的解决方案
     */
    public static class WrapContentLinearLayoutManager extends LinearLayoutManager {
        public WrapContentLinearLayoutManager(Context context) {
            super(context);
        }

        public WrapContentLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
            super(context, orientation, reverseLayout);
        }

        public WrapContentLinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
        }

        @Override
        public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
            try {
                super.onLayoutChildren(recycler, state);
            } catch (IndexOutOfBoundsException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Created by Administrator on 2017/5/5.
     */

    public static class RecyclerViewClickListener implements RecyclerView.OnItemTouchListener {

        private GestureDetector mGestureDetector;
        private OnItemClickListener mListener;

        //内部接口,定义点击方法以及长按方法
        public interface OnItemClickListener {
            void onItemClick(View view, int position);

            void onItemLongClick(View view, int position);
        }

        public RecyclerViewClickListener(Context context, final RecyclerView recyclerView, OnItemClickListener listener) {
            mListener = listener;
            mGestureDetector = new GestureDetector(context,
                    new GestureDetector.SimpleOnGestureListener() { //这里选择SimpleOnGestureListener实现类,可以根据需要选择重写的方法
                        //单击事件
                        @Override
                        public boolean onSingleTapUp(MotionEvent e) {
                            View childView = recyclerView.findChildViewUnder(e.getX(), e.getY());
                            if (childView != null && mListener != null) {
                                mListener.onItemClick(childView, recyclerView.getChildLayoutPosition(childView));
                                return true;
                            }
                            return false;
                        }

                        //长按事件
                        @Override
                        public void onLongPress(MotionEvent e) {
                            View childView = recyclerView.findChildViewUnder(e.getX(), e.getY());
                            if (childView != null && mListener != null) {
                                mListener.onItemLongClick(childView, recyclerView.getChildLayoutPosition(childView));
                            }
                        }
                    });
        }

        @Override
        public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
            //把事件交给GestureDetector处理
            if (mGestureDetector.onTouchEvent(e)) {
                return true;
            } else
                return false;
        }

        @Override
        public void onTouchEvent(RecyclerView rv, MotionEvent e) {
        }

        @Override
        public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        }
    }
}

二、使用介绍:

1,布局,(emptyView可以自己定义,也可以是简单的TextView、ImageView等)

<LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:background="@color/transparent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior">

            <com.csair.staffservice.view.CRecyclerView
                android:id="@+id/msgRecyclerView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="#f5f5f5"
                android:overScrollMode="never" />

            <com.csair.staffservice.view.EmptyView
                android:id="@+id/msgEmpty"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="#f5f5f5"
                android:overScrollMode="never" />
        </LinearLayout>

2,代码使用

 mRecyclerView = (CRecyclerView) view.findViewById(R.id.msgRecyclerView);
 mEmptyView = (View) view.findViewById(R.id.msgEmpty);

//设置layoutManager
mRecyclerView.setLayoutManager(mRecyclerView.new WrapContentLinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));

//设置空布局
 mRecyclerView.setEmptyView(mEmptyView);
//设置头部
mRecyclerView.addHeaderView(new HeaderView(getContext()));
//设置脚部
mRecyclerView.addFootView(new NoDataFooterView(getContext()));

这几个布局都可以自由发挥

添加Item点击事件

crv_hometown.addOnItemTouchListener(new CRecyclerView.RecyclerViewClickListener(getContext(), crv_hometown, new CRecyclerView.RecyclerViewClickListener.OnItemClickListener() {
            @Override
            public void onItemClick(View view, int position) {
                String region;
//                RegionsBean.DataBean bean = regionsBean.getData().get(position);
                ZujiRegionSelectedDM.setSelectedBean(regionsBean.getData().get(position));
                if (LanguageChecker.isZH()) {
                    region = regionsBean.getData().get(position).getRegion_cn();
                } else {
                    region = regionsBean.getData().get(position).getRegion_en();
                }
                tv_regoin_selected_name.setText(region);
//                mAdapter.setSelectedBean(selectedBean);
                crv_hometown.notifyDataSetChanged();
            }

            @Override
            public void onItemLongClick(View view, int position) {

            }
        }));

三、bug介绍

删除元素出现的bug及解决方案:

1、(亲测,不可靠) RecyclerView 删除元素后,点击报 IndexOutOfBoundsException 解决方法,

onBindViewHolder() 方法中的位置参数 position 不是实时更新的,所以在我们删除元素后,item 的 position 没有改变。为了实时获取元素的位置,RecyclerView 为我们提供了 ViewHolder.getAdapterPosition() 方法。
当把上面奔溃的代码中的 position 换成 holder.getAdapterPosition() 就解决了问题。

2、(亲测,不可靠)IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter的解决方案,

其实也不是什么解决方法,只是把这个异常捕获了,不让他奔溃了,这个问题的终极解决方案还是得让google去修复。
a、创建一个类LinearLayoutManagerWrapper继承LinearLayoutManager,重写onLayoutChildren方法

public class WrapContentLinearLayoutManager extends LinearLayoutManager {  
    public WrapContentLinearLayoutManager(Context context) {  
        super(context);  
    }  
  
    public WrapContentLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {  
        super(context, orientation, reverseLayout);  
    }  
  
    public WrapContentLinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {  
        super(context, attrs, defStyleAttr, defStyleRes);  
    }  
  
    @Override  
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {  
        try {  
            super.onLayoutChildren(recycler, state);  
        } catch (IndexOutOfBoundsException e) {  
            e.printStackTrace();  
        }  
    }  
}  

b、设置RecyclerView的布局管理为WrapContentLinearLayoutManager对象

mRecyclerView.setLayoutManager(new WrapContentLinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)); 
3、(亲测,相当可靠)positionViewHolder{a1bbfa3 position=2 id=-1, oldPos=-1, pLpos:-1 no parent}

网上查阅下说是原生bug,自定义了线性layout,重写LinearLayoutManagerWrapper.结果发现还是会报红,干脆在clear数据时,recycler_user.removeAllViews();就没遇到报的问题了.在删除元素时,我的真正有效的解决方法是:

try {
        StaffMsgBean.DataBean data = datas.remove(position);
        if (datas.size() <= 0) {
              crv.setFootViewVisibility(0, View.GONE);
        }
        //解决bug的核心
        crv.removeAllViews();//移除所有views
        notifyDataSetChanged();//刷新数据,会触发CRecyclerView中observer#onChanged()
        crv.scrollToPosition(position);//滚动至具体条目位置
        StaffRepo.getInstance().init(act).delete(data.getId());
     } catch (Exception e) {
          // e.printStackTrace();
          ToastUtil.showShort(act, "删除失败!");
     }

最后附上大牛文章
Android 优雅的为RecyclerView添加HeaderView和FooterView
RecyclerView添加EmptyView(空布局)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容