- 鸿洋大神为RecyclerView打造通用Adapter让RecyclerView更加好用
- 鸿洋大神Android优雅的为RecyclerView添加HeaderView和FooterView
之前使用RecyclerView.Adapter,基本就类似套用公式,死步骤,对Adapter感到既熟悉又陌生。从去年我开始接触学习Android之时,RecyclerView已经开始大量被运用,逐步取代ListView。遂,正好,那就先直接学习RecyclerView.Adapter相关知识
1. RecyclerView.Adapter适配器
RecyclerView.Adapter,一个抽象类,并支持泛型
public static abstract class Adapter<VH extends ViewHolder> {...
}
定义一个MyRecyclerViewAdapter继承RecyclerView.Adapter后,Android Stuido提醒需要重写3个方法,在重写3个方法前,一般会先定义一个Holder继承RecycelrView.ViewHolder,之后直接在MyRecyclerViewAdapter上,指定泛型就是RecyclerHolder
3个需要必须重写的方法:
方法1:public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)方法2:public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)方法3:public int getItemCount()
在指定了泛型为RecyclerHoler后,方法2也会根据泛型改变onBindViewHolder(RecyclerHolder holder, int position)
1.1 onCreateViewHolder(ViewGroup parent, int viewType)创建Holder
源码:
/*** Called when RecyclerView needs a new {@link ViewHolder} of the given type to represent an item.* * @param parent The ViewGroup into which the new View will be added after it is bound to an adapter position.* @param viewType The view type of the new View.** @return A new ViewHolder that holds a View of the given view type.*/
public abstract VH onCreateViewHolder(ViewGroup parent, int viewType);
- ViewGroup parent:可以简单理解为
item的根ViewGroup,item的子控件加载在其中 - int viewType:
item的类型,可以根据viewType来创建不同的ViewHolder,来加载不同的类型的item
这个方法就是用来创建出一个新的ViewHolder,可以根据需求的itemType,创建出多个ViewHolder。创建多个itemType时,需要getItemViewType(int position)方法配合
1.2 onBindViewHolder(RecyclerHolder holder, int position)绑定ViewHolder
源码:
**
*Called by RecyclerView to display the data at the specified position.
*This method should update the contents of the {@link ViewHolder#itemView} to reflect the item at the given position.
*
*@param holder The ViewHolder which should be updated to represent the contents of the item at the given position in the data set.
*@param position The position of the item within the adapter's data set.
*/public abstract void onBindViewHolder(VH holder, int position);
- VH holder:就是在
onCreateViewHolder()方法中,创建的ViewHolder - int position:
item对应的DataList数据源集合的postion
postion就是adapter position,RecycelrView中item的数量,就是根据DataList数据源集合的数量来创建的
1.3 getItemCount()获取Item的数目
源码:
/*** Returns the total number of items in the data set held by the adapter.** @return The total number of items in this adapter.*/
public abstract int getItemCount();
这个方法的返回值,便是RecyclerView中实际item的数量。有些情况下,当增加了HeaderView或者FooterView后,需要注意考虑这个返回值
1.4 简单实用
一个最简单的RecyclerViewAdapter
public class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.RecyclerHolder> {private Context mContext;private List<String> dataList = new ArrayList<>();public MyRecyclerViewAdapter(RecyclerView recyclerView) {this.mContext = recyclerView.getContext();}public void setData(List<String> dataList) {if (null != dataList) {this.dataList.clear();this.dataList.addAll(dataList);notifyDataSetChanged();}}@Overridepublic RecyclerHolder onCreateViewHolder(ViewGroup parent, int viewType) {View view = LayoutInflater.from(mContext).inflate(R.layout.id_rv_item_layout, parent, false);return new RecyclerHolder(view);}@Overridepublic void onBindViewHolder(RecyclerHolder holder, int position) {holder.textView.setText(dataList.get(position));}@Overridepublic int getItemCount() {return dataList.size();}class RecyclerHolder extends RecyclerView.ViewHolder {TextView textView;private RecyclerHolder(View itemView) {super(itemView);textView = (TextView) itemView.findViewById(R.id.tv__id_item_layout);}}
}
我的个人习惯是单独使用一个setData()方法将DataList传递进Adapter,看到网上有一些博客中会通过构造方法传递。我一般会在网络请求前就初始化Adapter,当异步网络请求拿到解析过的JSON数据后,调用这个方法将数据加载进Adapter,即使做了分页,也可以比较方。但感觉这种方法终究会浪费一点性能
注意,在 onCreateViewHolder()方法中:
View view = LayoutInflater.from(mContext).inflate(R.layout.id_rv_item_layout, parent, false);
inflate()方法使用的是3个参数的方法。
1.4.1 问题
以前使用2个参数的方法inflate(@LayoutRes int resource, @Nullable ViewGroup root),下面的形式
View view = LayoutInflater.from(mContext).inflate(R.layout.id_rv_item_layout,null);
遇到的一个问题
两种方法,使用的是同一套布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><TextViewandroid:id="@+id/tv__id_item_layout"android:layout_width="match_parent"android:layout_height="wrap_content"android:background="@color/colorAccent"android:textAllCaps="false"android:textColor="@android:color/white"android:textSize="20sp" /></LinearLayout>
item内的TextView的宽是match_parent,但使用两个参数的方法时,看起来却是wrap_content的效果
1.4.2尝试从源码中找问题
两个参数的方法源码
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {return inflate(resource, root, root != null);
}
两个参数的方法内部调用了3个参数的方法,此时inflate(resourceId, null, false),root为null,attachToRoot为false
最终来到了这里,只保留了部分代码:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {...View result = root;...// Temp is the root view that was found in the xmlfinal View temp = createViewFromTag(root, name, inflaterContext, attrs);ViewGroup.LayoutParams params = null;...if (root != null) {...params = root.generateLayoutParams(attrs);... }...rInflateChildren(parser, temp, attrs, true);...if (root == null || !attachToRoot) {result = temp;}}
使用两个参数的inflate()方法,ViewGroup.LayoutParams params最终为null;而如果使用3个参数的方法,最终params = params = root.generateLayoutParams(attrs)
这里为了减少出现问题的出现,就使用3个参数的方法inflate(R.layout.id_rv_item_layout, parent, false)
这里看源码也就看了这小段一段,inflate()方法的完整过程还是比较复杂的,比较浅显的知道问题出在哪里后,没有深挖
1.4 点击事件
完整代码:
public class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.RecyclerHolder> {private Context mContext;private List<String> dataList = new ArrayList<>();private onRecyclerItemClickerListener mListener;public MyRecyclerViewAdapter(RecyclerView recyclerView) {this.mContext = recyclerView.getContext();}/*** 增加点击监听*/public void setItemListener(onRecyclerItemClickerListener mListener) {this.mListener = mListener;}/*** 设置数据源*/public void setData(List<String> dataList) {if (null != dataList) {this.dataList.clear();this.dataList.addAll(dataList);notifyDataSetChanged();}}public List<String> getDataList() {return dataList;}@Overridepublic RecyclerHolder onCreateViewHolder(ViewGroup parent, int viewType) {View view = LayoutInflater.from(mContext).inflate(R.layout.id_rv_item_layout, parent, false);// View view = LayoutInflater.from(mContext).inflate(R.layout.id_rv_item_layout,null);return new RecyclerHolder(view);}@Overridepublic void onBindViewHolder(RecyclerHolder holder, int position) {holder.textView.setText(dataList.get(position));holder.textView.setOnClickListener(getOnClickListener(position));}private View.OnClickListener getOnClickListener(final int position) {return new View.OnClickListener() {@Overridepublic void onClick(View v) {if (null != mListener && null != v) {mListener.onRecyclerItemClick(v, dataList.get(position), position);}}};}@Overridepublic int getItemCount() {return dataList.size();}class RecyclerHolder extends RecyclerView.ViewHolder {TextView textView;private RecyclerHolder(View itemView) {super(itemView);textView = (TextView) itemView.findViewById(R.id.tv__id_item_layout);}}/*** 点击监听回调接口*/public interface onRecyclerItemClickerListener {void onRecyclerItemClick(View view, Object data, int position);}
}
定义一个接口onRecyclerItemClickerListener,这样可以在Actiivty设置监听对象,之后为TextView设置点击监听事件,在TextView的点击事件方法中,使用onRecyclerItemClick()进行回调
在Activity中使用:
//设置点击事件
adapter.setItemListener(new MyRecyclerViewAdapter.onRecyclerItemClickerListener() {@Overridepublic void onRecyclerItemClick(View view, Object data, int position) {String s = (String) data;adapter.getDataList().set(position, s + "---->hi");adapter.notifyItemChanged(position);}
});
在监控对象回调方法中,使用了notifyItemChanged(position)来进行局部刷新
但这种方式会new出一大堆View.OnClickListener,还有一种思路是利用RecycelrView的onTouchListener和GestureDetector手势来进行设置,可以看看三种方式实现RecyclerView的Item点击事件
1.5 一系列的notifyData方法
一共有10个方法
| 方法 | 作用 |
|---|---|
notifyDataSetChanged() | 通知RecycelrView进行全局刷新 |
notifyItemChanged(int position) | 通知RecycelrView在adapter position处局进行部刷新 |
notifyItemRemoved(int position) | 通知RecyclerView移除在adapter position处的item |
notifyItemMoved(int fromPosition, int toPosition) | 通知RecyclerView移除从fromPosition到toPosition的item |
notifyItemRangeRemoved(int positionStart, int itemCount) | 通知RecyclerView移除从positionStart开始的itemCount个item |
notifyItemChanged(int position, Object payload) | 通知RecyclerView改变指定position的item的object |
notifyItemRangeChanged(int positionStart,int itemCount) | 通知RecyclerView从positionStart开始改变itemCount个item |
notifyItemRangeChanged(int positionStart,int itemCount,Object payload) | 通知RecyclerView从positionStart开始改变itemCount个item的对象 |
notifyItemInserted(int position) | 通知RecyclerView在position处插入一个item |
notifyItemRangeInserted(int positionStart, int itemCount) | 通知RecyclerView从positionStart开始插入itemCount个item |
有些情况下,方法需要考虑组合使用,否则可能出现position错乱,例如
在Adapter中移除或者插入item
/*** 移除指定Position的Item*/
public void remove(int position) {if (dataList.size() == 0) return;dataList.remove(position);notifyItemRemoved(position);notifyItemRangeChanged(position, dataList.size() - position);
}//在Activity中使用 ,设置点击事件
adapter.setItemListener(new MyRecyclerViewAdapter.onRecyclerItemClickerListener() {@Overridepublic void onRecyclerItemClick(View view, Object data, int position) {adapter.remove(position);// String s = (String) data;// adapter.inserted(position,s+"---->hi");}
});//在指定位置插入一个item
public void inserted(int position, String s) {if (dataList.size() == 0) return;dataList.add(position, s);notifyItemInserted(position);notifyItemRangeChanged(position, dataList.size() - position);
}
notifyItemRemoved(position)虽然通知移除了RecycelrView在position位置上的itemA,但itemA之后的一系列item也需要进行改变,也需要通知RecyclerView进行改变
但这两个方法性能上都有问题,卡顿比较明显,应该会有更好的动态改变或者动态插入item的方法,以后学到了再补充
1.5 简易的封装
通用的ViewHolder:
public class BaseViewHolder extends RecyclerView.ViewHolder {private final SparseArray<View> sparseArray;public BaseViewHolder(View itemView) {super(itemView);this.sparseArray = new SparseArray<>(8); //一般一个Item 不会超过8种控件}public <T extends View> T getView(int viewId) {View view = sparseArray.get(viewId);if (view == null) {view = itemView.findViewById(viewId);sparseArray.put(viewId, view);}return (T) view;}public BaseViewHolder setText(int viewId, String text) {TextView tv = getView(viewId);if (tv != null) {tv.setText(text);}return this;}
}
主要思路就是使用SparseArray<View>将控件存起来
适配器:
public abstract class CommonBaseAdapter<T> extends RecyclerView.Adapter<BaseViewHolder> {protected List<T> data = new ArrayList<>();protected int itemLayoutId;protected Context mContext;private onRecyclerItemClickerListener mListener;public CommonBaseAdapter(RecyclerView rv, @LayoutRes int itemLayoutId) {this.itemLayoutId = itemLayoutId;this.mContext = rv.getContext();}public void setData(List<T> data) {if (data != null) {this.data.clear();this.data.addAll(data);notifyDataSetChanged();}}/*** 增加点击监听*/public void setItemListener(onRecyclerItemClickerListener mListener) {this.mListener = mListener;}@Overridepublic BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {//这里使用3个参数的方法View view = LayoutInflater.from(mContext).inflate(itemLayoutId, parent, false);return new BaseViewHolder(view);}@Overridepublic void onBindViewHolder(BaseViewHolder holder, int position) {bindViewData(holder, data.get(position), position);holder.itemView.setOnClickListener(getOnClickListener(position));}private View.OnClickListener getOnClickListener(final int position) {return new View.OnClickListener() {@Overridepublic void onClick(View v) {if (mListener != null && v != null) {mListener.onRecyclerItemClick(v, data.get(position), position);}}};}@Overridepublic int getItemCount() {return this.data.size();}public abstract void bindViewData(BaseViewHolder holder, T item, int position);interface onRecyclerItemClickerListener {void onRecyclerItemClick(View view, Object data, int position);}
}
内部提供一个抽象方法bindViewData(),子类重写抽象方法来做一些具体的操作。
封装的很简单,但平常学习使用也能减少一些重复代码。网上有很多强大的封装,可以再深入学习一下为RecyclerView打造通用Adapter让RecyclerView更加好用
使用:
public class RecyclerViewAdapter extends CommonBaseAdapter<String> {public RecyclerViewAdapter(RecyclerView rv, @LayoutRes int itemLayoutId, @IdRes int resId) {super(rv, itemLayoutId);}@Overridepublic void bindViewData(BaseViewHolder holder, String item, int position) {holder.setText(R.id.textViewId, item);}
}
在Activity中就可以进行使用
这个简易的封装并没有对添加加载图片的方法。加载图片的方法一开始也我封装在了这个CommonBaseAdapter中,但后来发现直接封装在这里并不是好的思路
图片需要做的处理比较多,而且主流的库有3个,为了易于维护,还是将图片的操作单独再封装在一个工具类中,在CommonBaseAdapter中使用操作图片的工具类比较好
1.6 添加HeaderView和FooterViewiew
学的鸿洋大神的代码和思路,涉及到了装饰模式。还有一种添加方式是直接通过使用多种item直接在现有的CommonBaseAdapter来修改,但感觉这种思路需要对CommonBaseAdapter改动的代码太多,点击事件的position也需要考虑,不如鸿洋大神的这种思路易于开发和维护
代码:
public class HeaderAndFooterAdapter extends RecyclerView.Adapter<BaseViewHolder> {private CommonBaseAdapter mAdapter;private static final int HEADER_VIEW_TYPE = 2 << 6;private static final int FOOTER_VIEW_TYPE = 2 << 5;private SparseArrayCompat<View> mHeaderViews = new SparseArrayCompat<>();private SparseArrayCompat<View> mFooterViews = new SparseArrayCompat<>();public HeaderAndFooterAdapter(CommonBaseAdapter mAdapter) {this.mAdapter = mAdapter;}@Overridepublic BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {if (null != mHeaderViews.get(viewType)) {return new HeaderAndFooterHolder(mHeaderViews.get(viewType));} else if (null != mFooterViews.get(viewType)) {return new HeaderAndFooterHolder(mFooterViews.get(viewType));}return mAdapter.onCreateViewHolder(parent, viewType);}@Overridepublic void onBindViewHolder(BaseViewHolder holder, int position) {if (isHeaderViewPosition(position)) return;if (isFooterViewPosition(position)) return;mAdapter.onBindViewHolder(holder, position - getHeaderViewCount());}@Overridepublic int getItemViewType(int position) {if (isHeaderViewPosition(position)) {return mHeaderViews.keyAt(position);} else if (isFooterViewPosition(position)) {return mFooterViews.keyAt(position-getHeaderViewCount()-getAdapterItemCount());}return mAdapter.getItemViewType(position - getHeaderViewCount());}@Overridepublic int getItemCount() {return getHeaderViewCount() + getFooterViewCount() + getAdapterItemCount();}/*** 加入HeaderView*/public void addHeaderView(View view) {mHeaderViews.put(mHeaderViews.size() + HEADER_VIEW_TYPE, view);}/*** 加入FooterView*/public void addFootView(View view) {mFooterViews.put(mFooterViews.size() + FOOTER_VIEW_TYPE, view);}/*** HeaderView 的数目*/public int getHeaderViewCount() {return mHeaderViews.size();}/*** FooterView 的数目*/public int getFooterViewCount() {return mFooterViews.size();}/*** 是不是HeaderView的Position*/private boolean isHeaderViewPosition(int position) {return position < getHeaderViewCount();}/*** 是不是FooterView的Position*/private boolean isFooterViewPosition(int position) {return position >= getHeaderViewCount() + getAdapterItemCount();}/*** 得到Adapter中Item的数目*/private int getAdapterItemCount() {return mAdapter.getItemCount();}private class HeaderAndFooterHolder extends BaseViewHolder {private HeaderAndFooterHolder(View itemView) {super(itemView);}}
}
封装的思路:
将add进来的view进行保存,当加载item时,利用itemType对view进行类型判断,如果是HeaderView或者FooterView就创建HeaderAndFooterHolder,然后绑定只是用来显示并没有对HeaderView或者FooterView做其他更多事件的处理
使用也比较方便:
//数据适配器
RecyclerViewAdapter adapter = new RecyclerViewAdapter(rv, R.layout.id_rv_item_layout, R.id.tv__id_item_layout);
//头View适配器
HeaderAndFooterAdapter headerAndFooterAdapter = new HeaderAndFooterAdapter(adapter);
//HeaderView
TextView headerView = new TextView(this);
headerView.setBackgroundColor(Color.BLACK);
headerView.setTextColor(Color.WHITE);
headerView.setWidth(1080);
headerView.setTextSize(50);
headerView.setText("我是头");
headerAndFooterAdapter.addHeaderView(headerView);
//设置适配器
rv.setAdapter(headerAndFooterAdapter);
//添加数据
addData(adapter);
RecyelrView设置的适配器是headerAndFooterAdapter,而添加数据使用的是adapter。HeaderAndFooterAdapter是支持添加多个HeaderView的
1.6.1 GridLayoutManger和StaggeredGridLayoutManager跨列问题
上面添加HeaderView时,使用的LinerLayoutManager,当使用GridLayoutManger时,便会有问题
HeaderView不能单独占据一行
加入针对GridLayoutManager跨列处理的代码:
/***当RecyelrView开始观察Adapter会被回调*/
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {mAdapter.onAttachedToRecyclerView(recyclerView);final RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();if (manager instanceof GridLayoutManager) {final GridLayoutManager gridLayoutManager = (GridLayoutManager) manager;gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {@Overridepublic int getSpanSize(int position) {int viewType = getItemViewType(position);//如果是HeaderView或者是FooterView,设置占据gridLayoutManager.getSpanCount()列if (null != mHeaderViews.get(viewType) || null != mFooterViews.get(viewType)) {return gridLayoutManager.getSpanCount();}return 1;}});}
}
当布局管理器为GridLayouManger时,对当前要的添加的item进行判断,如果是HeaderView或者是FooterView,就进行跨列处理,单独占据一行
加入针对StaggeredGridLayoutManager跨列处理的代码:
/*** 一个item通过adapter开始显示会被回调*/
@Override
public void onViewAttachedToWindow(BaseViewHolder holder) {super.onViewAttachedToWindow(holder);int position = holder.getLayoutPosition();if (isHeaderViewPosition(position)||isFooterViewPosition(position)){ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();if (null != lp && lp instanceof StaggeredGridLayoutManager.LayoutParams){StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;p.setFullSpan(true);//占满一行}}
}
当布局管理器为StaggeredGridLayoutManager时,对当前要的添加的item进行判断,如果是HeaderView或者是FooterView,就设置setFullSpan(true),占满一行

















