Goole 推荐 Android 程序员使用的 ConvertView + ViewHolder 的模式已经被众人所熟知,现已成为最基础的设计方式,能提升至少 70% 的性能。可以说,写 ListView 不用 ViewHolder 都不能算是合格的 Android 程序员。
但是,本文并不打算介绍传统的 ConvertView + ViewHolder 的模式,也不打算直接贴代码敷衍了事,而是站在代码优化的角度,从最基本的 BaseAdapter,教你如何一步步搭建一个通用的万能适配器,最终将引出 Android 中非常重要的”面向 Holder 的编程思想”。
本文全部示例代码,请移步 github
尽管,传统的 ConvertView + ViewHolder 模式能提高性能,但是存在以下缺点:
传统的模式将 ViewHolder 直接作为内部类写在 Activity 里,而每一个 Adapter 必须配备一个 ViewHolder,这种方式是难以维护、管理和扩展的。
getView 方法,基本是做重复的操作,无非这么几步
传统的模式将控件的 findViewById、赋值操作等业务逻辑,写在一个 getView 方法内,一旦布局中的控件数量一多,getView 方法就变得臃肿无比,就像一个大胖子。例如曾经开发的一个电商项目的购物车逻辑代码量 2000 行左右,其中 getView 方法占了近 1200 行。
传统的代码如下
public class SecondActivity extends Activity{
private ListView mList;
private List<AppInfo> mDatas;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
// 初始化
init();
}
/* 初始化 ListView */
private void init() {
mList = (ListView) findViewById(R.id.list);
mDatas = new ArrayList<AppInfo>();
mList.setAdapter(new MyAdapter());
}
private class MyAdapter extends BaseAdapter{
@Override
public int getCount() {
return mDatas == null ? 0 : mDatas.size();
}
@Override
public Object getItem(int position) {
return mDatas.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null){
convertView = View.inflate(SecondActivity.this, R.layout.item_appinfo, null);
holder = new ViewHolder();
holder.item_icon = (ImageView) findViewById(R.id.item_icon);
holder.item_content = (TextView) findViewById(R.id.item_content);
holder.item_title = (TextView) findViewById(R.id.item_title);
holder.item_rating = (RatingBar) findViewById(R.id.item_rating);
holder.item_size = (TextView) findViewById(R.id.item_size);
holder.item_bottom = (TextView) findViewById(R.id.item_bottom);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
AppInfo info = mDatas.get(position);
holder.item_icon.setImageResource(R.drawable.ic_action_search);
holder.item_content.setText(info.getDes());
holder.item_title.setText(info.getName());
holder.item_rating.setProgress((int)info.getStars());
holder.item_size.setText( Formatter.formatFileSize(SecondActivity.this, info.getSize()) );
holder.item_bottom.setText(info.getDes());
return convertView;
}
}
private class ViewHolder {
ImageView item_icon;
TextView item_content;
TextView item_title;
RatingBar item_rating;
TextView item_size;
TextView item_bottom;
}
}
看了上边一坨代码,是否感觉非常臃肿。好吧,下面我们开始优化之旅
第一步,就是把 View 创建相关的 inflate 、 findViewById,移到 ViewHolder 的构造方法,代码就变成了这样。
getView() 方法:
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null){
holder = new ViewHolder();
} else {
holder = (ViewHolder) convertView.getTag();
}
AppInfo info = mDatas.get(position);
holder.item_icon.setImageResource(R.drawable.ic_action_search);
holder.item_content.setText(info.getDes());
holder.item_title.setText(info.getName());
holder.item_rating.setProgress((int)info.getStars());
holder.item_size.setText( Formatter.formatFileSize(SecondActivity.this, info.getSize()) );
holder.item_bottom.setText(info.getDes());
return convertView;
}
ViewHolder:
private class ViewHolder {
private View contentView;
ImageView item_icon;
TextView item_content;
TextView item_title;
RatingBar item_rating;
TextView item_size;
TextView item_bottom;
public ViewHolder(){
// 初始化布局
contentView = View.inflate(SecondActivity.this, R.layout.item_appinfo, null);
// 初始化控件
item_icon = (ImageView) contentView。findViewById(R.id.item_icon) ;
item_content = (TextView) contentView。findViewById(R.id.item_content) ;
item_title = (TextView) contentView。findViewById(R.id.item_title) ;
item_rating = (RatingBar) contentView。findViewById(R.id.item_rating) ;
item_size = (TextView) findViewById(R.id.item_size) ;
item_bottom = (TextView) contentView。findViewById(R.id.item_bottom) ;
// 寄存 ViewHolder 到布局中
contentView.setTag(this);
}
}
这一步,主要是:
将控件的赋值放到 ViewHolder 内单独的 setData 方法中,别忘了在 getView 方法调用 setData
为 ViewHolder 创建 getContentView,对外提供根布局
getView 方法返回的是 holder 中保存的布局
getView():
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
holder = new ViewHolder();
} else {
holder = (ViewHolder) convertView.getTag();
}
AppInfo info = mDatas.get(position);
holder.setData(info);
// 返回 ViewHolder 保存的布局
return holder.getContentView();
}
ViewHolder:
private class ViewHolder {
private View contentView;
ImageView item_icon;
TextView item_content;
TextView item_title;
RatingBar item_rating;
TextView item_size;
TextView item_bottom;
public ViewHolder() {
contentView = initView();
contentView.setTag(this);
}
/** 初始化布局 */
public View initView() {
View view = View.inflate(SecondActivity.this,R.layout.item_appinfo, null);
item_icon = (ImageView) contentView.findViewById(R.id.item_icon);
item_content = (TextView) contentView.findViewById(R.id.item_content);
item_title = (TextView) contentView.findViewById(R.id.item_title);
item_rating = (RatingBar) contentView.findViewById(R.id.item_rating);
item_size = (TextView) contentView.findViewById(R.id.item_size);
item_bottom = (TextView) contentView.findViewById(R.id.item_bottom);
return view;
}
public void setData(AppInfo data){
item_icon.setImageResource(R.drawable.ic_action_search);
item_content.setText(data.getDes());
item_title.setText(data.getName());
item_rating.setProgress((int) data.getStars());
item_size.setText(Formatter.formatFileSize(SecondActivity.this, data.getSize()));
item_bottom.setText(data.getDes());
}
public View getContentView() {
return contentView;
}
}
现在,我们有了 ViewHolder,并且 ViewHolder 的构造方法、initView 还有 setData 方法是固定的。那么就可以将这些共性抽取到 BaseViewHolder
BaseViewHolder 类的主要作用是:
BaseViewHolder:
public abstract class BaseViewHolder<T>{
private View contentView;
public BaseViewHolder(){
contentView = initView();
contentView.setTag(this);
}
public abstract View initView();
public abstract void setData(T data);
public View getContentView() {
return contentView;
}
}
ViewHolder:
private class ViewHolder extends BaseViewHolder<AppInfo>{
ImageView item_icon;
TextView item_content;
TextView item_title;
RatingBar item_rating;
TextView item_size;
TextView item_bottom;
/** 初始化布局 */
@Override
public View initView() {
View view = View.inflate(SecondActivity.this,
R.layout.item_appinfo, null);
item_icon = (ImageView) contentView.findViewById(R.id.item_icon);
item_content = (TextView) contentView.findViewById(R.id.item_content);
item_title = (TextView) contentView.findViewById(R.id.item_title);
item_rating = (RatingBar) contentView.findViewById(R.id.item_rating);
item_size = (TextView) contentView.findViewById(R.id.item_size);
item_bottom = (TextView) contentView.findViewById(R.id.item_bottom);
return view;
}
@Override
public void setData(AppInfo data) {
item_icon.setImageResource(R.drawable.ic_action_search);
item_content.setText(data.getDes());
item_title.setText(data.getName());
item_rating.setProgress((int) data.getStars());
item_size.setText(Formatter.formatFileSize(SecondActivity.this,data.getSize()));
item_bottom.setText(data.getDes());
}
}
对于不同的业务逻辑,不同的布局控件,都要创建对应 View 对象,十分麻烦。所以,我们采用的 Android SDK 提供的 SparseArray,优化赋值操作。
这一步:
1. 定义了 SparseArray 对象,存储 View 对象
2. 创建 setImage、setText,以便为控件赋值
BaseViewHolder:
abstract class BaseViewHolder<T>{
private View contentView;
private SparseArray<View> mViews;
public BaseViewHolder(){
contentView = initView();
contentView.setTag(this);
mViews = new SparseArray<View>();
}
public abstract View initView();
public abstract void setData(T data);
public View getContentView() {
return contentView;
}
/** 设置文本 */
public void setText(int resId, String content){
TextView textView = (TextView) mViews.get(resId);
if (textView == null){
textView = (TextView) contentView.findViewById(resId);
mViews.put(resId, textView);
}
textView.setText(content);
}
/** 设置图片 */
public void setImage(int id, int resId){
ImageView imageView = (ImageView) mViews.get(id);
if (imageView == null){
imageView = (ImageView) contentView.findViewById(id);
mViews.put(id, imageView);
}
imageView.setImageResource(resId);
}
/** 获取 view 对象 */
public View getView(int resId){
View view = mViews.get(resId);
if (view == null){
view = contentView.findViewById(resId);
mViews.put(resId, view);
}
return view;
}
}
ViewHolder:
private class ViewHolder extends BaseViewHolder<AppInfo>{
/** 初始化布局 */
@Override
public View initView() {
View view = View.inflate(SecondActivity.this,R.layout.item_appinfo, null);
return view;
}
@Override
public void setData(AppInfo data) {
setImage(R.id.item_icon,R.drawable.ic_action_search);
setText(R.id.item_title, data.getName());
setText(R.id.item_size,Formatter.formatFileSize(SecondActivity.this,data.getSize()));
setText(R.id.item_bottom, data.getDes());
}
}
经过上面几步的操作,我们的小框架基本成型。我们发现 MyAdapter 的 getItem、getItemId、getCount、getView 几个方法具有共性,这一步,主要为这些方法抽取父类。
需要注意:
1. DefaultAdapter 应当添加泛型 T,以代表各种实体类
2. 数据集合通过构造方法传给 DefaultAdapter
3. getView 中的 holder 交给父类 BaseViewHolder 接收(多态)
DefaultAdapter:
private class DefaultAdapter<T> extends BaseAdapter{
private List<T> mDatas;
public DefaultAdapter(List<T> mDatas) {
this.mDatas = mDatas;
}
@Override
public int getCount() {
return mDatas == null ? 0 : mDatas.size();
}
@Override
public Object getItem(int position) {
return mDatas.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
BaseViewHolder holder = null;
if (convertView == null) {
holder = new ViewHolder();
} else {
holder = (ViewHolder) convertView.getTag();
}
T info = mDatas.get(position);
holder.setData(info);
return holder.getContentView();
}
}
MyAdapter:
private class MyAdapter extends DefaultAdapter<AppInfo> {
public MyAdapter(List<AppInfo> mDatas) {
super(mDatas);
}
}
经过 抽取适配器共性到 DefaultAdapter ,其实这个框架基本已经完成了。但还是每次要创建 子类 ViewHolder 去实现 initView 创建各自的布局。能不能更优?答案是能。
这一步,主要做了:
1. 将布局的资源 id 通过 DefaultAdapter 的构造方法传入,并传递给 BaseViewHolder
DefaultAdapter:
abstract class DefaultAdapter<T> extends BaseAdapter{
private List<T> mDatas;
private int mLayoutId;
public DefaultAdapter(List<T> mDatas, int layoutId) {
this.mDatas = mDatas;
this.mLayoutId = layoutId;
}
// getItem、getItemId、getCount略
@Override
public View getView(int position, View convertView, ViewGroup parent) {
BaseViewHolder holder = null;
if (convertView == null) {
holder = new BaseViewHolder<T>(mLayoutId) {
@Override
public void setData(T data) {
setHolder(this, data);
}
};
} else {
holder = (BaseViewHolder) convertView.getTag();
}
T info = mDatas.get(position);
holder.setData(info);
return holder.getContentView();
}
protected abstract void setHolder(BaseViewHolder holder, T data);
}
终于到了成果展示的时候,看到这下面的代码,你会发现,我们的辛苦是值的。
private void init() {
mList = (ListView) findViewById(R.id.list);
mDataSet = new ArrayList<AppInfo>();
mList.setAdapter(new MyAdapter(mDataSet, R.layout.item_appinfo));
}
private class MyAdapter extends DefaultAdapter<AppInfo> {
public MyAdapter(List<AppInfo> mDatas, int layoutId) {
super(mDatas, layoutId);
}
@Override
protected void setHolder(BaseViewHolder holder, AppInfo data) {
holder.setImage(R.id.item_icon, R.drawable.ic_action_search);
holder.setText(R.id.item_title, data.getName());
holder.setText(R.id.item_size,Formatter.formatFileSize(SecondActivity.this,data.getSize()));
holder.setText(R.id.item_bottom, data.getDes());
}
}
代码瞬间从上百行,减少到十几行,而且 DefaultAdapter 和 BaseViewHolder 可以直接拷到项目中使用。
尽管这个框架功能强大,能大大减少代码量,但就像毕向东所说,凡事都有两面性,代码量是少了,扩展性也差了。遇到特别复杂的需求,这两个类就显得力不从心。以后,我会抽时间再写篇文章,讲述怎么修改这个适配器框架以适应不同需求,力求在”方便”和”灵活”之间找到一个平衡点,将代码优化到极致!
下一篇:ListView 优化篇:从 BaseViewHolder 到面向 Holder 的思想
示例代码:https://github.com/heshiweij/BaseViewHolder