前言
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
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 mViewMap = new SparseArray<>(); // 1
public BaseViewHolder(View itemView) {
super(itemView);
mView = itemView;
}
//返回根View
public View getView() {
return mView;
}
/**
* 根据View的id来返回view实例
*/
public 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 mListener;
private OnItemLongClickListener mLongListener;
private SparseArray> mChildListenerMap = new SparseArray<>();
private SparseArray> 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 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 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 listener) {
mListener = listener;
}
/**
* 点击事件
*
* @param id 控件id,可传入子view ID
* @param listener
*/
public void setOnChildClickListener(@IdRes int id, OnChildClickListener listener) {
mChildListenerMap.put(id,listener);
}
public void setOnChildLongClickListener(@IdRes int id, OnChildLongClickListener listener) {
mChildLongListenerMap.put(id,listener);
}
/**
* 长按点击事件
*/
public void setOnItemLongClickListener(OnItemLongClickListener 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() {
@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 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 的数量才能得到原始数据集的相对位置。
以上,就完成了本次的小封装,赶紧去代码中实战吧。