转载

02-15 RecyclerView 优雅封装

最近准备打造一款 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
    publicvoidonRefreshBegin(PtrFrameLayout frame){
        frame.postDelayed(new Runnable() {
            @Override
            publicvoidrun(){
                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<RepositoryInfo>());
recyclerView.setAdapter(mAdapter);
mAdapter.setOnLoadMoreListener(recyclerView, new BaseRecyclerViewAdapter.onLoadMoreListener() {
    @Override
    publicvoidloadMore(intcurrentPage){
        searchRepository(currentPage);
    }
});

mAdapter.setOnItemClickListener(new BaseRecyclerViewAdapter.OnItemClickListener() {
    @Override
    publicvoidonItemClick(View view,intposition,longid){
        showToastShort("你点击的是第"+position+"个位置"+" id="+id);
    }
});

GithubApi.searchRepositiories(query.toString(), "stars", "desc",currentPage,new IDataCallback<ReponseRepositories>() {
    @Override
    publicvoidonSuccess(ReponseRepositories object, Headers headers){
        if (object!=null&&object.getRepositoryList()!=null){
                mAdapter.addListData(object.getRepositoryList(),currentPage);
        }
    }

    @Override
    publicvoidonError(intcode, 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 classBaseRecyclerViewAdapter<T,VHextendsBaseRecyclerViewAdapter.RecyclerViewHolder>extendsRecyclerView.Adapter<VH>{


    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<T> multiItemType;

    private OnItemClickListener mOnItemClickListener;

    protected ILoadMore loadMoreFooterView;

    protected RecyclerView mRecyclerView;

    private List<T> mData;
    private Context mContext;

    private SparseArray<View> mHeaderViews = new SparseArray<>();
    private SparseArray<View> mFooterViews = new SparseArray<>();


    publicBaseRecyclerViewAdapter(RecyclerView recyclerView,List<T> mData){
        this.mData = mData;
        this.mContext = recyclerView.getContext();
        this.mRecyclerView = recyclerView;
        initDefaultLoadMoreView();
    }

    privatevoidinitDefaultLoadMoreView(){
        loadMoreFooterView = new BaseFooter(mContext,mRecyclerView);
    }

    publicabstractvoidbindDataToItemView(VH vh, T item);

    publicabstractintgetAdapterLayout();

    publicvoidsetOnItemClickListener(OnItemClickListener onItemClickListener){
        this.mOnItemClickListener = onItemClickListener;
    }

    publicvoidaddHeaderView(View headerView){
        mHeaderViews.put(mHeaderViews.size()+ITEM_TYPE_HEADER,headerView);
    }

    publicvoidaddFooterView(View footerView){
        mFooterViews.put(mFooterViews.size()+ITEM_TYPE_FOOTER,footerView);
    }

    /**
     * 这里直接使用RecyclerViewHolder,子类也直接使用这个ViewHolder,如果子类要自定义ViewHolder,这里的方法需要延迟到子类去加载。
     * @param v
     * @return
     */
    publicVHcreateViewHolder(View v){
        return (VH) new RecyclerViewHolder(v);
    }

    @Override
    publicVHonCreateViewHolder(ViewGroup parent,intviewType){
// 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(mHeaderViews.get(viewType));
        }else if (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
    publicintgetItemViewType(intposition){
        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);
        }
    }

    publicILoadMoregetFooter(){
        return loadMoreFooterView;
    }

    privateintgetRealDataPosition(intmixedPosition){
        return mixedPosition-mHeaderViews.size();
    }

    privatebooleanisHeader(intposition){
        return mHeaderViews.size() > 0 && position < mHeaderViews.size();
    }

    privatebooleanisFooter(intposition){
        return mFooterViews.size() > 0 && position >= getItemCount() - mFooterViews.size()- COUNT_FIXED_ITEM;
    }

    @Override
    publicvoidonBindViewHolder(finalVH holder,intposition){
        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
                publicvoidonClick(View v){
                    int position = holder.getAdapterPosition();
                    mOnItemClickListener.onItemClick(v,position,getRealDataPosition(position));
                }
            });
        }
    }

    @Override
    publicintgetItemCount(){
        return mData.size()+ COUNT_FIXED_ITEM +mHeaderViews.size()+mFooterViews.size();
    }

    publicTgetItem(intposition){
        return mData.get(position);
    }

    publicvoidaddListData(T data){
        if (mData == null){
            mData = new ArrayList<>();
            mData.add(data);
        }else {
            mData.add(data);
        }
        notifyDataSetChanged();
    }

    publicvoidaddListData(List<T> data){
        addListData(data,-1);
    }

    /**
     *
     * @param data
     * @param currentPage 为1的话刷新加载更多的状态,否则的话不管
     */
    publicvoidaddListData(List<T> data,intcurrentPage){
        if (mData == null){
            mData = data;
        }else {
            if (currentPage == 1){
                mData.clear();
            }
            mData.addAll(data);
        }
        if (loadMoreFooterView!=null){
            loadMoreFooterView.setState(data.size(),currentPage);
        }
        notifyDataSetChanged();
    }

    publicvoidloadMoreError(){
        if (loadMoreFooterView!=null){
            loadMoreFooterView.setLoadingError();
        }
    }


    /**
     * 是否禁用加载更多
     * @param enable
     */
    publicvoidenableLoadMoreView(booleanenable){
        loadMoreFooterView.isEnable = enable;
    }

    public static classRecyclerViewHolderextendsRecyclerView.ViewHolder{

        private final SparseArray<View> views;

        publicRecyclerViewHolder(View itemView){
            super(itemView);
            views = new SparseArray<>();
        }

        public <T extends View> TgetView(intid){
            View view = views.get(id);
            if (view == null){
                view = itemView.findViewById(id);
                views.put(id,view);
            }
            return (T) view;
        }
    }

    /**
     * add for multiItemType
     * @param <T>
     */
    public interfaceMultiItemType<T>{
        intgetLayoutId(intitemType);

        intgetItemViewType(intposition,T t);
    }



    public interfaceOnItemClickListener{
        voidonItemClick(View view,intposition,longid);
    }

    /**
     * 当LayoutManager是GridLayoutManager时,设置header和footer占据的列数
     * @param recyclerView recyclerView
     */
    @Override
    publicvoidonAttachedToRecyclerView(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
                publicintgetSpanSize(intposition){
                    return (isFooter(position) || isHeader(position))
                            ? gridManager.getSpanCount() : 1;
                }
            });
        }
    }

    /**
     * 当LayoutManager是StaggeredGridLayoutManager时,设置header和footer占据的列数
     * @param holder holder
     */
    @Override
    publicvoidonViewAttachedToWindow(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()));
        }
    }


    publicvoidsetOnLoadMoreListener(RecyclerView recyclerView,finalonLoadMoreListener onLoadMoreListener){
        if (recyclerView!=null && onLoadMoreListener!=null){
            recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
                @Override
                publicvoidonScrollStateChanged(RecyclerView recyclerView,intnewState){
                    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
                publicvoidonScrolled(RecyclerView recyclerView,intdx,intdy){
                    super.onScrolled(recyclerView, dx, dy);
                }
            });
        }
    }

    public interfaceonLoadMoreListener{
        voidloadMore(intcurrentPage);
    }

    //找到数组中的最大值
    privatestaticintfindMax(int[] lastPositions){
        int max = lastPositions[0];
        for (int value : lastPositions) {
            if (value > max) {
                max = value;
            }
        }
        return max;
    }

}

BaseFooter.java

public abstract classBaseFooter{

    public boolean loading = false;
    public boolean hasMoreData = true;
    public int currentTotal = 0;
    public int currentPage = 1;

    public boolean isEnable = true;


    publicabstractViewgetContainerView();
    protectedabstractvoidsetNoMoreDataView();
    protectedabstractvoidsetHasMoreDataView();
    protectedabstractvoidsetLoadMoreErrorView();
    protectedabstractvoidhideLoadMoreView();
    protectedabstractvoidshowLoadMoreView();

    publicvoidsetState(intnewDataSize,intcurrentPage){

        if (!checkIsEnable()){
            return;
        }
        if (currentPage == 1){
            resetState();
            isNoMoreData(newDataSize);
        }else{
            if (isNoMoreData(newDataSize)){
            } else {
                hasMoreData = true;
                loading = false;
                setHasMoreDataView();
                hideLoadMoreView();
            }
        }
    }

    publicbooleanisNeedLoadMore(intlastPosition,intcurrentTotal){

        if (!checkIsEnable()){
            return false;
        }

        this.currentTotal = currentTotal;
        if (lastPosition == currentTotal-1 && hasMoreData){
            if (!loading){
                currentPage++;
                showLoadMoreView();
                loading = true;
                return true;
            }
        }
        return false;

    }

    publicvoidsetLoadingError(){
        if (!checkIsEnable()){
            return;
        }
        currentPage--;
        loading = false;
        setLoadMoreErrorView();
    }

    privatebooleancheckIsEnable(){
        if (!isEnable){
            hideLoadMoreView();
            return false;
        }else {
            return true;
        }
    }

    privatevoidresetState(){
        loading = false;
        currentTotal = 0;
        currentPage = 1;
        hasMoreData = true;
        hideLoadMoreView();
        setHasMoreDataView();
    }

    privatebooleanisNoMoreData(intnewDataSize){
        if (newDataSize < BaseRecyclerViewAdapter.PER_PAGE_SIZE){
            hasMoreData = false;
            loading = false;
            setNoMoreDataView();
            if (newDataSize>0){
                showLoadMoreView();
            }else {
                hideLoadMoreView();
            }
            return true;
        }
        return false;
    }
}

SimpleFooter.java

public classSimpleFooterextendsBaseFooter{

    private TextView loadMoreTv;
    private View containterView;

    publicBaseFooter(Context context, ViewGroup parent){
        initView(context,parent);
    }

    publicvoidinitView(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
    publicViewgetContainerView(){
        return containterView;
    }

    @Override
    publicvoidsetNoMoreDataView(){
        loadMoreTv.setText("没有更多了");
    }

    @Override
    publicvoidsetHasMoreDataView(){
        loadMoreTv.setText("加载更多...");
    }

    @Override
    publicvoidsetLoadMoreErrorView(){
        loadMoreTv.setText("加载失败");
    }
    @Override
    publicvoidhideLoadMoreView(){
        containterView.setVisibility(View.GONE);
    }

    @Override
    publicvoidshowLoadMoreView(){
        containterView.setVisibility(View.VISIBLE);
    }

}

RepositoriesAdapter.java

public classRepositoriesAdapterextendsBaseRecyclerViewAdapter<RepositoryInfo,BaseRecyclerViewAdapter.RecyclerViewHolder>{


    publicRepositoriesAdapter(RecyclerView recyclerView, List<RepositoryInfo> mData){
        super(recyclerView,mData);
    }

    @Override
    publicvoidbindDataToItemView(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
    publicintgetAdapterLayout(){
        return R.layout.listitem_repositories;
    }
}
原文  http://zalezone.cn/2017/02/15/RecyclerView优雅封装/
正文到此结束
Loading...