一、概述
写项目也有一段时间了,每次写ListView或者是GridView时,只要item内容不同,就要写不同的自定义adapter,感觉很麻烦,其实这些代码都可以抽出来,我们需要做的只是改写getView方法和ViewHolder,那下面我们就开始吧。
有兴趣的可以去这里看看:https://github.com/JoanZapata/base-adapter-helper
二、逻辑实现
首先我们先写一个简单的联系人布局如下图:
1. 从标准自定义adapter写法开始
下面就是我认为比较标准的自定义adapter写法,我们从这一步一步开始分析哪些可以抽出来,避免重写代码。
/**
* 适配器的标准写法
*
* @author LiYang
*
*/
public class ContantAdapter extends BaseAdapter {
private Context context;
private List<ContantBean> list;
public ContantAdapter(Context context, List<ContantBean> list) {
this.context = context;
this.list = list;
}
@Override
public int getCount() {
return list.size();
}
@Override
public ContantBean getItem(int position) {
return list.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
holder = new ViewHolder();
convertView = LayoutInflater.from(context).inflate(
R.layout.item_main, null);
holder.ivIcon = (ImageView) convertView
.findViewById(R.id.iv_item_icon);
holder.tvNumber = (TextView) convertView
.findViewById(R.id.tv_item_number);
holder.cb = (CheckBox) convertView.findViewById(R.id.cb_item_check);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
ContantBean bean = list.get(position);
holder.ivIcon.setImageResource(bean.getImgRes());
holder.tvNumber.setText(bean.getNumber());
holder.cb.setChecked(bean.isChecked());
return convertView;
}
class ViewHolder {
ImageView ivIcon;
TextView tvNumber;
CheckBox cb;
}
}
MainActivity代码:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView lv = (ListView) findViewById(R.id.lv);
List<ContantBean> list = new ArrayList<ContantBean>();
ContantBean bean;
for (int i = 0; i < 5; i++) {
// 循环获取资源文件中类似的资源id,如图片icon1,icon2...
int imgRes = getResources().getIdentifier("icon" + (i + 1),
"drawable", getPackageName());
bean = new ContantBean(imgRes, "131-1565-156" + i, true);
list.add(bean);
}
lv.setAdapter(new ContantAdapter(this, list));
}
}
可以发现,继承BaseAdapter中重写的getCount()、getItem(int position)、getItemId(int position)这三个方法的写法是相对固定的,我们可以把它抽出来。
我们现在就可以开始写通用的adapter了,新建类MyBaseAdapter,我们上面分析了,重写的四个方法中只有getView()是我们每次都需要重写的,因为这个方法负责了每个item的展示和数据的加载显示,我们可以先把getView这个代码块的实现放到外部,如下:
2. getView()方法的抽出——加载数据到视图
我们会发现,getView()中代码挺多的,具体该怎么抽出来呢,我们可以先把 视图加载数据这块代码抽出来,写成一个内部接口,并在构造方法中以参数传入,这样在初始化adapter就会实现这个接口,通过接口回调实现getView()代码块:
public class MyBaseAdapter extends BaseAdapter {
...
private LoadData loadData;
public MyBaseAdapter(Context context, List<ContantBean> list,
LoadData loadData) {
...
this.loadData = loadData;
}
public interface LoadData {
/**
* 加载数据到视图
*/
void convert(ViewHolder holder, ContantBean bean);
}
...
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
holder = new ViewHolder();
convertView = LayoutInflater.from(context).inflate(
R.layout.item_main, null);
holder.ivIcon = (ImageView) convertView
.findViewById(R.id.iv_item_icon);
holder.tvNumber = (TextView) convertView
.findViewById(R.id.tv_item_number);
holder.cb = (CheckBox) convertView.findViewById(R.id.cb_item_check);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
ContantBean bean = list.get(position);
loadData.convert(holder, bean);
// holder.ivIcon.setImageResource(bean.getImgRes());
// holder.tvNumber.setText(bean.getNumber());
// holder.cb.setChecked(bean.isChecked());
return convertView;
}
public class ViewHolder {...}
改变的就是加入一个接口,构造方法中传入,在getView()方法中回调接口方法convert,从而把数据加载到视图抽出来了,相应的MainActivity中也要变化:
MyBaseAdapter adapter = new MyBaseAdapter(this, list, new LoadData() {
@Override
public void convert(ViewHolder holder, ContantBean bean) {
holder.ivIcon.setImageResource(bean.getImgRes());
holder.tvNumber.setText(bean.getNumber());
holder.cb.setChecked(bean.isChecked());
}
});
lv.setAdapter(adapter);
// lv.setAdapter(new ContantAdapter(this, list));
我们发现上面的显然很不合理,ViewHolder负责的是整个item里面的所有需要改变的控件的初始化以及为了优化和convertView绑定这两部分逻辑,我们可以把这个逻辑都放在ViewHolder里面实现;
3. getView方法继续的抽出-通用ViewHolder
具体实现思路:
- ViewHolder包含一个item中的所有需要使用的控件,所以我们需要有一个集合(SparseArray)来保存这个控件,从而把每个控件初始化(即findViewById)的过程放到ViewHolder中实现;
- 在ViewHolder中实现ListView优化;
- 其他一些细节的实现。
代码如下,注解很详细:
/**
* 通用ViewHolder
*
* @author LiYang
*
*/
public class MyViewHolder {
/**
* 键值对存放view视图,键-int,值-View
*/
private SparseArray<View> views;
/**
* item布局视图
*/
private View convertView;
/**
* 私有构造方法,进行初始化操作,类的初始化放在静态方法getViewHolder中
*/
private MyViewHolder(ViewGroup parent, int resId, Context context) {
views = new SparseArray<View>();
convertView = LayoutInflater.from(context)
.inflate(resId, parent, false);
getConvertView().setTag(this);
}
/**
* 静态方法,获取一个MyViewHolder实例,从getView()方法中的逻辑思路来写,先判断convertView是否为空,
* 为空就先初始化convertView
* 、初始化ViewHolder、绑定ViewHolder到view上,主要就是获取到ViewHolder实例来把数据加载到视图上
*
*/
public static MyViewHolder getViewHolder(View convertView,
ViewGroup parent, int resId, Context context) {
if (convertView == null) {
return new MyViewHolder(parent, resId, context);
}
return (MyViewHolder) convertView.getTag();
}
/**
* 通过资源ID获取控件
*
* @param resId
* @return
*/
public View getView(int resId) {
View v = views.get(resId);
if (v == null) {
v = getConvertView().findViewById(resId);
views.put(resId, v);
}
return v;
}
public View getConvertView() {
return convertView;
}
}
当然,改变了ViewHolder的写法,相应的Adapter也得修改,新增的就是布局文件要传入,还有就是把getView()中方法体都提取出来, 变为下列形式:
public class MyBaseAdapter extends BaseAdapter {
...
private int resId;
public MyBaseAdapter(Context context, List<ContantBean> list,
LoadData loadData, int resId) {
...
this.resId = resId;
}
public interface LoadData {
void convert(MyViewHolder holder, ContantBean bean);
}
...
@Override
public View getView(int position, View convertView, ViewGroup parent) {
MyViewHolder holder = MyViewHolder.getViewHolder(convertView, parent,
resId, context);
loadData.convert(holder, list.get(position));
return holder.getConvertView();
}
// public class ViewHolder {
// public ImageView ivIcon;
// public TextView tvNumber;
// public CheckBox cb;
// }
}
4.最后的改造
查看上面我们写的代码,发现几处需要改进的地方:
- 使用接口的方式比较麻烦,可以改为抽象类,这样在初始化时就必须实现抽象方法convert;
- 通用ViewHolder可以实现更多的一些方法,比如设置字体,设置背景等等;
- 可以试着支持更多的数据类型,而不是自己写的ContantBean;
- 代码可以更加简洁。
最终代码如下:
MyBaseAdapter:
/**
* 通用Adapter
*
* @author LiYang
* @param <T>
* 要填充的数据bean
*
*/
public abstract class MyBaseAdapter<T> extends BaseAdapter {
private Context context;
private List<T> list;
private int resId;
public MyBaseAdapter(Context context, List<T> list, int resId) {
this.context = context;
this.list = list;
this.resId = resId;
}
/**
* 加载数据到视图,在初始化时必须实现该方法
*
* @param holder
* 视图项
* @param bean
* 数据项
*/
public abstract void convert(MyViewHolder holder, T bean);
@Override
public int getCount() {
return list.size();
}
@Override
public T getItem(int position) {
return list.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
MyViewHolder holder = MyViewHolder.getViewHolder(convertView, parent,
resId, context);
convert(holder, list.get(position));
return holder.getConvertView();
}
}
MyViewHolder:
/**
* 通用ViewHolder
*
* @author LiYang
*
*/
public class MyViewHolder {
/**
* 键值对存放view视图,键-int,值-View
*/
private SparseArray<View> views;
/**
* item布局视图
*/
private View convertView;
/**
* 私有构造方法,进行初始化操作,类的初始化放在静态方法getViewHolder中
*/
private MyViewHolder(ViewGroup parent, int resId, Context context) {
views = new SparseArray<View>();
convertView = LayoutInflater.from(context)
.inflate(resId, parent, false);
getConvertView().setTag(this);
}
/**
* 静态方法,获取一个MyViewHolder实例,从getView()方法中的逻辑思路来写,先判断convertView是否为空,
* 为空就先初始化convertView
* 、初始化ViewHolder、绑定ViewHolder到view上,主要就是获取到ViewHolder实例来把数据加载到视图上
*
*/
public static MyViewHolder getViewHolder(View convertView,
ViewGroup parent, int resId, Context context) {
if (convertView == null) {
return new MyViewHolder(parent, resId, context);
}
return (MyViewHolder) convertView.getTag();
}
/**
* 通过资源ID获取控件
*
* @param viewId
* @return
*/
@SuppressWarnings("unchecked")
public <T extends View> T getView(int viewId) {
View v = views.get(viewId);
if (v == null) {
v = getConvertView().findViewById(viewId);
views.put(viewId, v);
}
return (T) v;
}
public View getConvertView() {
return convertView;
}
/*-----------以下是我们可以扩充的方法-------------*/
public void setBackgroundResource(int viewId, int resId) {
getView(viewId).setBackgroundResource(resId);
}
public void setText(int viewId, String text) {
TextView v = getView(viewId);
v.setText(text);
}
public void setImageResource(int viewId, int resId) {
ImageView iv = getView(viewId);
iv.setImageResource(resId);
}
}
三、简单的使用
使用就很简单了,直接通过初始化,把布局和list集合穿进去,实现convert方法即可,如下:
MyBaseAdapter<ContantBean> adapter = new MyBaseAdapter<ContantBean>(
this, list, R.layout.item_main) {
@Override
public void convert(MyViewHolder holder, ContantBean bean) {
holder.setText(R.id.tv_item_number, bean.getNumber());
holder.setImageResource(R.id.iv_item_icon, bean.getImgRes());
((CheckBox) holder.getView(R.id.cb_item_check))
.setChecked(false);
}
};
lv.setAdapter(adapter);
源代码在这:下载