在开始本篇阅读前,建议大家先看下上一篇《 打造自己的下拉刷新库(Ultra-Pull-To-Refresh)(一)》,主要是由于本篇很多接口和设计都在上一篇提到,在这里不会做过多展开。
在本系列的上一篇文章中,我们为大家分析了整个下拉刷新库的结构,其中最关键的就是我们将Ultra-PTR封装到了PullToRefreshBaseView基类中,为我们给各种view实现下拉刷新提供了便利的接入。那么今天我们继续给大家呈上PullToRefreshRecyclerView的打造过程,继承PullToRefreshBaseView基类轻松地为RecyclerView实现下拉刷新的功能。
由于之前业务上的需求,PullToRefreshRecyclerView目前只支持LinearLayoutManager的布局方式,也就是说用RecyclerView实现ListView的模式。在后续有时间会考虑接入阿里前段时间开源的VLayout,也能非常轻松的实现各种样式的RecyclerView。
这章节的PullToRefreshRecyclerView,主要实现下拉刷新、上拉加载、数据自动装载刷新、封装统一的adapter、模拟ListView的简单分割线这几项功能。
开始
首先我们继承PullToRefreshBaseView基类创建 一个PullToRefreshRecyclerView,实现onInitContent方法,在其中返回我们要实现的内部容器RecyclerView。
@Override
public View onInitContent() {
mRecyclerView = new RecyclerView(getContext());
mRecyclerView.setLayoutParams(new RecyclerView.LayoutParams(-1, -1));
return mRecyclerView;
}
其次,我们初始化刷新的默认头部、底部,给RecyclerView配置LaytouManager,这样就完成了基本的封装。
private void initView() {
setDefaultLoadingHeaderView();
setDefaultLoadingFooterView();
setOnRefreshListener(this);
mLinearLayoutManager = new LinearLayoutManager(getContext());
mRecyclerView.setLayoutManager(mLinearLayoutManager);
mRecyclerView.setHasFixedSize(true); //确定每个item高度相同,提高性能
mRecyclerView.setAdapter(new EmptyRecyclerViewAdapter(getContext()));
}
其中setOnRefreshListener设置的是要实现下拉刷新和上拉加载两个监听。
EmptyRecyclerViewAdapter是继承RecyclerView.Adapter实现的一个空的adapter,因为在实际调用过程中,会产生一个警告:
“Recycler View..No adapter attached: skipping layout”
在网上查到的资料中显示,是因为网络请求的数据还没回来就调用了notifyDataSetChanged()导致的,这里只需要加个EmptyRecyclerViewAdapter即可解决。
好啦,写到这里,实际上我们已经实现了RecyclerView下拉刷新、上拉加载的简单封装。现在的PullToRefreshRecyclerView已经具备刷新的功能,通过实现onPullDownToRefresh方法,可以捕获下拉事件;通过实现onPullUpToRefresh方法,可以捕获上拉事件。直接操作mRecyclerView即可实现数据填充、增加list头部底部等。
当然我们的追求远不止那么简单,直接操作mRecyclerView显然不是我们的风格,所以我们进一步对mRecyclerView进行封装处理。
分割线Divider
设置分割线,我们希望能像下面这么简单地调个方法,即可设置item之间间隔的宽度和颜色。
mPullRefreshRecyclerView.setDivider(R.dimen.dp_07, R.color.default_dividing_line);
当然也支持自定义RecyclerView.ItemDecoration。
mPullRefreshRecyclerView.setDivider(mItemDecoration);
首先RecyclerView提供了RecyclerView.ItemDecoration抽象类给我们自定义分割线,实现onDraw方法,利用Canvas绘画即可。
public class PTRRecyclerViewDecoration extends RecyclerView.ItemDecoration {
private Drawable mDivider;
private int dividerHeight;
private int dividerWidth;
private int mOrientation;
public boolean isHadHeader = false;
public boolean isHadFooter = false;
public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
public PTRRecyclerViewDecoration(Context context, int orientation, Drawable drawable) {
this.mDivider = drawable;
this.dividerHeight = mDivider != null ? mDivider.getIntrinsicHeight() : 0;
this.dividerWidth = mDivider != null ? mDivider.getIntrinsicWidth() : 0;
setOrientation(orientation);
}
public PTRRecyclerViewDecoration(Context context, int orientation, Drawable drawable, int dividerHeight) {
this.mDivider = drawable;
this.dividerHeight = dividerHeight;
this.dividerWidth = mDivider != null ? mDivider.getIntrinsicWidth() : 0;
setOrientation(orientation);
}
//设置屏幕方向
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
throw new IllegalArgumentException("invalid orientation");
}
mOrientation = orientation;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (parent.getChildCount() > 2) {
if (mOrientation == HORIZONTAL_LIST) {
drawVerticalLine(c, parent, state);
} else {
drawHorizontalLine(c, parent, state);
}
}
}
//横向
public void drawHorizontalLine(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (parent.getAdapter() == null) {
return;
}
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount();
int dataEndPosition = parent.getAdapter().getItemCount();
for (int i = 1; i < childCount - 1; i++) {
if (mDivider == null) {
break;
}
View child = parent.getChildAt(i);
int position = parent.getChildAdapterPosition(child);
//获取child的布局信息
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int top = child.getBottom() + params.bottomMargin;
int bottom = top + dividerHeight;
//处理第一个HeaderView、最后一个FooterView分割线
if ((isHadHeader && position <= 1) || (isHadFooter && position == dataEndPosition - 1)) {
bottom = top;
}
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
//竖向
public void drawVerticalLine(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (parent.getAdapter() == null) {
return;
}
int top = parent.getPaddingTop();
int bottom = parent.getHeight() - parent.getPaddingBottom();
final int childCount = parent.getChildCount();
int dataEndPosition = parent.getAdapter().getItemCount();
for (int i = 1; i < childCount - 1; i++) {
if (mDivider == null) {
break;
}
View child = parent.getChildAt(i);
int position = parent.getChildAdapterPosition(child);
//获取child的布局信息
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int left = child.getRight() + params.rightMargin;
int right = left + mDivider.getIntrinsicWidth();
//处理第一个HeaderView、最后一个FooterView分割线
if ((isHadHeader && position <= 1) || (isHadFooter && position == dataEndPosition - 1)) {
right = left;
}
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
if (mOrientation == HORIZONTAL_LIST) {
outRect.set(0, 0, dividerWidth, 0);
} else {
outRect.set(0, 0, 0, dividerHeight);
}
}
}
核心主要还是绘图的代码,这里就不做展开,如果有一些复杂需求的分割线,可以网上搜搜,这方面的资料还是挺多的。
这里主要提一下两个关键的标记变量isHadHeader和isHadFooter。由于我们在下面要加入HeaderView和FooterView的支持,而我们在增加RecyclerView头部和底部的时候,都是不希望加入分割线的。所以这用两个标记变量isHadHeader和isHadFooter来判断,当前是否有加入头部/底部,从而“隐藏”分割线。
有了自定义的PTRRecyclerViewDecoration,我们就实现上面的setDivider方法了。
public void setDivider(int padding, int divider) {
if (padding > 0 && divider >= 0) {
Drawable _divider = divider != 0 ? getResources().getDrawable(divider) : null;
myDecoration = new PTRRecyclerViewDecoration(getContext(), PTRRecyclerViewDecoration.VERTICAL_LIST, _divider, (int) getResources().getDimension(padding));
mRecyclerView.addItemDecoration(myDecoration);
}
}
优雅地添加HeaderView和FooterView
RcyclerView本身是不提供添加HeaderView和FooterView方法的,需要使用RecyclerView.Adapter来实现。而如果我们直接在我们已经实现的adapter上修改,增加头部和底部,这样我们需要为每个adapter加入同样的代码,显然不符合我们的封装思想。
这一节我们参考了鸿洋大神的下面这篇文章。
采用装饰者模式的思想,给adapter包装一层,专门负责管理头部、底部的添加和删除。
/**
* HeaderAndFooterWrapper .java
*/
public class HeaderAndFooterWrapper extends RecyclerView.Adapter{
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> mFooterViews = new SparseArrayCompat<>();
private RecyclerView.Adapter mInnerAdapter;
public HeaderAndFooterWrapper(RecyclerView.Adapter adapter) {
mInnerAdapter = adapter;
}
private boolean isHeaderViewPos(int position) {
return position < getHeadersCount();
}
private boolean isFooterViewPos(int position) {
return position >= getHeadersCount() + getRealItemCount();
}
public void addHeaderView(View view) {
mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view);
}
public void addFooterView(View view) {
mFooterViews.put(mFooterViews.size() + BASE_ITEM_TYPE_FOOTER, view);
}
public void addHeaderView(List<View> view) {
for (int i = 0; i < view.size(); i++) {
addHeaderView(view.get(i));
}
}
public void addFooterView(List<View> view) {
for (int i = 0; i < view.size(); i++) {
addFooterView(view.get(i));
}
}
public void removeFooterView(View view) {
int idx = mFooterViews.indexOfValue(view);
if (idx != -1) {
mFooterViews.removeAt(idx);
}
}
public int getHeadersCount() {
return mHeaderViews.size();
}
public int getFootersCount() {
return mFooterViews.size();
}
private int getRealItemCount()
{
return mInnerAdapter.getItemCount();
}
@Override
public int getItemViewType(int position) {
if (isHeaderViewPos(position)) {
return mHeaderViews.keyAt(position);
}else if (isFooterViewPos(position)) {
return mFooterViews.keyAt(position - getHeadersCount() - getRealItemCount());
}
return mInnerAdapter.getItemViewType(position - getHeadersCount());
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (mHeaderViews.get(viewType) != null) {
View headerView = mHeaderViews.get(viewType);
headerView.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT));
CommonViewHolder myViewHolder = new CommonViewHolder(headerView);
return myViewHolder;
}else if (mFooterViews.get(viewType) != null) {
View footerView = mFooterViews.get(viewType);
footerView.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT));
CommonViewHolder myViewHolder = new CommonViewHolder(footerView);
return myViewHolder;
}
return mInnerAdapter.onCreateViewHolder(parent, viewType);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (isHeaderViewPos(position)) {
return;
}else if (isFooterViewPos(position)) {
return;
}
mInnerAdapter.onBindViewHolder(holder, position - getHeadersCount());
}
@Override
public int getItemCount() {
return getHeadersCount() + getFootersCount() + getRealItemCount();
}
}
有了HeaderAndFooterWrapper这个包装类,我们就可以增加一些方法,来管理PullToRefreshRecyclerView拥有的HeaderView和FooterView。
private List<View> mHeaderViewList;
private List<View> mFooterViewList;
public void addHeaderView(View headerView) {
if (mHeaderViewList == null){
mHeaderViewList = new ArrayList<>();
}
mHeaderViewList.add(headerView);
//指定分割线标记,当前拥有头部
if(myDecoration != null) {
myDecoration.isHadHeader = true;
}
}
/**
* 增加FooterView
*/
public void addFooterView(View footerView) {
if (mFooterViewList == null){
mFooterViewList = new ArrayList<>();
}
mFooterViewList.add(footerView);
//指定分割线标记,当前拥有底部
if(myDecoration != null) {
myDecoration.isHadFooter = true;
}
RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
if (adapter != null && !(adapter instanceof EmptyRecyclerViewAdapter)) {
if (adapter instanceof HeaderAndFooterWrapper) {
((HeaderAndFooterWrapper) adapter).addFooterView(footerView);
adapter.notifyDataSetChanged();
}else {
HeaderAndFooterWrapper headerAndFooterWrapper = new HeaderAndFooterWrapper(adapter);
headerAndFooterWrapper.addFooterView(footerView);
mRecyclerView.setAdapter(headerAndFooterWrapper);
}
}
}
/**
* 移除FooterView
*/
public void removeFooterView(View footerView) {
RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
if (adapter != null && (adapter instanceof HeaderAndFooterWrapper)) {
((HeaderAndFooterWrapper) adapter).removeFooterView(footerView);
if (mFooterViewList != null && mFooterViewList.indexOf(footerView) != -1) {
mFooterViewList.remove(footerView);
}
}
}
/**
* 包装adapter,增加HeaderView,FooterView
*/
private RecyclerView.Adapter getWrappedListAdapter(RecyclerView.Adapter adapter) {
if ((mHeaderViewList != null && mHeaderViewList.size() != 0) || (mFooterViewList != null && mFooterViewList.size() != 0)) {
HeaderAndFooterWrapper headerAndFooterWrapper = new HeaderAndFooterWrapper(adapter);
//增加HeaderView
if (mHeaderViewList != null && mHeaderViewList.size() != 0) {
headerAndFooterWrapper.addHeaderView(mHeaderViewList);
}
//增加FooterView
if (mFooterViewList != null && mFooterViewList.size() != 0) {
headerAndFooterWrapper.addFooterView(mFooterViewList);
}
return headerAndFooterWrapper;
}
return adapter;
}
一键式数据装载与Item布局
上一篇文章中,我们提到了OnPullListActionListener接口,提供数据加载、item点击、item初始化、刷新完成的回调方法。主要用于在封装统一adapter的时候,使界面只需要关系接口请求和界面布局,实现一键式的数据装载与item布局指定。
/**
* OnPullListActionListener .java
*/
public interface OnPullListActionListener<T> {
void loadData(int pageIndex, String tips);
void clickItem(T item, int position);
void createListItem(ViewHolder holder, T currentItem, List<T> list, int position);
void onRefreshComplete();
}
- loadData:发起获取数据请求
- clickItem:item点击事件
- createListItem:初始化item布局,其中ViewHolder是统一View控制器,避免要定义一系列view的变量;currentItem是当前item的数据
- onRefreshComplete:加载完成事件
/**
* ViewHolder.java
*/
public class ViewHolder {
private final SparseArray<View> mViews;
private View mConvertView;
private OnClickListener mOnClickListener;
public ViewHolder(View parent) {
mConvertView = parent;
mViews = new SparseArray<View>();
}
public ViewHolder(View parent, OnClickListener clickListener) {
mOnClickListener = clickListener;
mConvertView = parent;
mViews = new SparseArray<View>();
}
private ViewHolder(Context context, ViewGroup parent, int layoutId) {
this.mViews = new SparseArray<View>();
mConvertView = LayoutInflater.from(context).inflate(layoutId, parent, false);
mConvertView.setTag(this);
}
public static ViewHolder get(Context context, View convertView,
ViewGroup parent, int layoutId, int position) {
if (convertView == null) {
return new ViewHolder(context, parent, layoutId);
}
return (ViewHolder) convertView.getTag();
}
public View getConvertView() {
return mConvertView;
}
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 setOnClickListener(int viewId) {
View view = getView(viewId);
setClickListener(view);
return view;
}
public TextView setText(int viewId, CharSequence text) {
TextView view = getView(viewId);
if (view != null) {
view.setText(text);
}
return view;
}
public TextView setTextColor(int viewId, int Color) {
TextView view = getView(viewId);
if (view != null) {
view.setTextColor(Color);
}
return view;
}
public ImageView setImageResource(int viewId, int drawableId) {
ImageView view = getView(viewId);
if (view != null) {
view.setImageResource(drawableId);
}
return view;
}
public View setBackgroundResource(int viewId, int drawableId) {
View view = getView(viewId);
if (view != null) {
view.setBackgroundResource(drawableId);
}
return view;
}
public View setVisibility(int viewId, int visibility) {
View view = getView(viewId);
view.setVisibility(visibility);
return view;
}
public int getVisibility(int viewId) {
View view = getView(viewId);
return view.getVisibility();
}
public void setClickListener(View view) {
if (mOnClickListener != null && view != null) {
view.setOnClickListener(mOnClickListener);
}
}
public void setClickListener(OnClickListener clickListener) {
mOnClickListener = clickListener;
}
}
我们有了ViewHolder对View的统一控制,就可以在adapter中使用起来,继承RecyclerView.Adapter我们可以创建一个公共的CommonBaseAdapter,封装CommonViewHolder用来包装上面的ViewHolder,即可实现一个通用的Adapter,而不需要自己每次单独实现RecyclerView.ViewHolder。
/**
* CommonViewHolder.java
* 这部分代码非常简单,就封装了一个上面的ViewHolder
*/
public class CommonViewHolder extends RecyclerView.ViewHolder {
public CommonViewHolder(View view) {
super(view);
}
ViewHolder viewHolder;
}
公共的CommonBaseAdapter。
/**
* CommonBaseAdapter.java
*/
public abstract class CommonBaseAdapter<T> extends RecyclerView.Adapter<CommonViewHolder>
{
private Context mContext;
private List<T> mData;
protected final int mItemLayoutId;
public CommonBaseAdapter(Context context, List<T> mData, int itemLayoutId)
{
this.mContext = context;
this.mItemLayoutId = itemLayoutId;
this.mData = mData;
}
@Override
public CommonViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewHolder viewHolder = getViewHolder(0, null, parent);
CommonViewHolder myViewHolder = new CommonViewHolder(viewHolder.getConvertView());
myViewHolder.viewHolder = viewHolder;
return myViewHolder;
}
@Override
public void onBindViewHolder(final CommonViewHolder holder, final int position) {
holder.itemView.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
onItemClick(holder.itemView, position);
}
});
convert(holder.viewHolder, mData.get(position), mData, position);
}
private ViewHolder getViewHolder(int position, View convertView, ViewGroup parent) {
return ViewHolder.get(mContext, convertView, parent, mItemLayoutId, position);
}
protected abstract void convert(ViewHolder holder, T item,List<T> list,int position);
protected abstract void onItemClick(View itemView, int position);
@Override
public int getItemCount() {
return mData.size();
}
}
这样,我们可以在PullToRefreshRecyclerView创建一个内部类MyListAdapter,用来实现CommonBaseAdapter,从而将adapter内的创建、点击事件通过OnPullListActionListener传递到上层。
private class MyListAdapter extends CommonBaseAdapter<T> {
public MyListAdapter(Context context, List<T> mData, int itemLayoutId) {
super(context, mData, itemLayoutId);
}
@Override
protected void onItemClick(View itemView, int position) {
if (position >= 0 && mList.size() > 0) {
T item = mList.get(position);
if (mOnPullListActionListener != null && item != null) {
int numHeaderView = mHeaderViewList != null ? mHeaderViewList.size() : 0;
mOnPullListActionListener.clickItem(item, position + numHeaderView);
}
}
}
@Override
protected void convert(ViewHolder holder, T item, List<T> list, int position) {
if (mOnPullListActionListener != null && item != null) {
mOnPullListActionListener.createListItem(holder, item, list, position);
}
}
}
这里,我们先简单回顾一下,看看我们上面都实现了哪些功能。
- 添加分割线:setDivider(int padding, int divider)
- 添加HeaderView:addHeaderView(View headerView),
- 添加FooerView:addFooterView(View footerView)
- 移除FooterView:removeFooterView(View footerView)
- 初始化Item布局:通过接口OnPullListActionListener的createListItem,可以拿到ViewHolder,轻松实现布局和填充item的数据
- Item点击:同样通过接口OnPullListActionListener的clickItem,可以捕获Item点击事件
一切似乎都已经非常强大了,但貌似还漏了些什么。没错万事具备,只欠东风,我们还缺少了最关键的加载数据,和数据展示。不急,马上为您呈上!
OnPullListActionListener接口还有一个关键的loadData()方法,这当然就是用来为我们加载数据调用的。
/**
* 下拉刷新加载数据
*/
public void loadRefreshData(boolean isShowTops) {
String tips = isShowTops ? TIPS_LOAD_DATA : "";
mPageIndex = 1;
if (mOnPullListActionListener != null) {
mOnPullListActionListener.loadData(mPageIndex, tips);
}
}
/**
* 上拉刷新加载更多数据
*/
public void loadMoreData(int taskId, boolean isShowTops) {
String tips = isShowTops ? TIPS_LOAD_DATA : "";
if (mOnPullListActionListener != null) {
mOnPullListActionListener.loadData(mPageIndex, tips);
}
}
在上面我们已经实现了对adapter的一个包装类MyListAdapter,因此这里可以很简单的实现数据的装载与刷新。
/**
* 显示数据
* 传入数据数组list,和指定的item布局itemLayoutId
*/
public void showAllData(List<T> list, int itemLayoutId) {
if (commonBaseAdapter == null) {
commonBaseAdapter = new MyListAdapter(getContext(), list, itemLayoutId);
mRecyclerView.setAdapter(getWrappedListAdapter(commonBaseAdapter));
} else {
getAdapter().notifyDataSetChanged();
}
}
写在最后
到这里,我们就将PullToRefreshRecyclerView封装RecyclerView的打造过程,完整地分析给了大家。完整的源码这里就不再贴出来了,我们在上一篇已经贴给大家了。
其实封装PullToRefreshRecyclerView的时候,更多是从我们项目的需求出发,所以我们暂时只实现了LinearLayoutManager列表式布局,在后面如果有时间,我打算接入阿里的VLayout,这样就能实现各种样式的下拉刷新RecyclerView。这里大家如果有兴趣,也可以继承PullToRefreshBaseView尝试自己实现一下,欢迎一起交流,共同学习!