基于 Multitype 开源库封装更好用的RecyclerView.Adapter
前言
MultiType 这个项目,至今 v3.x 稳定多时,考虑得非常多,但也做得非常克制。原则一直是 直观、灵活、可靠、简单纯粹(其中直观和灵活是非常看重的)。
这是 MultiType 框架作者给出的项目简述。
作为一个 RecyclerView 的 Adapter 框架,感觉这项目的设计非常的优雅,而且可以满足很多常用的需求,而且像作者所说,该项目非常克制,没有因为便利而加入一些会导致项目臃肿的功能,它只提供了数据的绑定,其他的功能我们只需要稍微加以封装就可以实现。
为什么要封装
如果还没用过这个库的先去看看作者的文档
我们先来看看框架的原始用法:
Step 1. 创建一个 class,它将是你的数据类型或 Java bean / model. 对这个类的内容没有任何限制。示例如下:
public class Category {@NonNull public final String text;public Category(@NonNull String text) {this.text = text;}
}
复制代码
Step 2. 创建一个 class 继承 ItemViewBinder.
ItemViewBinder 是个抽象类,其中 onCreateViewHolder 方法用于生产你的 item view holder, onBindViewHolder 用于绑定数据到 Views. 一般一个 ItemViewBinder 类在内存中只会有一个实例对象,MultiType 内部将复用这个 binder 对象来生产所有相关的 item views 和绑定数据。示例:
public class CategoryViewBinder extends ItemViewBinder<Category, CategoryViewBinder.ViewHolder> {@NonNull @Overrideprotected ViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {View root = inflater.inflate(R.layout.item_category, parent, false);return new ViewHolder(root);}@Overrideprotected void onBindViewHolder(@NonNull ViewHolder holder, @NonNull Category category) {holder.category.setText(category.text);}static class ViewHolder extends RecyclerView.ViewHolder {@NonNull private final TextView category;ViewHolder(@NonNull View itemView) {super(itemView);this.category = (TextView) itemView.findViewById(R.id.category);}}
}
复制代码
Step 3. 在 Activity 中加入 RecyclerView 和 List 并注册你的类型,示例:
public class MainActivity extends AppCompatActivity {private MultiTypeAdapter adapter;/* Items 等同于 ArrayList<Object> */private Items items;@Override protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);RecyclerView recyclerView = (RecyclerView) findViewById(R.id.list);/* 注意:我们已经在 XML 布局中通过 app:layoutManager="LinearLayoutManager"* 给这个 RecyclerView 指定了 LayoutManager,因此此处无需再设置 */adapter = new MultiTypeAdapter();/* 注册类型和 View 的对应关系 */adapter.register(Category.class, new CategoryViewBinder());adapter.register(Song.class, new SongViewBinder());recyclerView.setAdapter(adapter);/* 模拟加载数据,也可以稍后再加载,然后使用* adapter.notifyDataSetChanged() 刷新列表 */items = new Items();for (int i = 0; i < 20; i++) {items.add(new Category("Songs"));items.add(new Song("drakeet", R.drawable.avatar_dakeet));items.add(new Song("许岑", R.drawable.avatar_cen));}adapter.setItems(items);adapter.notifyDataSetChanged();}
}
复制代码
我把作者文档中的事例搬了过来,可以看到,使用还是非常简易的,沿用了原生 ViewHolder 的用法,上手很快。
- 但是这也是一个非常不便的问题,因为作者没有进一步的封装,所以我们还需要为每个 Binder 去配置一个 ViewHolder ,所以我们还是做了很多重复性的工作。
- 并且在 Adapter 或 Binder 中没有为我们提供 Item 的点击反馈接口,这样就导致我们的点击万一依赖到 Activity 或者 Fragment 的一些变量的话,又需要我们去写一个 Callback 。
所以我们的封装就是为了解决上面的两个问题。
封装
问题
上面说到我们封装就是要解决上面提到的两个问题,让其更好用:
- 封装 ViewHolder
- 添加点击事件
- 添加 Sample Binder
- 添加Header、Footer
第三点是随便添加上去的,用于只有一个 TextView 的 Item。
方案
1. 封装ViewHolder
思路其实很简单,就是创建一个 BaseViewHolder 来代替我们之前需要频繁创建的 ViewHolder.
废话少说,看代码:
public class BaseViewHolder extends RecyclerView.ViewHolder {private View mView;private SparseArray<View> mViewMap = new SparseArray<>(); // 1public BaseViewHolder(View itemView) {super(itemView);mView = itemView;}//返回根Viewpublic View getView() {return mView;}/*** 根据View的id来返回view实例*/public <T extends View> T getView(@IdRes int ResId) {View view = mViewMap.get(ResId);if (view == null) {view = mView.findViewById(ResId);mViewMap.put(ResId, view);}return (T) view;}
}复制代码
整个类就一个方法 getView
的两个重载,没有参数的 那个返回我们 Item 的根 View ,有参数的那个可以根据控件的 Id 来返回相对应 View。
在 getView(@IdRes int ResId)
方法中,我们用 ResId 为键,View 为值的 SparseArray 来存储当前 ViewHolder 的各种View,然后首次加载(即mViewMap
没有对应的值)时就用 findViewById
方法来获取相对View并存起来,然后复用的时候就可以直接重 mViewMap
中获取相对于的值(View)来进行数据绑定。
接着,为了方便,我们可以添加一系列的方法在此类中,例如:
public BaseViewHolder setText(@IdRes int viewId, @StringRes int strId) {TextView view = getView(viewId);view.setText(strId);return this;}public BaseViewHolder setImageResource(@IdRes int viewId, @DrawableRes int imageResId) {ImageView view = getView(viewId);view.setImageResource(imageResId);return this;}复制代码
这样一来,我们就可以在 Binder 类的onBindViewHolder中进行更加简便的数据绑定,例如:
@Override
protected void onBindViewHolder(@NonNull BaseViewHolder holder, @NonNull T item) {holder.setText(R.id.name,“张三”);holder.setImageResource(R.id.avatar,R.mimap.icon_avatar);
}
复制代码
2. 封装 ItemBinder
为了解决我们上面问题中的第2点,我们需要封装一个 ItemBinder 来实现我们的功能。代码如下:
public abstract class LwItemBinder<T> extends ItemViewBinder<T, LwViewHolder> {private OnItemClickListener<T> mListener;private OnItemLongClickListener<T> mLongListener;private SparseArray<OnChildClickListener<T>> mChildListenerMap = new SparseArray<>();private SparseArray<OnChildLongClickListener<T>> mChildLongListenerMap = new SparseArray<>();protected abstract View getView(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent);protected abstract void onBind(@NonNull LwViewHolder holder, @NonNull T item);@NonNull@Overrideprotected final LwViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {return new LwViewHolder(getView(inflater, parent));}@Overrideprotected final void onBindViewHolder(@NonNull LwViewHolder holder, @NonNull T item) {bindRootViewListener(holder, item);bindChildViewListener(holder, item);onBind(holder, item);}/*** 绑定子View点击事件** @param holder* @param item*/private void bindChildViewListener(LwViewHolder holder, T item) {//点击事件for (int i = 0; i < mChildListenerMap.size(); i++) {int id = mChildListenerMap.keyAt(i);View view = holder.getView(id);if (view != null) {view.setOnClickListener(v -> {OnChildClickListener<T> l = mChildListenerMap.get(id);if (l!=null){l.onChildClick(holder,view,item);}});}}//长按点击for (int i = 0; i < mChildLongListenerMap.size(); i++) {int id = mChildLongListenerMap.keyAt(i);View view = holder.getView(id);if (view != null) {view.setOnClickListener(v -> {OnChildLongClickListener<T> l = mChildLongListenerMap.get(id);if (l != null) {l.onChildLongClick(holder,view, item);}});}}}/*** 绑定根view** @param holder* @param item*/private void bindRootViewListener(LwViewHolder holder, T item) {//根View点击事件holder.getView().setOnClickListener(v -> {if (mListener != null) {mListener.onItemClick(holder, item);}});//根View长按事件holder.getView().setOnLongClickListener(v -> {boolean result = false;if (mLongListener != null) {result = mLongListener.onItemLongClick(holder, item);}return result;});}/*** 点击事件*/public void setOnItemClickListener(OnItemClickListener<T> listener) {mListener = listener;}/*** 点击事件** @param id 控件id,可传入子view ID* @param listener*/public void setOnChildClickListener(@IdRes int id, OnChildClickListener<T> listener){mChildListenerMap.put(id,listener);}public void setOnChildLongClickListener(@IdRes int id, OnChildLongClickListener<T> listener){mChildLongListenerMap.put(id,listener);}/*** 长按点击事件*/public void setOnItemLongClickListener(OnItemLongClickListener<T> l) {mLongListener = l;}/*** 长按点击事件** @param id 控件id,可传入子view ID*/public void removeChildClickListener(@IdRes int id){mChildListenerMap.remove(id);}public void removeChildLongClickListener(@IdRes int id){mChildLongListenerMap.remove(id);}/*** 移除点击事件*/public void removeItemClickListener() {mListener = null;}public void removeItemLongClickListener() {mLongListener = null;}public interface OnItemLongClickListener<T> {boolean onItemLongClick(LwViewHolder holder, T item);}public interface OnItemClickListener<T> {void onItemClick(LwViewHolder holder, T item);}public interface OnChildClickListener<T> {void onChildClick(LwViewHolder holder, View child, T item);}public interface OnChildLongClickListener<T> {void onChildLongClick(LwViewHolder holder, View child, T item);}}复制代码
代码也很简单,提供了Click以及LongClick的监听,并且在 onCreateViewHolder()
方法中将我们刚刚封装的 BaseViewHolder 给传进去,然后提供两个抽象方法:
getView(@NonNull LayoutInflater inflater,@NonNull ViewGroup parent)
- 需要返回Item的View实例
onBind(@NonNull BaseViewHolder holder, @NonNull T item)
- 在此方法内进行数据绑定
以后我们就不必为每个 Binder 都设置一套ViewHolder了,实例如下:
public class RankItemBinder extends LwItemBinder<Rank> {private final int[] RANK_IMG = {R.drawable.no_4,R.drawable.no_5,R.drawable.no_6,R.drawable.no_7,R.drawable.no_8,R.drawable.no_9,R.drawable.no_10};@Overrideprotected View getView(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {return inflater.inflate(R.layout.item_rank, parent, false);}@Overrideprotected void onBind(@NonNull BaseViewHolder holder, @NonNull Rank item) {Context context = holder.getView().getContext();holder.setText(R.id.tv_name, item.getUserNickname());holder.setText(R.id.tv_num, context.getString(R.string.text_caught_doll_num, item.getCaughtNum()));loadCircleImage(context,item.getUserIconUrl(),0,0,holder.getView(R.id.iv_avatar));if (holder.getAdapterPosition() < 7) {holder.setImageResource(R.id.iv_rank, RANK_IMG[holder.getAdapterPosition()]);}}public void loadCircleImage(final Context context, String url, int placeholderRes, int errorRes, final ImageView imageView) {RequestOptions requestOptions = new RequestOptions().circleCrop();if (placeholderRes != 0) requestOptions.placeholder(placeholderRes);if (errorRes != 0) requestOptions.error(errorRes);Glide.with(context).load(url).apply(requestOptions).into(imageView);}
}复制代码
可以看到,非常的简洁,并且可以在 Activity 或 Fragment 中添加监听事件:
RankItemBinder binder = new RankItemBinder();
binder.setOnItemClickListener(new BaseItemBinder.OnItemClickListener<Rank>() {@Overridepublic void onItemClick(BaseViewHolder holder, Rank item) {ToastUtils.showShort("点击了"+item.getUserNickname());}
});复制代码
如果使用 lambda 表达式,则可以更简洁:
binder.setOnItemClickListener((holder, item) -> ToastUtils.showShort("点击了"+item.getUserNickname()));
复制代码
以上就是整套的封装了,很简单,但是也很实用,可以在日常开发中省下不少代码。
3. 封装Sample
上面说了,我们还可以通过继承这个 BaseItemBinder 来实现一个只有一个 TextView 的Sample:
public class SampleBinder extends LwItemBinder<Object> {public static final int DEFAULT_TEXT_SIZE = 15; //sppublic static final int DEFAULT_HEIGHT = 50; //dppublic static final int DEFAULT_PADDING_HORIZONTAL = 6; //dppublic static final int DEFAULT_PADDING_VERTICAL = 4; //dp@Overrideprotected View getView(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {Context context = parent.getContext();DisplayMetrics metrics = context.getResources().getDisplayMetrics();float density = metrics.density;int heightPx = dp2px(density, DEFAULT_HEIGHT);int paddingHorizontal = dp2px(density, DEFAULT_PADDING_HORIZONTAL);TextView textView = new TextView(context);textView.setTextSize(DEFAULT_TEXT_SIZE);textView.setGravity(Gravity.CENTER_VERTICAL);textView.setPadding(paddingHorizontal, 0, paddingHorizontal, 0);ViewGroup.LayoutParams params =new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, heightPx);textView.setLayoutParams(params);custom(textView, parent);return textView;}@Overrideprotected void onBind(@NonNull LwViewHolder holder, @NonNull Object item) {TextView textView = holder.getView();textView.setText(item.toString());}private int dp2px(float density, float dp) {return (int) (density * dp + 0.5f);}protected void custom(TextView textView, ViewGroup parent) {}
}复制代码
很简单的一个扩展,根 View 就是一个 TextView
,然后提供了一些属性的设置修改,如果不满足默认样式还可以重写 custom(TextView textView, ViewGroup parent)
方法对 TextView
进行样式的修改,或者重写 custom(TextView textView, ViewGroup parent)
方法在进行绑定的时候进行控件的属性修改等逻辑。
4. 添加Header、Footer
MultiType 其实本身就支持
HeaderView
、FooterView
,只要创建一个Header.class
-HeaderViewBinder
和Footer.class
-FooterViewBinder
即可,然后把new Header()
添加到items
第一个位置,把new Footer()
添加到items
最后一个位置。需要注意的是,如果使用了 Footer View,在底部插入数据的时候,需要添加到最后位置 - 1
,即倒二个位置,或者把Footer
remove 掉,再添加数据,最后再插入一个新的Footer
.
这个是作者文档里面说的,简单,但是繁琐,既然我们要封装,肯定就不能容忍这么繁琐的事情。
先理一下要实现的点:
- 一行代码添加 Header/Footer
- 源数据的更改更新与 Header/Footer 无关
接下来看看具体实现:
public class LwAdapter extends MultiTypeAdapter {//...省略部分代码private HeaderExtension mHeader;private FooterExtension mFooter;/*** 添加Footer** @param o Header item*/public LwAdapter addHeader(Object o) {createHeader();mHeader.add(o);notifyItemRangeInserted(getHeaderSize() - 1, 1);return this;}/*** 添加Footer** @param o Footer item*/public LwAdapter addFooter(Object o) {createFooter();mFooter.add(o);notifyItemInserted(getItemCount() + getHeaderSize() + getFooterSize() - 1);return this;}/*** 增加Footer数据集** @param items Footer 的数据集*/public LwAdapter addFooter(Items items) {createFooter();mFooter.addAll(items);notifyItemRangeInserted(getFooterSize() - 1, items.size());return this;}private void createHeader() {if (mHeader == null) {mHeader = new HeaderExtension();}}private void createFooter() {if (mFooter == null) {mFooter = new FooterExtension();}}
}复制代码
先看上面的实现,用 addHeader(Object o)
添加 Header,添加 Footer 同理,一行代码就实现,但是这个 addHeader(Object o)
方法里面的逻辑是怎样的呢,首先是调用了 createHeader()
,即创建一个 HeaderExtension
对象并把引用赋值给 mHeader,然后再调用mHeader.add(o)
将我们传过来的 item 实例给添加进去,最后调用Adapter
的notifyItemInserted
方法刷新一下列表就OK了。逻辑很简单,但是这样为什么就可以实现了添加 Header 的功能呢,HeaderExtension
又是什么鬼呢?
接下来看看 HeaderExtension
是什么?
public class HeaderExtension implements Extension {private Items mItems;public HeaderExtension(Items items) {this.mItems = items;}public HeaderExtension(){this.mItems = new Items();}@Overridepublic Object getItem(int position) {return mItems.get(position);}@Overridepublic boolean isInRange(int adapterSize, int adapterPos) {return adapterPos < getItemSize();}@Overridepublic int getItemSize() {return mItems.size();}@Overridepublic void add(Object o) {mItems.add(o);}@Overridepublic void remove(Object o) {mItems.add(o);}//...省略部分代码
}
复制代码
该类实现了Extension
接口,我们调用add()
方法就是将传过来的对象保存起来而已。整个类最主要的方法就是 isInRange(int adapterSize, int adapterPos)
方法,看到这个方法的实现相信你也能明白他的作用了,就是用来判断 Adapter
里面传过来的 position 对应的 Item 是否是 Header.接下来看一下这个方法在 Adapter 内的使用在哪里:
#LwAdapter.java
@Overridepublic final int getItemViewType(int position) {Object item = null;int headerSize = getHeaderSize();int mainSize = getItems().size();if (mHeader != null) {if (mHeader.isInRange(getItemCount(), position)) {item = mHeader.getItem(position);return indexInTypesOf(position, item);}}if (mFooter != null) {if (mFooter.isInRange(getItemCount(), position)) {int relativePos = position - headerSize - mainSize;item = mFooter.getItem(relativePos);return indexInTypesOf(relativePos, item);}}int relativePos = position - headerSize;return super.getItemViewType(relativePos);}
复制代码
第一次的调用在这里,到这里我们应该就恍然大悟了,原来就是根据 position 来判断是否用于 Header/Footer ,然后再用 父类里面的 indexInTypesOf(int,Object)
来获取对应的类型。接着在 onCreateViewHolder(ViewGroup parent, int indexViewType)
会自动创建我们对应的 ViewHolder
,最后在onBindViewHolder()
中再进行相应的绑定即可:
@SuppressWarnings("unchecked")@Overridepublic final void onBindViewHolder(RecyclerView.ViewHolder holder, int position,@NonNull List<Object> payloads) {Object item = null;int headerSize = getHeaderSize();int mainSize = getItems().size();ItemViewBinder binder = getTypePool().getItemViewBinder(holder.getItemViewType());if (mHeader != null) {if (mHeader.isInRange(getItemCount(), position)) {item = mHeader.getItem(position);}}if (mFooter != null) {if (mFooter.isInRange(getItemCount(), position)) {int relativePos = position - headerSize - mainSize;item = mFooter.getItem(relativePos);}}if (item != null) {binder.onBindViewHolder(holder, item);return;}super.onBindViewHolder(holder, position - headerSize, payloads);}
复制代码
onBindViewHolder
跟 getItemViewType
的实现思想类似,判断是否是 Header/Footer 拿到相应的实体类,然后进行绑定。整个流程就是这样,当然别忘了也要在 getItemCount
方法中将我们的 Header 与 Footer 的数量加进入,如:
@Override
public final int getItemCount() {int extensionSize = getHeaderSize() + getFooterSize();return super.getItemCount() + extensionSize;
}
复制代码
这样的封装可以让我们的 Header/Footer 里面的数据集与原本的数据集分离,我们的主数据再怎么增删查改都不会影响到Header/Footer 的正确性。
这样的实现目前有个比较蛋疼的点,我们调用ViewHolder
的 getAdapterPosition()
时候会返回实际的 position,即包含了 Header 的数量,目前这点还没解决,需要手动把该 position 减去 Header 的数量才能得到原始数据集的相对位置。
以上,就完成了本次的小封装,赶紧去代码中实战吧。
基于 Multitype 开源库封装更好用的RecyclerView.Adapter相关推荐
- 基于第三方开源库的OPC服务器开发指南(2)——LightOPC的编译及部署
基于第三方开源库的OPC服务器开发指南(2)--LightOPC的编译及部署 前文已经说过,OPC基于微软的DCOM技术,所以开发OPC服务器我们要做的事情就是开发一个基于DCOM的EXE文件.一个代 ...
- libcurl开源库封装ftp工具,支持多线程并发、断点续传、超时连接、传输速率控制
各位朋友好,第一次在CSDN写博客,后续时间充足的话会陆续更新一些资源,大家一起探讨交流,感谢!!! 如有任何疑问,可以留言. 目的:再次封装CURL接口,使用FTP相关接口更方便,更易懂 功能: 可 ...
- 【barcode】 基于Jbarcode开源库生成条形码,提供添加备注信息的解决方案
上一篇使用google的barcode4开源库生成条码,效果还是不错的,但是由于前几天leader有个需求,条码下面要添加备注信息- 当然解决方案也可以生成两个图片拼接在一起,但是觉得不太方便,就查了 ...
- 无信息变量选择(UVE)波长筛选算法--基于OpenSA开源库实现
系列文章目录 "光晰本质,谱见不同",光谱作为物质的指纹,被广泛应用于成分分析中.伴随微型光谱仪/光谱成像仪的发展与普及,基于光谱的分析技术将不只停留于工业和实验室,即将走入生活, ...
- IFC模型文件查看器(基于IFC++开源库实现)
关于IFC IFC是由buildingSMART以工业的产品资料交换标准STEP编号ISO-10303-11的产品模型信息描述用EXPERSS语言为基础,基于BIM中AEC/FM相关领域信息交流所指定 ...
- 基于 C++ POCO 库封装的异步多线程的 CHttpClient 类
用惯了 Jetty 的 基于事件的 HttpClient 类,在C++平台上也没找到这样调用方式的类库,只好自己写一个了. 目前版本 1.0,朋友们看了给点建议.(注:Kylindai原创,转载请注明 ...
- C++ 使用libwebsockets开源库封装client类
本文参考:封装利用libwebsockets写出的客户端.服务端程序为客户端服务端类_逍遥游的博客-CSDN博客_libwebsockets封装 最近项目需要使用C++连接websocket服务器,选 ...
- 涂抹去水印(基于lama-cleaner开源库)
github连接:https://github.com/Sanster/lama-cleaner 今天发现了好玩的,拖动就可以去除水印,下边是原图和修改后的 安装lama-cleaner库 在你的环境 ...
- NLP - 微信好友个性签名情感分析( 基于Python开源库snownlp )
配置与简介:https://blog.csdn.net/qq_42292831/article/details/88932177 本文源码下载:https://github.com/Hirehop/P ...
最新文章
- 西湖大学生命科学学院杨剑教授实验室招聘启事
- 中国软件业真的到了该反思的时候了
- cocos2d_x_03_经常使用类的使用_事件_画图
- 第二十八条:利用有限制通配符来提升API的灵活性
- html使用xml数据岛,html中的xml数据岛记录编辑与添加_xml技巧
- Map-Reduce入门
- 什么是爱?什么是幸福?
- CSS3的box-shadow属性:给指定的区域加阴影
- 【LeetCode】剑指 Offer 55 - I. 二叉树的深度
- python读取日志错误信息_使用Python将Exception异常错误堆栈信息写入日志文件
- oracle 日期型函数转换,oracle中,日期转换函数
- Nginx网络压缩 CSS压缩 图片压缩 JSON压缩
- Python 智能银行卡识别系统的实现 (2)—系统的实现
- Tensorflow 中文语音识别
- 开课吧JAVA高级架构师怎么样_开课吧JavaEE企业级高级架构师
- linux sd卡 分区变大,Linux下使用fdisk命令将高容量SD卡(SDHC)格成两个分区
- Ayla艾拉物联基于AWS构建IoT艾拉云
- RFNet:基于RGB-D数据的语义分割和意外障碍物检测的实时融合网络
- Java实现 LeetCode 292 Nim游戏
- 【c++基础】ifstream、istringstream的示例应用