最近准备打造一款 material design 风格的 github 的 Android 客户端,在实现搜索仓库列表界面时,涉及到列表肯定要下拉刷新,上拉加载更多,以前项目中用到的都是一个开源项目 Android-PullToRefresh ,但是这个仓库介绍中发现以及标记为 DEPRECATED 了 ,这个框架对于一般的需求确实够用了,但是不支持 RecyclerView ,这个开源框架的痛点就是如果要支持某一个控件的下拉刷新功能,就需要对这个控件进行适配修改,在控件这么多的时代,如果要全部支持那是一个巨大工作量,因为这个框架不是通用性的,估计这也是这个框架的开发者将之废弃的原因吧。
那么,什么样的下拉刷新,上拉更多框架是我所想要的呢?我所想要的大概具有以下特点:
首先推荐一个我最喜欢的下拉刷新的框架 android-Ultra-Pull-To-Refresh ,以前自己写的一个项目 Android 通用下拉刷新 就是按照这个框架的思想去实现的。这个框架的优点是它的下拉刷新跟内部的子控件没有耦合,类似 Google 官方推出的那个下拉刷新的思想,但是谷歌官方的那个做得确实不怎么好看所以不喜欢用。这个下拉刷新框架几乎支持所有的控件,确实很强大。但是被问及为什么不支持上拉加载更多的功能时,原作者表示这两个不应该属于同一个层次,上拉加载更多应该由子控件自己去做,既然作者都明确表示不支持上拉加载更多,那只好自己去扩展了。
当然下拉刷新你也可以使用其他的如 Google 推出的下拉刷新框架,不影响的。
看到网上很多人在这个下拉刷新的框架上是这样扩展上拉加载的,就是将原来的下拉刷新的逻辑倒过来用在上拉加载上,这个一开始我觉得还是可行的并且比较统一也很通用的,但是后来细想以后就发现还是有局限性,比如我的子控件是一些不规则的列表比如瀑布流或者横向列表,那么按照这个逻辑去实现的话就不能满足我们的需求,所以我也赞同做一个通用的框架下拉刷新和上拉加载更多不应该是在同一个层次去实现。还是由自控件自己去做通用的上拉加载。
RecyclerView 这一个控件出来已经有很长时间了,但是很多老项目还是使用的是 ListView,毕竟 ListView 已经基本满足日常使用的需求了,并且迁移成本太大。另外,一个控件越强大自由意味着需要的定制化就越多。先说以下 RecyclerView 和 ListView 主要的一些不同点吧:
因为主要在下拉刷新框架上进行扩展,所以也就放在了那个框架中,主要新加了两个类:
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); } });
这就是一个简单的使用方法,其中暴露出来的
关于 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; } }
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; } }
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); } }
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; } }