RecyclerView 优雅封装

2,340 阅读8分钟

前言

一些框架的痛点

最近准备打造一款 material design 风格的 github 的 Android 客户端,在实现搜索仓库列表界面时,涉及到列表肯定要下拉刷新,上拉加载更多,以前项目中用到的都是一个开源项目 Android-PullToRefresh,但是这个仓库介绍中发现以及标记为 DEPRECATED 了 ,这个框架对于一般的需求确实够用了,但是不支持 RecyclerView ,这个开源框架的痛点就是如果要支持某一个控件的下拉刷新功能,就需要对这个控件进行适配修改,在控件这么多的时代,如果要全部支持那是一个巨大工作量,因为这个框架不是通用性的,估计这也是这个框架的开发者将之废弃的原因吧。

我所想要的

那么,什么样的下拉刷新,上拉更多框架是我所想要的呢?我所想要的大概具有以下特点:

  • 支持下拉刷新,上拉加载并且对不同的控件通用低耦合
  • 支持多种不通的数据类型显示
  • 支持多种个性化 Header 和 Footer
  • 易用性高

解决方案

下拉刷新

首先推荐一个我最喜欢的下拉刷新的框架 android-Ultra-Pull-To-Refresh,以前自己写的一个项目Android 通用下拉刷新就是按照这个框架的思想去实现的。这个框架的优点是它的下拉刷新跟内部的子控件没有耦合,类似 Google 官方推出的那个下拉刷新的思想,但是谷歌官方的那个做得确实不怎么好看所以不喜欢用。这个下拉刷新框架几乎支持所有的控件,确实很强大。但是被问及为什么不支持上拉加载更多的功能时,原作者表示这两个不应该属于同一个层次,上拉加载更多应该由子控件自己去做,既然作者都明确表示不支持上拉加载更多,那只好自己去扩展了。
当然下拉刷新你也可以使用其他的如 Google 推出的下拉刷新框架,不影响的。

上拉加载

看到网上很多人在这个下拉刷新的框架上是这样扩展上拉加载的,就是将原来的下拉刷新的逻辑倒过来用在上拉加载上,这个一开始我觉得还是可行的并且比较统一也很通用的,但是后来细想以后就发现还是有局限性,比如我的子控件是一些不规则的列表比如瀑布流或者横向列表,那么按照这个逻辑去实现的话就不能满足我们的需求,所以我也赞同做一个通用的框架下拉刷新和上拉加载更多不应该是在同一个层次去实现。还是由自控件自己去做通用的上拉加载。

通用 RecyclerView 实现

RecyclerView 这一个控件出来已经有很长时间了,但是很多老项目还是使用的是 ListView,毕竟 ListView 已经基本满足日常使用的需求了,并且迁移成本太大。另外,一个控件越强大自由意味着需要的定制化就越多。先说以下 RecyclerView 和 ListView 主要的一些不同点吧:

  • RecyclerView 没有 HeaderView 和 FooterView
  • RecyclerView 没有封装 Item 的单机事件
  • RecyclerView 提供了 ViewHolder
  • RecyclerView 提供了多种样式的 LayoutManager

扩展新增类

因为主要在下拉刷新框架上进行扩展,所以也就放在了那个框架中,主要新加了两个类:

  • BaseRecyclerViewAdapter:通用的 RecyclerViewAdapter 抽象类,所有业务 Adapter 都继承这个。
  • BaseFooter:通用的上拉加载抽象类,所有的自定义的加载更多的 View 都继承这个。

使用方法

ptrFrameLayout = (PtrClassicFrameLayout) findViewById(R.id.ptr_container);
ptrFrameLayout.setPtrHandler(new PtrDefaultHandler() {
    @Override
    public void onRefreshBegin(PtrFrameLayout frame) {
        frame.postDelayed(new Runnable() {
            @Override
            public void run() {
                ptrFrameLayout.refreshComplete();
            }
        },2000);
    }
});
recyclerView = (RecyclerView) findViewById(R.id.recycleview);
recyclerView.setLayoutManager(new LinearLayoutManager(mActivity));
recyclerView.setItemAnimator(new DefaultItemAnimator());
recyclerView.setHasFixedSize(true);
TextView tv = new TextView(mActivity);
mAdapter.addHeaderView(tv);
//mAdapter.addFooterView(tv);
mAdapter = new RepositoriesAdapter(recyclerView,new ArrayList());
recyclerView.setAdapter(mAdapter);
mAdapter.setOnLoadMoreListener(recyclerView, new BaseRecyclerViewAdapter.onLoadMoreListener() {
    @Override
    public void loadMore(int currentPage) {
        searchRepository(currentPage);
    }
});
mAdapter.setOnItemClickListener(new BaseRecyclerViewAdapter.OnItemClickListener() {
    @Override
    public void onItemClick(View view, int position,long id) {
        showToastShort("你点击的是第"+position+"个位置"+" id="+id);
    }
});
GithubApi.searchRepositiories(query.toString(), "stars", "desc",currentPage,new IDataCallback() {
    @Override
    public void onSuccess(ReponseRepositories object, Headers headers) {
        if (object!=null&&object.getRepositoryList()!=null){
                mAdapter.addListData(object.getRepositoryList(),currentPage);
        }
    }
    @Override
    public void onError(int code, String message) {
        mAdapter.loadMoreError();
        showToastShort(code+message);
    }
});

这就是一个简单的使用方法,其中暴露出来的

  • onRefreshBegin 是下拉刷新的回调方法
  • loadMore 是上拉加载更多的回调方法,currentPage是分页中的方法,如果是下拉刷新或者重新加载数据都传1,否则传其他值
  • onItemClick 点击事件,仿照 Listview 中的点击接口
  • mAdapter.addListData(List,int currentPage) 加载数据,currentPage 传1表示刷新数据,否则直接 add 数据
  • mAdapter.loadMoreError() 加载失败调用

总结

关于 RecyclerView 之类的基础知识这里就不再赘述了,对于如何封装通用的 RecyclerView,网上也有很多思路,水平也参差不齐,而且标题党比价多,看标题吊炸天的点进去大失所望。这里主要提供我的一种思路,总体实现了对 RecyclerView 的下拉刷新,上拉加载的封装,简化了 RecyclerView 的使用,希望对大家有所帮助。

源代码

因为不想再创建一个仓库,所以直接贴一下主要代码。
BaseRecyclerViewAdapter.java

public abstract class BaseRecyclerViewAdapter extends RecyclerView.Adapter{
    public final static String TAG = "BaseRecyclerViewAdapter";
    public final static int ITEM_TYPE_HEADER = 55000;
    public final static int ITEM_TYPE_FOOTER = 60000;
    public final static int FIXED_ITEM_LOAD_MORE = 50000;
    public final static int COUNT_FIXED_ITEM = 1;
    public final static int PER_PAGE_SIZE =20;
    protected MultiItemType multiItemType;
    private OnItemClickListener mOnItemClickListener;
    protected ILoadMore loadMoreFooterView;
    protected RecyclerView mRecyclerView;
    private List mData;
    private Context mContext;
    private SparseArray mHeaderViews = new SparseArray<>();
    private SparseArray mFooterViews = new SparseArray<>();
    public BaseRecyclerViewAdapter(RecyclerView recyclerView,List mData) {
        this.mData = mData;
        this.mContext = recyclerView.getContext();
        this.mRecyclerView = recyclerView;
        initDefaultLoadMoreView();
    }
    private void initDefaultLoadMoreView() {
        loadMoreFooterView = new BaseFooter(mContext,mRecyclerView);
    }
    public abstract void bindDataToItemView(VH vh, T item);
    public abstract int getAdapterLayout();
    public void setOnItemClickListener(OnItemClickListener onItemClickListener){
        this.mOnItemClickListener = onItemClickListener;
    }
    public void addHeaderView(View headerView){
        mHeaderViews.put(mHeaderViews.size()+ITEM_TYPE_HEADER,headerView);
    }
    public void addFooterView(View footerView){
        mFooterViews.put(mFooterViews.size()+ITEM_TYPE_FOOTER,footerView);
    }
    /**
     * 这里直接使用RecyclerViewHolder,子类也直接使用这个ViewHolder,如果子类要自定义ViewHolder,这里的方法需要延迟到子类去加载。
     * @param v
     * @return
     */
    public VH createViewHolder(View v){
        return (VH) new RecyclerViewHolder(v);
    }
    @Override
    public VH onCreateViewHolder(ViewGroup parent, int viewType) {
//        View v = View.inflate(parent.getContext(), getAdapterLayout(),null);
        //fix do supply the parent
        if (viewType == FIXED_ITEM_LOAD_MORE){
            return createViewHolder(loadMoreFooterView.getContainerView());
        }else if (viewType >= ITEM_TYPE_HEADER && viewType= ITEM_TYPE_FOOTER){
            return createViewHolder(mFooterViews.get(viewType));
        }
        int layoutId = getAdapterLayout();
        if (multiItemType!=null){
            layoutId = multiItemType.getLayoutId(viewType);
        }
        View v = LayoutInflater.from(parent.getContext()).inflate(layoutId,parent,false);
        return createViewHolder(v);
    }
    @Override
    public int getItemViewType(int position) {
        if (position == getItemCount()-1){
            return FIXED_ITEM_LOAD_MORE;
        }else if (isHeader(position)){
            return ITEM_TYPE_HEADER+position;
        }else if (isFooter(position)){
            return ITEM_TYPE_FOOTER+(position-mHeaderViews.size()-mData.size());
        }else if (multiItemType!=null){
            int realPosition = getRealDataPosition(position);
            return multiItemType.getItemViewType(realPosition,mData.get(realPosition));
        }else {
            return super.getItemViewType(position);
        }
    }
    public ILoadMore getFooter(){
        return loadMoreFooterView;
    }
    private int getRealDataPosition(int mixedPosition){
        return mixedPosition-mHeaderViews.size();
    }
    private boolean isHeader(int position) {
        return mHeaderViews.size() > 0 && position < mHeaderViews.size();
    }
    private boolean isFooter(int position) {
        return mFooterViews.size() > 0 && position >= getItemCount() - mFooterViews.size()- COUNT_FIXED_ITEM;
    }
    @Override
    public void onBindViewHolder(final VH holder, int position) {
        if (getItemViewType(position) < FIXED_ITEM_LOAD_MORE){
            T item = getItem(getRealDataPosition(position));
            bindDataToItemView(holder,item);
        }
        if (mOnItemClickListener!=null){
            holder.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    int position = holder.getAdapterPosition();
                    mOnItemClickListener.onItemClick(v,position,getRealDataPosition(position));
                }
            });
        }
    }
    @Override
    public int getItemCount() {
        return mData.size()+ COUNT_FIXED_ITEM +mHeaderViews.size()+mFooterViews.size();
    }
    public T getItem(int position){
        return mData.get(position);
    }
    public void addListData(T data){
        if (mData == null){
            mData = new ArrayList<>();
            mData.add(data);
        }else {
            mData.add(data);
        }
        notifyDataSetChanged();
    }
    public void addListData(List data){
        addListData(data,-1);
    }
    /**
     *
     * @param data
     * @param currentPage 为1的话刷新加载更多的状态,否则的话不管
     */
    public void addListData(List data,int currentPage){
        if (mData == null){
            mData = data;
        }else {
            if (currentPage == 1){
                mData.clear();
            }
            mData.addAll(data);
        }
        if (loadMoreFooterView!=null){
            loadMoreFooterView.setState(data.size(),currentPage);
        }
        notifyDataSetChanged();
    }
    public void loadMoreError(){
        if (loadMoreFooterView!=null){
            loadMoreFooterView.setLoadingError();
        }
    }
    /**
     * 是否禁用加载更多
     * @param enable
     */
    public void enableLoadMoreView(boolean enable){
        loadMoreFooterView.isEnable = enable;
    }
    public static class RecyclerViewHolder extends RecyclerView.ViewHolder {
        private final SparseArray views;
        public RecyclerViewHolder(View itemView) {
            super(itemView);
            views = new SparseArray<>();
        }
        public  T getView(int id){
            View view = views.get(id);
            if (view == null){
                view = itemView.findViewById(id);
                views.put(id,view);
            }
            return (T) view;
        }
    }
    /**
     * add for multiItemType
     * @param 
     */
    public interface MultiItemType{
        int getLayoutId(int itemType);
        int getItemViewType(int position,T t);
    }
    public interface OnItemClickListener{
        void onItemClick(View view,int position,long id);
    }
    /**
     * 当LayoutManager是GridLayoutManager时,设置header和footer占据的列数
     * @param recyclerView recyclerView
     */
    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        super.onAttachedToRecyclerView(recyclerView);
        final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        if (layoutManager instanceof GridLayoutManager) {
            final GridLayoutManager gridManager = ((GridLayoutManager) layoutManager);
            gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    return (isFooter(position) || isHeader(position))
                            ? gridManager.getSpanCount() : 1;
                }
            });
        }
    }
    /**
     * 当LayoutManager是StaggeredGridLayoutManager时,设置header和footer占据的列数
     * @param holder holder
     */
    @Override
    public void onViewAttachedToWindow(VH holder) {
        super.onViewAttachedToWindow(holder);
        final ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
        if (layoutParams != null && layoutParams instanceof StaggeredGridLayoutManager.LayoutParams) {
            StaggeredGridLayoutManager.LayoutParams params = (StaggeredGridLayoutManager.LayoutParams) layoutParams;
            params.setFullSpan(isHeader(holder.getLayoutPosition())
                    || isFooter(holder.getLayoutPosition()));
        }
    }
    public void setOnLoadMoreListener(RecyclerView recyclerView, final onLoadMoreListener onLoadMoreListener){
        if (recyclerView!=null && onLoadMoreListener!=null){
            recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                    super.onScrollStateChanged(recyclerView, newState);
                    if (!loadMoreFooterView.isEnable){
                        return;
                    }
                    int lastPosition = -1;
                    if (newState == RecyclerView.SCROLL_STATE_IDLE){
                        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
                        if (layoutManager instanceof GridLayoutManager){
                            lastPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
                        }else if (layoutManager instanceof LinearLayoutManager){
                            lastPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
                        }else if (layoutManager instanceof StaggeredGridLayoutManager){
                            int[] lastPositions = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()];
                            ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(lastPositions);
                            lastPosition = findMax(lastPositions);
                        }
                        if (loadMoreFooterView.isNeedLoadMore(lastPosition,recyclerView.getLayoutManager().getItemCount())){
                            onLoadMoreListener.loadMore(loadMoreFooterView.currentPage);
                        }
//                        Log.i("listener","currentTOtal:"+getFooter(recyclerView).currentTotal+" lastposition:"
//                                +lastPosition+" loading:"+getFooter(recyclerView).loading+" hasMoreData:"+getFooter(recyclerView).hasMoreData);
                    }
                }
                @Override
                public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                    super.onScrolled(recyclerView, dx, dy);
                }
            });
        }
    }
    public interface onLoadMoreListener{
        void loadMore(int currentPage);
    }
    //找到数组中的最大值
    private static int findMax(int[] lastPositions) {
        int max = lastPositions[0];
        for (int value : lastPositions) {
            if (value > max) {
                max = value;
            }
        }
        return max;
    }
}

BaseFooter.java

public abstract class BaseFooter {
    public boolean loading = false;
    public boolean hasMoreData = true;
    public int currentTotal = 0;
    public int currentPage = 1;
    public boolean isEnable = true;
    public abstract View getContainerView();
    protected abstract void setNoMoreDataView();
    protected abstract void setHasMoreDataView();
    protected abstract void setLoadMoreErrorView();
    protected abstract void hideLoadMoreView();
    protected abstract void showLoadMoreView();
    public void setState(int newDataSize,int currentPage){
        if (!checkIsEnable()){
            return;
        }
        if (currentPage == 1){
            resetState();
            isNoMoreData(newDataSize);
        }else{
            if (isNoMoreData(newDataSize)){
            } else {
                hasMoreData = true;
                loading = false;
                setHasMoreDataView();
                hideLoadMoreView();
            }
        }
    }
    public boolean isNeedLoadMore(int lastPosition,int currentTotal){
        if (!checkIsEnable()){
            return false;
        }
        this.currentTotal = currentTotal;
        if (lastPosition == currentTotal-1 && hasMoreData){
            if (!loading){
                currentPage++;
                showLoadMoreView();
                loading = true;
                return true;
            }
        }
        return false;
    }
    public void setLoadingError(){
        if (!checkIsEnable()){
            return;
        }
        currentPage--;
        loading = false;
        setLoadMoreErrorView();
    }
    private boolean checkIsEnable(){
        if (!isEnable){
            hideLoadMoreView();
            return false;
        }else {
            return true;
        }
    }
    private void resetState(){
        loading = false;
        currentTotal = 0;
        currentPage = 1;
        hasMoreData = true;
        hideLoadMoreView();
        setHasMoreDataView();
    }
    private boolean isNoMoreData(int newDataSize){
        if (newDataSize < BaseRecyclerViewAdapter.PER_PAGE_SIZE){
            hasMoreData = false;
            loading = false;
            setNoMoreDataView();
            if (newDataSize>0){
                showLoadMoreView();
            }else {
                hideLoadMoreView();
            }
            return true;
        }
        return false;
    }
}

SimpleFooter.java

public class SimpleFooter extends BaseFooter {
    private TextView loadMoreTv;
    private View containterView;
    public BaseFooter(Context context, ViewGroup parent) {
        initView(context,parent);
    }
    public void initView(Context context,ViewGroup parent) {
        containterView = LayoutInflater.from(context).inflate(R.layout.listitem_loadmore,parent,false);
        loadMoreTv = (TextView) containterView.findViewById(R.id.load_more_tv);
        containterView.setVisibility(View.GONE);
    }
    @Override
    public View getContainerView() {
        return containterView;
    }
    @Override
    public void setNoMoreDataView() {
        loadMoreTv.setText("没有更多了");
    }
    @Override
    public void setHasMoreDataView() {
        loadMoreTv.setText("加载更多...");
    }
    @Override
    public void setLoadMoreErrorView() {
        loadMoreTv.setText("加载失败");
    }
    @Override
    public void hideLoadMoreView(){
        containterView.setVisibility(View.GONE);
    }
    @Override
    public void showLoadMoreView(){
        containterView.setVisibility(View.VISIBLE);
    }
}

RepositoriesAdapter.java

public class RepositoriesAdapter extends BaseRecyclerViewAdapter {
    public RepositoriesAdapter(RecyclerView recyclerView, List mData) {
        super(recyclerView,mData);
    }
    @Override
    public void bindDataToItemView(RecyclerViewHolder recyclerViewHolder, RepositoryInfo repositoryInfo) {
        ((TextView)recyclerViewHolder.getView(R.id.repository_name)).setText(TextUtils.isEmpty(repositoryInfo.fullName)?"":repositoryInfo.fullName);
        ((TextView)recyclerViewHolder.getView(R.id.language_type)).setText(TextUtils.isEmpty(repositoryInfo.language)?"":repositoryInfo.language);
        ((TextView)recyclerViewHolder.getView(R.id.repository_desc)).setText(TextUtils.isEmpty(repositoryInfo.description)?"":repositoryInfo.description);
        ((TextView)recyclerViewHolder.getView(R.id.stars_tv)).setText(repositoryInfo.starsCount+"");
        ((TextView)recyclerViewHolder.getView(R.id.forks_tv)).setText(repositoryInfo.forksCount+"");
        ((TextView)recyclerViewHolder.getView(R.id.update_time)).setText("Updated on "+repositoryInfo.updatedAt.substring(0,10)+"");
        ImageUitl.loadUriPic(repositoryInfo.owner.avatar_url, (SimpleDraweeView) recyclerViewHolder.getView(R.id.repository_owner_im));
    }
    @Override
    public int getAdapterLayout() {
        return R.layout.listitem_repositories;
    }
}