前言
这个完整的例子包括了以下功能,我想应该能满足 90% 的需求吧:
刷新
加载更多
支持 header item
支持 empty item,并允许再次刷新
支持 error item,并允许再次刷新
支持 footer item,包括 3 种状态:加载中,出错并允许重试,没有更多数据
支持多种数据类型的 item,我们在这个例子中只展示了 ImageItem 和 TextItem 两种类型
大纲
实现篇
// build.gradle
android {
//... dataBinding { enabled = true }
}
dependencies {
//... compile 'com.android.support:recyclerview-v7:25.1.0'
}
接着,我们来开始来实现这个 adapter,取名为 MultiTypeAdapter,因为我们要支持多类型,那么 adapter 里的 item 必然是抽象的,我们定义为 IItem:
// MultiTypeAdapter.java
public interface IItem {
}
private List<IItem> items = new ArrayList<>();
我们先从简单的入手,先来看看 getItemCount(),我们用 showHeader和 showFooter两个变量来控制是否显示 header 或 footer,那么 getItemCount()的实现如下:
// MultiTypeAdapter.java
@Override
public int getItemCount() {
int cnt = items.size();
if (showHeader) {
cnt++;
}
if (showFooter) {
cnt++;
}
return cnt;
}
接着来实现 getItemViewType(int position),关于这个方法,一般的实现,我们要根据 position 和相应位置的 item 类型来返回不同的值,比如:
// MultiTypeAdapter.java
@Override
public int getItemViewType(int position) {
if (position == 0 && showHeader) {
return ITEM_TYPE_HEADER;
} else if (position == getItemCount() -1 && showFooter) {
return ITEM_TYPE_FOOTER;
} else {
if (items.get(position) instanceof ImageItem) {
return ITEM_TYPE_IMAGE;
} else {
return ITEM_TYPE_TEXT;
}
}
}
这样的实现,很烦很丑是不是。关于这个方法的优化,我们很容易达成一种共识,首先,我们不再返回类似 ITEM_TYPE_IMAGE这种常量类型,而是直接返回它的 xml layout,其次,我们直接从 item 自身得到这个 layout。因此,我们IItem 增加一个 getType()
的接口方法。代码如下:
public interface IItem {
// should directly return layout
int getType();
}
@Override
public int getItemViewType(int position) {
if (position == 0 && showHeader) {
return R.layout.item_header;
} else if (position == getItemCount() -1 && showFooter) {
return R.layout.item_footer;
} else {
return items.get(position).getType();
}
}
(2017/2/15 Update: 由于 getType()实际是应该返回一个 xml layout 的,为了让这个方法名意义更明确,从 1.0.7 开始,这个方法重命名为 getLayout(),但整个教程仍然保留为 getType())
因为 header 和 footer,尤其是 footer,只是单纯地用来显示 正在 loading 等一些状态,我们很容易把它跟常规的数据 item 区别对待,但是,实际上我们可以把它看成一个伪 item,没有数据,只有布局的 item。我们分别实现只有布局的 HeaerItem 和 FooterItem,并在合适的时机加到 items 里面或从 items 里移除,就可以控制 header 和 footer 的显示与隐藏了。
// HeaerItem.java
public class HeaderItem implements MulitTypeAdapter.IItem {
@Override
public int getType() {
return R.layout.item_header;
}
}
// FooterItem.java
public class FooterItem implements MulitTypeAdapter.IItem {
@Override
public int getType() {
return R.layout.item_footer;
}
}
这样,我们的 getItemViewType()终于可以简化成一行代码了,清爽!
@Override
public int getItemViewType(int position) {
return items.get(position).getType();
}
这样,我们也不需要 showHeader 和 showFooter 这样的状态变量了,那么 getItemCount()
也可以简化成一行代码了。
public int getItemCount() {
return items.size();
}
刚才说到我们要在合适的时机把 HeaerItem 或 FooterItem 加到 items 或从 items 中移除,所以我们给 adapter 加上一些操作 items 的方法。如下所示:
// MultiTypeAdapter.java
public void setItem(IItem item) {
clearItems();
addItem(item);
}
public void setItems(List<IItem> items) {
clearItems();
addItems(items);
}
public void addItem(IItem item) {
items.add(item);
}
public void addItem(IItem item, int index) {
items.add(index, item);
}
public void addItems(List<IItem> items) {
this.items.addAll(items);
}
public void removeItem(IItem item) {
items.remove(item);
}
public void clearItems() {
items.clear();
}
你可能会想,诶,在这些操作函数里最后再加上 notifyDatasetChanged() 是不是会更方便点,这样我在上层就不用再手动调用一下 adapter.notifyDatasetChanged(),实际当你自己写起来的时候,你就会发现这样并不灵活。因为,我可能并不想每一次 addItem都刷新一次 UI,我可能要多次 addItem后才刷新一次 UI,这样,在上层由调用者来决定何时刷新 UI 会更灵活,更何况,我可能并不想只调用 notifyDatasetChanged(),我有时想调用 notifyItemRemoved(),或是 notifyItemChaned()。
当然,你也可以给 adapter 加上一个 getItems()的方法,然后把这些对 items 的操作逻辑都移动上层去处理,但我自己还是倾向于在 adapter 内封装这些方法。
// MulitTypeAdapter.java
public List<IItem> getItems() {
return items;
}
// ItemViewHolder.java
public abstract class ItemViewHolder extends RecyclerView.ViewHolder {
public ItemViewHolder(View itemView) {
super(itemView);
}
public abstract void bindTo(MulitTypeAdapter.IItem item);
}
分别实现 ImageViewHolder 和 TextViewHolder:
// ImageViewHolder.java
public class ImageViewHolder extends ItemViewHolder {
public ImageViewHolder(View itemView) {
super(itemView);
}
public void bindTo(MulitTypeAdapter.IItem item) {
ImageItem imageItem = (ImageItem) item;
// then do something
}
}
// TextViewHolder.java
public class TextViewHolder extends ItemViewHolder {
public TextViewHolder(View itemView) {
super(itemView);
}
public void bindTo(MulitTypeAdapter.IItem item) {
TextItem textItem = (TextItem) item;
// then do something
}
}
然后实现 onCreateViewHolder():
// MulitTypeAdapter.java
@Override
public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(viewType, parent, false);
if (viewType == R.layout.item_image) {
return new ImageViewHolder(itemView);
} else if (viewType == R.layout.item_text) {
return new TextViewHolder(itemView);
}
return null;
}
实现 onBindViewHolder():
// MulitTypeAdapter.java
@Override
public void onBindViewHolder(ItemViewHolder holder, int position) {
holder.bindTo(items.get(position));
}
可以看到,onBindViewHolder()的实现也已经变得非常简洁。那么就剩下 onCreateViewHolder()了。一般来说,我们会把这一部分逻辑通过工厂方法来优化,代码如下所示:
// ViewHolderFactory.java
public class ViewHolderFactory {
public static ItemViewHolder create(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(viewType, parent, false);
switch (viewType) {
case R.layout.item_image:
return new ImageViewHolder(itemView);
case R.layout.item_text:
return new TextViewHolder(itemView);
default:
return null;
}
}
}
那么 onCreateViewHolder 就可以同样简化成一行代码,如下所示:
public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return ViewHolderFactory.create(parent, viewType);
}
// ImageItem.java
public class ImageItem implements MulitTypeAdapter.IItem {
@Override
public int getType() {
return R.layout.item_image;
}
////////////////////////////////////////////////
public final String url;
public ImageItem() {
url = "https://unsplash.it/200/200?random&" + new Random().nextInt(40);
}
}
// TextItem.java
public class TextItem implements MulitTypeAdapter.IItem {
@Override
public int getType() {
return R.layout.item_text;
}
///////////////////////////////////////////
public final String content;
public TextItem() {
content = new Date().toString();
}
}
item_image.xml:
<layout>
<data>
<variable
name="item"
type="com.baurine.multitypeadaptertutorial.item.ImageItem"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:error="@{@drawable/ic_launcher}"
app:imageUrl="@{item.url}"
app:placeholder="@{@drawable/ic_launcher}"/>
</LinearLayout>
</layout>
// BindingUtil.java
public class BindingUtil {
@BindingAdapter({"imageUrl", "error", "placeholder"})
public static void loadImage(ImageView imgView,
String url,
Drawable error,
Drawable placeholder) {
Glide.with(imgView.getContext())
.load(url)
.error(error)
.placeholder(placeholder)
.into(imgView);
}
}
item_text.xml:
<layout>
<data>
<variable
name="item"
type="com.baurine.multitypeadaptertutorial.item.TextItem"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{item.content}"/>
</LinearLayout>
</layout>
在使用了 databinding 后,在创建 ViewHolder 时,ViewHolder 里需要保存就是不再是 itemView,而是 ViewDataBinding,每一个使用 <layout></layout>形式的 xml 布局都会被 databinding 框架自动生成一个 ViewDataBinding 类的派生类,比如 item_image.xml会生成 ItemImageBinding,item_text.xml会生成 ItemTextBinding,而 ViewDataBinding 是它们的基类。因此我们改写 ItemViewHolder/ImageViewHolder/TextViewHolder。
public abstract class ItemViewHolder extends RecyclerView.ViewHolder {
protected final ViewDataBinding binding;
public ItemViewHolder(ViewDataBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public abstract void bindTo(MulitTypeAdapter.IItem item);
}
public class ImageViewHolder extends ItemViewHolder {
public ImageViewHolder(ViewDataBinding binding) {
super(binding);
}
public void bindTo(MulitTypeAdapter.IItem item) {
ImageItem imageItem = (ImageItem) item;
((ItemImageBinding) binding).setItem(imageItem);
}
}
public class TextViewHolder extends ItemViewHolder {
public TextViewHolder(ViewDataBinding binding) {
super(binding);
}
public void bindTo(MulitTypeAdapter.IItem item) {
TextItem textItem = (TextItem) item;
((ItemTextBinding) binding).setItem(textItem);
}
}
此时,ViewHolderFactory 中的代码是这样的,我们要 inflate 得到ViewDataBinding,如下所示:
public class ViewHolderFactory {
public static ItemViewHolder create(ViewGroup parent, int viewType) {
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),
viewType, parent, false);
switch (viewType) {
case R.layout.item_image:
return new ImageViewHolder(binding);
case R.layout.item_text:
return new TextViewHolder(binding);
default:
return null;
}
}
}
public boolean setVariable(int variableId, Object variable) {
switch(variableId) {
case BR.item :
setItem((com.baurine.multitypeadaptertutorial.item.ImageItem) variable);
return true;
}
return false;
}
可见,这个方法就是对各种 setXyz方法的一层封装。而因为这个方法是由基类 ViewDataBinding 定义的,根据 OOP 的多态特性, 我们直接调用基类的 setVariable()方法即可,因此,ImageViewHolder 中的 bindTo()方法就可以简化成一行代码:
public void bindTo(MulitTypeAdapter.IItem item) {
binding.setVariable(BR.item, item);
}
而对于 TextViewHolder 来说,也是一样的。如此一来,如果我们在不同的 item xml 中使用相同的 variable name,如上例中都使用了 name="item",那么 bindTo()方法就可以统一成一种写法了,如上面所示。
ImageViewHolder 和 TextViewHolder 从形式上已经是一样的了,那我们就没有必要实现多个 ViewHolder 了,统一用一个 ItemViewHolder 来实现,在 setVariable()后执行 binding.executePendingBindings()
来让 UI 马上变化:
public class ItemViewHolder extends RecyclerView.ViewHolder {
private final ViewDataBinding binding;
public ItemViewHolder(ViewDataBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bindTo(MulitTypeAdapter.IItem item) {
binding.setVariable(BR.item, item);
binding.executePendingBindings();
}
}
但是我们一定要理解的是,单一 ViewHolder 的背后,是由 databinding 框架生成的多个 ViewDataBinding。总体上来说,代码量并没有减少,但对于我们开发者来说,要写的代码和逻辑确是大大减少了。
此时,ViewHolderFactory 可以简化成如下所示:
public class ViewHolderFactory {
public static ItemViewHolder create(ViewGroup parent, int viewType) {
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),
viewType, parent, false);
return new ItemViewHolder(binding);
}
}
但是实际上,由于我们并不需要多个 ViewHolder 了,这个工厂类也就失去意义了,我们把 create()这个方法移到 ItemViewHolder 中,删除 ViewHolderFactory 类,并修改 adapter 的 onCreateViewHolder()方法,如下所示:
// ItemViewHolder.java
public static ItemViewHolder create(ViewGroup parent, int viewType) {
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),
viewType, parent, false);
return new ItemViewHolder(binding);
}
// MulitTypeAdapter.java
public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return ItemViewHolder.create(parent, viewType);
}
public class MultiTypeAdapter extends RecyclerView.Adapter<MultiTypeAdapter.ItemViewHolder> {
public interface IItem {
// should directly return layout
int getType();
}
private List<IItem> items = new ArrayList<>();
///////////////////////////////////////////////////////
@Override
public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return ItemViewHolder.create(parent, viewType);
}
@Override
public void onBindViewHolder(ItemViewHolder holder, int position) {
holder.bindTo(items.get(position));
}
@Override
public int getItemCount() {
return items.size();
}
@Override
public int getItemViewType(int position) {
return items.get(position).getType();
}
///////////////////////////////////////////////////////
// operate items
public List<IItem> getItems() {
return items;
}
public void setItem(IItem item) {
clearItems();
addItem(item);
}
public void setItems(List<IItem> items) {
clearItems();
addItems(items);
}
public void addItem(IItem item) {
items.add(item);
}
public void addItem(IItem item, int index) {
items.add(index, item);
}
public void addItems(List<IItem> items) {
this.items.addAll(items);
}
public void removeItem(IItem item) {
items.remove(item);
}
public void clearItems() {
items.clear();
}
///////////////////////////////////////////////////
static class ItemViewHolder extends RecyclerView.ViewHolder {
private final ViewDataBinding binding;
static ItemViewHolder create(ViewGroup parent, int viewType) {
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),
viewType, parent, false);
return new ItemViewHolder(binding);
}
ItemViewHolder(ViewDataBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
void bindTo(MultiTypeAdapter.IItem item) {
binding.setVariable(BR.item, item);
binding.executePendingBindings();
}
}
}