前使用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 简单shi yong
一个最简单的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(); } } @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个参数的方法。
1.4.1 问题
以前使用2个参数的方法inflate(@LayoutRes int resource, @Nullable ViewGroup root)
,下面的形式
View view = LayoutInflater.from(mContext).inflate(R.layout.id_rv_item_layout,null);
遇到的一个问题
2个参数方法遇到的问题
两种方法,使用的是同一套布局
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android: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 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()方法的完整过程还是比较复杂的,比较浅显的知道问题出在哪里后,没有深挖
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; } @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点击事件
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() { @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
的方法,以后学到了再补充
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; } @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
中使用操作图片的工具类比较好
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; } @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);
添加HeaderView
RecyelrView
设置的适配器是headerAndFooterAdapter
,而添加数据使用的是adapter
。HeaderAndFooterAdapter
是支持添加多个HeaderView
的
1.6.1 GridLayoutManger和StaggeredGridLayoutManager跨列问题
上面添加HeaderView
时,使用的LinerLayoutManager
,当使用GridLayoutManger
时,便会有问题
HeaderView不能单独占据一行
GridLayoutManager遇到问题
加入针对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)
,占满一行
学习资料:
- 为RecyclerView打造通用Adapter让RecyclerView更加好用
- Android优雅的为RecyclerView添加HeaderView和FooterView
本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。
ctvol管理联系方式QQ:251552304
本文章地址:https://www.ctvol.com/addevelopment/895955.html