Databinding之RecyclerViewAdapter使用与封装
Databinding之RecyclerViewAdapter使用与封装
RecyclerViewAdapter大家都不陌生,那么在使用Databinding时,RecyclerViewAdapter该如何编写呢?
本文用一个邮箱类型列表作为案例,来讲解在使用Databinding时如何编写RecyclerViewAdapter,并且如何有效的封装RecyclerViewAdapter。
简单使用
首先编写MailType类,text和icon分别表示邮箱类型的名称以及图标。
/**
* Created by gongw on 2018/7/11.
*/
public class MailType {
private String text;
private String icon;
public MailType(String text, String icon)
{
this.text = text;
this.icon = icon;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public String getIcon() {
return icon;
}
public void setIcon(String icon) {
this.icon = icon;
}
}
编写MailTypeModell类,通过getDatas()提供邮箱类型数据。
/**
* Created by gongw on 2018/7/11.
*/
public class MailTypeModel {
private static class InstanceHolder {
static final MailTypeModel INSTANCE = new MailTypeModel();
}
private MailTypeModel(){}
public static MailTypeModel getInstance(){
return InstanceHolder.INSTANCE;
}
public List<MailType> getDatas(){
List<MailType> datas = new ArrayList<>();
String[] types = BaseApplication.getContext().getResources().getStringArray(R.array.mail_type);
for(String type : types){
String[] mailType = type.split("=");
datas.add(new MailType(mailType[0], mailType[1]));
}
return datas;
}
}
这里使用一个简单的StringArray作为数据源,用“=“分割邮箱类型的名称和图标资源名称。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<array name="mail_type">
<item>QQ邮箱=icqqmail</item>
<item>139邮箱=ic139mail</item>
<item>189邮箱=ic189mail</item>
<item>阿里邮箱=ic_alimail</item>
</array>
</resources>
编写邮箱类型的item布局。
这里使用了Databinding的一个自定义属性的功能,为ImageView设置了一个drawable的属性,功能是通过图片名称获取资源id,并为imageview设置图片。
@BindingAdapter({"drawable"})
public static void setImage(ImageView imageView, String icon){
int r_id = imageView.getResources().getIdentifier(icon, "drawable", imageView.getContext().getPackageName());
imageView.setImageResource(r_id);
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="mailType"
type="com.gongw.login.model.MailType"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:orientation="horizontal"
android:paddingStart="26dp"
android:paddingEnd="26dp">
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center_vertical"
app:drawable="@{mailType.icon}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginStart="26dp"
android:layout_marginLeft="26dp"
android:gravity="center_vertical"
android:singleLine="true"
android:text="@{mailType.text}" />
</LinearLayout>
</layout>
编写MailTypeAdapter,可以看到使用Databinding可以有效的减少Adapter中代码的行数,在onBindViewHolder方法中,只需要一行binding.setMailType即可为item布局中的视图设置数据。
/**
* Created by gongw on 2018/7/14.
*/
public class MailTypeAdapter extends RecyclerView.Adapter<MailTypeAdapter.MailTypeViewHolder> {
private List<MailType> itemDatas;
public MailTypeAdapter(List<MailType> itemDatas){
this.itemDatas = itemDatas;
}
@Override
public MailTypeAdapter.MailTypeViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ItemMailTypeBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.item_mail_type, parent, false);
return new MailTypeViewHolder(binding);
}
@Override
public void onBindViewHolder(MailTypeAdapter.MailTypeViewHolder holder, int position) {
holder.binding.setMailType(itemDatas.get(position));
holder.binding.executePendingBindings();
}
@Override
public int getItemCount() {
return itemDatas.size();
}
class MailTypeViewHolder extends RecyclerView.ViewHolder{
ItemMailTypeBinding binding;
public MailTypeViewHolder(ItemMailTypeBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
}
将MailType数据通过MailTypeAdapter绑定到RecyclerView。
public class MailTypeFragment extends BaseFragment {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
FragmentMailTypeBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_mail_type, container, false);
RecyclerView recyclerView = binding.recyclerView;
recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext(), LinearLayoutManager.VERTICAL, false));
MailTypeAdapter adapter = new MailTypeAdapter(MailTypeModel.getInstance().getDatas());
recyclerView.setAdapter(adapter);
return binding.getRoot();
}
}
运行效果
通用Adapter封装
上面我们已经成功用Databinding实现了RecyclerviewAdapter的使用,但上面的写法存在几个问题:
- 上面的写法把Item的布局和item的类型写死在了Adapter中,如果我们想再实现一个使用其他的item布局展示其他数据的列表,上面的Adapter就无法使用。
- 无法使用多布局,项目中经常存在一个列表中使用多种布局的场景,遇到这种场景,上面的Adapter也无法使用。
- 没有留出设置各类事件回调的方法,比如设置item的点击事件等等。
鉴于上面的问题,这里我们尝试编写一个适用于任何场景的Adapter类。
首先,来解决问题1。
为了能适用于不同的数据,我们不能将数据类型写死在Adapter中,需要用泛型替代具体的数据类型,其次,item的布局也不能写死,需要从构造方法中注入。
在封装过程中会遇到这样一个问题,由于Databinding是与布局绑定的,布局的不固定会导致binding在设置数据时无法用具体的binding.setMailType这样的写法,只能通过binding.setVariable的方式,而这个方法需要提供一个BR id,所以BR id也需要从构造方法中注入。
下面是具体的实现
/**
* Created by gongw on 2018/7/13.
*/
public class SimpleAdapter<T> extends RecyclerView.Adapter<SimpleAdapter.BaseViewHolder> {
private List<T> itemDatas;
private int layoutId;
private int brId;
public SimpleAdapter(List<T> itemDatas, int layoutId, int brId){
this.itemDatas = itemDatas;
this.layoutId = layoutId;
this.brId = brId;
}
@Override
public SimpleAdapter.BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), layoutId, parent, false);
return new BaseViewHolder(binding);
}
@Override
public void onBindViewHolder(SimpleAdapter.BaseViewHolder holder, int position) {
holder.binding.setVariable(brId, itemDatas.get(position));
holder.binding.executePendingBindings();
}
@Override
public int getItemCount() {
return itemDatas == null ? 0 : itemDatas.size();
}
class BaseViewHolder extends RecyclerView.ViewHolder {
ViewDataBinding binding;
BaseViewHolder(ViewDataBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
}
通过上面的修改,SimpleAdapter已经可以适用于不同的数据类型和item布局的情况了,成功解决问题1;
现在再来看问题2,如何使Adapter适配多布局的场景。
这里假定一个这样的场景,当邮箱类型的是QQ邮箱时,需要用第二种item布局,将邮箱的图标和文字位置互换。
我们都知道,要适配多种布局需要实现RecyclerViewAdapter的getItemViewType方法,通过不同的viewType来对应不同的item布局。这里我们可以取个巧,因为viewType和item布局都是int类型,所以可以在getItemViewType方法中直接返回item布局id,而具体返回哪个item布局id则交由外部实现。
这里定义一个getItemLayout的方法,默认返回构造方法传入的item布局。外部可以通过重写这个方法来修改返回item布局的逻辑,以此应对多布局的场景。
public class SimpleAdapter<T> extends RecyclerView.Adapter<SimpleAdapter.BaseViewHolder> {
private List<T> itemDatas;
private int defaultLayout;
private int brId;
public SimpleAdapter(List<T> itemDatas, int defaultLayout, int brId){
this.itemDatas = itemDatas;
this.defaultLayout = defaultLayout;
this.brId = brId;
}
public int getItemLayout(T itemData){
return defaultLayout;
}
@Override
public int getItemViewType(int position) {
return getItemLayout(itemDatas.get(position));
}
@Override
public SimpleAdapter.BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), viewType, parent, false);
return new BaseViewHolder(binding);
}
@Override
public void onBindViewHolder(SimpleAdapter.BaseViewHolder holder, int position) {
holder.binding.setVariable(brId, itemDatas.get(position));
holder.binding.executePendingBindings();
}
@Override
public int getItemCount() {
return itemDatas == null ? 0 : itemDatas.size();
}
class BaseViewHolder extends RecyclerView.ViewHolder {
ViewDataBinding binding;
BaseViewHolder(ViewDataBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
}
重写getItemLayout方法,返回Adapter应该使用的item布局,这里如果是QQ邮箱就使用R.layout.item_mail_type1,否则使用R.layout.item_mail_type作为item布局。
/**
* Created by gongw on 2018/7/10.
*/
public class MailTypeFragment extends BaseFragment {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
FragmentMailTypeBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_mail_type, container, false);
RecyclerView recyclerView = binding.recyclerView;
recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext(), LinearLayoutManager.VERTICAL, false));
SimpleAdapter<MailType> adapter = new SimpleAdapter<MailType>(MailTypeModel.getInstance().getDatas(), R.layout.item_mail_type, BR.mailType){
@Override
public int getItemLayout(MailType itemData) {
return itemData.getText().equals("QQ邮箱") ? R.layout.item_mail_type1 : R.layout.item_mail_type;
}
};
recyclerView.setAdapter(adapter);
return binding.getRoot();
}
}
编写第二种item布局R.layout.item_mail_type1。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="mailType"
type="com.gongw.login.model.MailType"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/item_height_normal"
android:orientation="horizontal"
android:paddingStart="26dp"
android:paddingEnd="26dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginStart="26dp"
android:layout_marginLeft="26dp"
android:gravity="center_vertical"
android:singleLine="true"
android:text="@{mailType.text}" />
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center_vertical"
app:drawable="@{mailType.icon}"/>
</LinearLayout>
</layout>
运行效果,可以看到QQ邮箱使用的布局与其他邮箱是不同的
通过上面的修改,SimpleAdapter可以通过重写getItemLayout来应对多布局的场景了,问题2解决;
现在再来看问题3,需要提供设置item事件回调的方法。
事件回调封装的思想主要有两点:首先需要在onBindViewHolder中设置回调,因为只有在这个方法中才可以拿到具体的view和position。其次,item布局中可能有多个不同的view,每个view都应该可以设置自己的事件回调,而且事件回调的类型根据view的类型可能有很多种,如一般view都有的onclick,checkbox的onCheckedChange,srcollview的onScrollChange等等,这些类型无法用一个抽象的参数来表示。
基于以上两点,我决定将事件绑定的整套逻辑交由外部实现,adapter向外部提供事件绑定所需的资源即view、itemData和position。这里提供一个空方法addListener供外部实现,onBindViewHolder中通过调用addListener来设置事件回调。
/**
* Created by gongw on 2018/7/13.
*/
public class SimpleAdapter<T> extends RecyclerView.Adapter<SimpleAdapter.BaseViewHolder> {
private List<T> itemDatas;
private int defaultLayout;
private int brId;
public SimpleAdapter(List<T> itemDatas, int defaultLayout, int brId){
this.itemDatas = itemDatas;
this.defaultLayout = defaultLayout;
this.brId = brId;
}
public int getItemLayout(T itemData){
return defaultLayout;
}
public void addListener(View root, T itemData, int position){}
@Override
public int getItemViewType(int position) {
return getItemLayout(itemDatas.get(position));
}
@Override
public SimpleAdapter.BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), viewType, parent, false);
return new BaseViewHolder(binding);
}
@Override
public void onBindViewHolder(SimpleAdapter.BaseViewHolder holder, int position) {
holder.binding.setVariable(brId, itemDatas.get(position));
addListener(holder.binding.getRoot(), itemDatas.get(position), position);
holder.binding.executePendingBindings();
}
@Override
public int getItemCount() {
return itemDatas == null ? 0 : itemDatas.size();
}
class BaseViewHolder extends RecyclerView.ViewHolder {
ViewDataBinding binding;
BaseViewHolder(ViewDataBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
}
重写addListener方法,自行设置item中具体view的具体事件。
/**
* Created by gongw on 2018/7/10.
*/
public class MailTypeFragment extends BaseFragment {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
FragmentMailTypeBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_mail_type, container, false);
RecyclerView recyclerView = binding.recyclerView;
recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext(), LinearLayoutManager.VERTICAL, false));
recyclerView.addItemDecoration(new RecyclerViewDivider(recyclerView.getContext(), LinearLayoutManager.VERTICAL));
SimpleAdapter<MailType> adapter = new SimpleAdapter<MailType>(MailTypeModel.getInstance().getDatas(), R.layout.item_mail_type, BR.mailType){
@Override
public void addListener(View root, MailType itemData, final int position) {
root.findViewById(R.id.textView).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getActivity(), "textView clicked!", Toast.LENGTH_SHORT).show();
}
});
root.findViewById(R.id.imageView).setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Toast.makeText(getActivity(), "imageView long clicked!", Toast.LENGTH_SHORT).show();
return true;
}
});
}
};
recyclerView.setAdapter(adapter);
return binding.getRoot();
}
}
通过上面的修改,SimpleAdapter可以通过重写addListener方法来设置多种类型的事件回调了,问题3解决;
除了上面提到的问题,一个成熟的RecyclerViewAdapter还应该提供一些常用的方法供项目中方便使用,如item数据变化、范围变化、范围增加、范围删除时,Adapter自动刷新列表的方法。
public void onItemDatasChanged(List<T> newItemDatas){
this.itemDatas = newItemDatas;
notifyDataSetChanged();
}
protected void onItemRangeChanged(List<T> newItemDatas, int positionStart, int itemCount)
{
this.itemDatas = newItemDatas;
notifyItemRangeChanged(positionStart,itemCount);
}
protected void onItemRangeInserted(List<T> newItemDatas, int positionStart, int itemCount)
{
this.itemDatas = newItemDatas;
notifyItemRangeInserted(positionStart,itemCount);
}
protected void onItemRangeRemoved(List<T> newItemDatas, int positionStart, int itemCount)
{
this.itemDatas = newItemDatas;
notifyItemRangeRemoved(positionStart,itemCount);
}
最终得出的产物如下,所有代码加起来只有80多行,却可以应对项目中的绝大多数场景重复使用,也可以说得上短小精悍了。
package com.gongw.common.adapter;
import android.databinding.DataBindingUtil;
import android.databinding.ViewDataBinding;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.List;
/**
* Created by gongw on 2018/7/13.
*/
public class SimpleAdapter<T> extends RecyclerView.Adapter<SimpleAdapter.BaseViewHolder> {
private List<T> itemDatas;
private int defaultLayout;
private int brId;
public SimpleAdapter(List<T> itemDatas, int defaultLayout, int brId){
this.itemDatas = itemDatas;
this.defaultLayout = defaultLayout;
this.brId = brId;
}
public int getItemLayout(T itemData){
return defaultLayout;
}
public void addListener(View root, T itemData, int position){}
public void onItemDatasChanged(List<T> newItemDatas){
this.itemDatas = newItemDatas;
notifyDataSetChanged();
}
protected void onItemRangeChanged(List<T> newItemDatas, int positionStart, int itemCount)
{
this.itemDatas = newItemDatas;
notifyItemRangeChanged(positionStart,itemCount);
}
protected void onItemRangeInserted(List<T> newItemDatas, int positionStart, int itemCount)
{
this.itemDatas = newItemDatas;
notifyItemRangeInserted(positionStart,itemCount);
}
protected void onItemRangeRemoved(List<T> newItemDatas, int positionStart, int itemCount)
{
this.itemDatas = newItemDatas;
notifyItemRangeRemoved(positionStart,itemCount);
}
@Override
public int getItemViewType(int position) {
return getItemLayout(itemDatas.get(position));
}
@Override
public SimpleAdapter.BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), viewType, parent, false);
return new BaseViewHolder(binding);
}
@Override
public void onBindViewHolder(SimpleAdapter.BaseViewHolder holder, int position) {
holder.binding.setVariable(brId, itemDatas.get(position));
addListener(holder.binding.getRoot(), itemDatas.get(position), position);
holder.binding.executePendingBindings();
}
@Override
public int getItemCount() {
return itemDatas == null ? 0 : itemDatas.size();
}
class BaseViewHolder extends RecyclerView.ViewHolder {
ViewDataBinding binding;
BaseViewHolder(ViewDataBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
}