MultiType 这个项目,至今 v3.x 稳定多时,考虑得很是多,但也作得很是克制。原则一直是 直观、灵活、可靠、简单纯粹(其中直观和灵活是很是看重的)。java
这是 MultiType 框架做者给出的项目简述。git
做为一个 RecyclerView 的 Adapter 框架,感受这项目的设计很是的优雅,并且能够知足不少经常使用的需求,并且像做者所说,该项目很是克制,没有由于便利而加入一些会致使项目臃肿的功能,它只提供了数据的绑定,其余的功能咱们只须要稍微加以封装就能够实现。github
若是还没用过这个库的先去看看做者的文档bash
public class Category {
@NonNull public final String text;
public Category(@NonNull String text) {
this.text = text;
}
}
复制代码
ItemViewBinder 是个抽象类,其中 onCreateViewHolder 方法用于生产你的 item view holder, onBindViewHolder 用于绑定数据到 Views. 通常一个 ItemViewBinder 类在内存中只会有一个实例对象,MultiType 内部将复用这个 binder 对象来生产全部相关的 item views 和绑定数据。示例:app
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);
}
}
}
复制代码
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 的用法,上手很快。框架
因此咱们的封装就是为了解决上面的两个问题。ide
上面说到咱们封装就是要解决上面提到的两个问题,让其更好用:布局
第三点是随便添加上去的,用于只有一个 TextView 的 Item。ui
思路其实很简单,就是建立一个 BaseViewHolder 来代替咱们以前须要频繁建立的 ViewHolder.this
废话少说,看代码:
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 来实现咱们的功能。代码以下:
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)
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()));
复制代码
以上就是整套的封装了,很简单,可是也很实用,能够在平常开发中省下很多代码。
上面说了,咱们还能够经过继承这个 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)
方法在进行绑定的时候进行控件的属性修改等逻辑。
MultiType 其实自己就支持
HeaderView
、FooterView
,只要建立一个Header.class
-HeaderViewBinder
和Footer.class
-FooterViewBinder
便可,而后把new Header()
添加到items
第一个位置,把new Footer()
添加到items
最后一个位置。须要注意的是,若是使用了 Footer View,在底部插入数据的时候,须要添加到最后位置 - 1
,即倒二个位置,或者把Footer
remove 掉,再添加数据,最后再插入一个新的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 的数量才能获得原始数据集的相对位置。
以上,就完成了本次的小封装,赶忙去代码中实战吧。