之前使用RecyclerView.Adapter
,基本就类似套用公式,死步骤,对Adapter
感到既熟悉又陌生。从去年我开始接触学习Android
之时,RecyclerView
已经开始大量被运用,逐步取代ListView
。遂,正好,那就先直接学习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)
源码:
/**
* 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);
item
的根ViewGroup
,item
的子控件加载在其中item
的类型,可以根据viewType
来创建不同的ViewHolder
,来加载不同的类型的item
这个方法就是用来创建出一个新的ViewHolder
,可以根据需求的itemType
,创建出多个ViewHolder
。创建多个itemType
时,需要getItemViewType(int position)
方法配合
源码:
**
*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);
onCreateViewHolder()
方法中,创建的ViewHolder
item
对应的DataList
数据源集合的postion
postion
就是adapter position
,RecycelrView
中item
的数量,就是根据DataList
数据源集合的数量来创建的
源码:
/**
* 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
后,需要注意考虑这个返回值
一个最简单的RecyclerViewAdapter
public class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.RecyclerHolder> {
private Context mContext;
private List dataList = new ArrayList<>();
public MyRecyclerViewAdapter(RecyclerView recyclerView) {
this.mContext = recyclerView.getContext();
}
public void setData(List dataList) {
if (null != dataList) {
this.dataList.clear();
this.dataList.addAll(dataList);
notifyDataSetChanged();
}
}
@Override
public RecyclerHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(mContext).inflate(R.layout.id_rv_item_layout, parent, false);
return new RecyclerHolder(view);
}
@Override
public void onBindViewHolder(RecyclerHolder holder, int position) {
holder.textView.setText(dataList.get(position));
}
@Override
public 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个参数的方法。
以前使用2个参数的方法inflate(@LayoutRes int resource, @Nullable ViewGroup root)
,下面的形式
View view = LayoutInflater.from(mContext).inflate(R.layout.id_rv_item_layout,null);
遇到的一个问题
两种方法,使用的是同一套布局
"http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
"@+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" />
item
内的TextView
的宽是match_parent
,但使用两个参数的方法时,看起来却是wrap_content
的效果
两个参数的方法源码
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 xml
final 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()方法的完整过程还是比较复杂的,比较浅显的知道问题出在哪里后,没有深挖
完整代码:
public class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.RecyclerHolder> {
private Context mContext;
private List 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 dataList) {
if (null != dataList) {
this.dataList.clear();
this.dataList.addAll(dataList);
notifyDataSetChanged();
}
}
public List getDataList() {
return dataList;
}
@Override
public 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);
}
@Override
public 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() {
@Override
public void onClick(View v) {
if (null != mListener && null != v) {
mListener.onRecyclerItemClick(v, dataList.get(position), position);
}
}
};
}
@Override
public 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() {
@Override
public 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点击事件
一共有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() {
@Override
public 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
的方法,以后学到了再补充
通用的ViewHolder:
public class BaseViewHolder extends RecyclerView.ViewHolder {
private final SparseArray sparseArray;
public BaseViewHolder(View itemView) {
super(itemView);
this.sparseArray = new SparseArray<>(8); //一般一个Item 不会超过8种控件
}
public 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
将控件存起来
适配器:
public abstract class CommonBaseAdapter<T> extends RecyclerView.Adapter<BaseViewHolder> {
protected List 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 data) {
if (data != null) {
this.data.clear();
this.data.addAll(data);
notifyDataSetChanged();
}
}
/**
* 增加点击监听
*/
public void setItemListener(onRecyclerItemClickerListener mListener) {
this.mListener = mListener;
}
@Override
public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//这里使用3个参数的方法
View view = LayoutInflater.from(mContext).inflate(itemLayoutId, parent, false);
return new BaseViewHolder(view);
}
@Override
public 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() {
@Override
public void onClick(View v) {
if (mListener != null && v != null) {
mListener.onRecyclerItemClick(v, data.get(position), position);
}
}
};
}
@Override
public 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);
}
@Override
public void bindViewData(BaseViewHolder holder, String item, int position) {
holder.setText(R.id.textViewId, item);
}
}
在Activity
中就可以进行使用
这个简易的封装并没有对添加加载图片的方法。加载图片的方法一开始也我封装在了这个CommonBaseAdapter
中,但后来发现直接封装在这里并不是好的思路
图片需要做的处理比较多,而且主流的库有3个,为了易于维护,还是将图片的操作单独再封装在一个工具类中,在CommonBaseAdapter
中使用操作图片的工具类比较好
学的鸿洋大神的代码和思路,涉及到了装饰模式。还有一种添加方式是直接通过使用多种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 mHeaderViews = new SparseArrayCompat<>();
private SparseArrayCompat mFooterViews = new SparseArrayCompat<>();
public HeaderAndFooterAdapter(CommonBaseAdapter mAdapter) {
this.mAdapter = mAdapter;
}
@Override
public 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);
}
@Override
public void onBindViewHolder(BaseViewHolder holder, int position) {
if (isHeaderViewPosition(position)) return;
if (isFooterViewPosition(position)) return;
mAdapter.onBindViewHolder(holder, position - getHeaderViewCount());
}
@Override
public 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());
}
@Override
public 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
的
上面添加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() {
@Override
public 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)
,占满一行