内容简介:MultiType 这个项目,至今 v3.x 稳定多时,考虑得非常多,但也做得非常克制。原则一直是 直观、灵活、可靠、简单纯粹(其中直观和灵活是非常看重的)。这是 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 @Override protected ViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { View root = inflater.inflate(R.layout.item_category, parent, false); return new ViewHolder(root); } @Override protected 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<>(); // 1 public BaseViewHolder(View itemView) { super(itemView); mView = itemView; } //返回根View public 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 @Override protected final LwViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { return new LwViewHolder(getView(inflater, parent)); } @Override protected 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 }; @Override protected View getView(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { return inflater.inflate(R.layout.item_rank, parent, false); } @Override protected 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>() { @Override public 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; //sp public static final int DEFAULT_HEIGHT = 50; //dp public static final int DEFAULT_PADDING_HORIZONTAL = 6; //dp public static final int DEFAULT_PADDING_VERTICAL = 4; //dp @Override protected 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; } @Override protected 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(); } @Override public Object getItem(int position) { return mItems.get(position); } @Override public boolean isInRange(int adapterSize, int adapterPos) { return adapterPos < getItemSize(); } @Override public int getItemSize() { return mItems.size(); } @Override public void add(Object o) { mItems.add(o); } @Override public void remove(Object o) { mItems.add(o); } //...省略部分代码 } 复制代码
该类实现了 Extension
接口,我们调用 add()
方法就是将传过来的对象保存起来而已。整个类最主要的方法就是 isInRange(int adapterSize, int adapterPos)
方法,看到这个方法的实现相信你也能明白他的作用了,就是用来判断 Adapter
里面传过来的 position 对应的 Item 是否是 Header.接下来看一下这个方法在 Adapter 内的使用在哪里:
#LwAdapter.java
@Override public 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") @Override public 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》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 封装JDBC—非框架开发必备的封装类
- SpringBlade 2.3.2 发布,增加 OSS 封装及单元测试封装
- SpringBlade 2.3.2 发布,增加 OSS 封装及单元测试封装
- docker 封装 alinode
- 封装Apk签名工具
- axios封装笔记
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
PHP5与MySQL5 Web开发技术详解
杜江 / 电子工业出版社 / 2007-11 / 79.00元
《PHP5与MySQL5 Web开发技术详解》(含光盘)是目前中文版本第一个真正介绍PHP5及MYSQL5新增语法与功能的权威宝典!一起来看看 《PHP5与MySQL5 Web开发技术详解》 这本书的介绍吧!