您的位置:首页 > 移动开发 > Android开发

小白成长记——Android进阶之打造通用的适配器

2017-08-09 16:49 260 查看
LIstView、GridView和BaseAdapter在Android开发中可谓是再常见不过了。

每当我们需要用ListView或者GridView显示数据的时候都要编写一个Adapter适配器并绑定数据源,然后ListView或GridView实现Adapter适配器。那么,如果一个项目中出现多次ListView或是GridView等,是不是我们每个都要实现一遍创建适配器、绑定数据源、实现适配器的过程呢?答案当然是不需要,我们完全可以封装一个通用的适配器,大大提高了编程效率,同时减少代码冗余。

目标:

封装一个通用的ViewHolder类,用于对每个Item项中的各个控件进行操作;

实现一个CommonAdapter,封装一些通用的方法,每次需要使用的时候只需编写一个Adapter继承自CommonAdapter做一些个性化修改就行了。       

下面具体讲述实现过程:

1):item项的预期效果图:



具体XML代码实现比较简单,不做描述

2):根据实现需求定义存放数据的Bean类

public class Bean {
private String title;
private String desc;
private String time;
private boolean isChecked;

public boolean isChecked() {
return isChecked;
}

public void setChecked(boolean checked) {
isChecked = checked;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public String getDesc() {
return desc;
}

public void setDesc(String desc) {
this.desc = desc;
}

public String getTime() {
return time;
}

public void setTime(String time) {
this.time = time;
}

public Bean(String title, String time, String desc) {

this.title = title;
this.desc = desc;
this.time = time;
}

public Bean() {
}
}

3):首先回顾一下传统写法:

自定义MyAdapter类:

//自定义的Adapter
public class MyAdapter extends BaseAdapter {
private List<Bean> mDatas;
//创建布局加载器对象,用于加载Item项的布局文件
private LayoutInflater mInflater;
//含参构造方法,用于传入上下文、数据源以及初始化布局加载器
public MyAdapter(Context context, List<Bean> Datas) {
this.mDatas = Datas;
mInflater = LayoutInflater.from(context);
}

@Override
public int getCount() {
return mDatas.size();
}

@Override
public Object getItem(int i) {
return mDatas.get(i);
}

@Override
public long getItemId(int i) {
return i;
}

@Override
public View getView(int i, View view, ViewGroup viewGroup) {
ViewHolder viewHolder = null;
//判断缓存中是否已经存在View,为空则新建
if (view == null) {
view = mInflater.inflate(R.layout.list_item, viewGroup, false);
viewHolder = new ViewHolder();
viewHolder.mTitle = view.findViewById(R.id.title);
viewHolder.mDesc = view.findViewById(R.id.desc);
viewHolder.mTime = view.findViewById(R.id.time);
view.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) view.getTag();
}
//为各个控件加载数据
Bean bean = mDatas.get(i);
viewHolder.mTitle.setText(bean.getTitle());
viewHolder.mDesc.setText(bean.getDesc());
viewHolder.mTime.setText(bean.getTime());
return view;
}
//内部类ViewHolder,用来加载Item项中的各个控件
private class ViewHolder {
TextView mTitle;
TextView mDesc;
TextView mTime;
}
}

在Activity中进行数据源加载及适配器的绑定:

public class TestActivity extends AppCompatActivity {
private ListView mListView;
private List<Bean> beanList;
private MyAdapter mAdapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
mListView = (ListView) findViewById(R.id.listView);
initData();
mAdapter = new MyAdapter(this,beanList);
//ListView绑定Adapter
mListView.setAdapter(mAdapter);
}
//模拟添加数据源
private void initData() {
beanList = new ArrayList<Bean>();
for (int i = 0; i < 20; i++) {
Bean bean = new Bean("标题" + i, "2014-12-" + (i + 11), "内容描述" + i);
beanList.add(bean);
}
}
}

运行之后可以正常显示无误。

回到一开始的问题,如果我一个项目中需要多次用到ListView或者GridView加载数据,每次都这样写是不是很繁琐?那么,到底怎样打造一个通用的适配器呢?

4).抽取通用的ViewHolder类:

分析ViewHolder的作用,就是实现Item中各个控件的引用,通过view.setTag(viewHolder)加载到ListView的各个Item中。

那么,我们抽取的时候要想获得控件该怎么做呢?我们可以通过类似于Map的键值对存储形式,以控件的Id对应该控件。因为存储形式为int - Object,所以我们采用效率更高的sparseArray来存储。

(1).首先创建ViewHolder类,生成构造方法并传入必要参数

public class ViewHolder {
private SparseArray<View> mViews;
private View mConvertView;
private int mPosition;

public ViewHolder(Context context, ViewGroup parent, int layoutId, int position) {
this.mViews = new SparseArray<View>();
this.mPosition = position;
//为mConvertView加载布局
mConvertView = LayoutInflater.from(context).inflate(layoutId, parent, false);
//为mConvertView实现布局
mConvertView.setTag(this);
}

(2).因为不确定convertView缓存是否有值,需要写一个入口方法进行判断

private static ViewHolder get(Context context, View convertView, ViewGroup parent, int layoutId, int position) {
if (convertView == null) {
return new ViewHolder(context, parent, layoutId, position);
}
ViewHolder viewHolder = (ViewHolder) convertView.getTag();
return viewHolder;
}


(3).为了方便获取convertView,为它写一个get方法

public View getConvertView() {
return mConvertView;
}

(4).然后是通过控件Id获取控件的方法

public <T extends View> T getView(int viewId) {
View view = mViews.get(viewId);
if (view == null) {
view = mConvertView.findViewById(viewId);
mViews.put(viewId, view);
}
return (T) view;
}

如果是第一次调用,view为空,通过findViewById找到对应控件并存入SparseArray中,以后可以直接从中通过Id获取

(5).这样,MyAdapter中ge'tView方法就可以简化为:

public View getView(int i, View view, ViewGroup viewGroup) {
ViewHolder viewHolder = ViewHolder.get(mContext, view, viewGroup, R.layout.list_item, i);
//为各个控件加载数据
Bean bean = mDatas.get(i);
TextView title = viewHolder.getView(R.id.title);
title.setText(bean.getTitle());
TextView desc = viewHolder.getView(R.id.desc);
desc.setText(bean.getDesc());
TextView time = viewHolder.getView(R.id.time);
time.setText(bean.getTime());
return viewHolder.getConvertView();
}

记得最后的返回值一定要改为viewHolder.getConvertView()
运行结果正常。

5).通用CommonAdapter的抽取:

分析MyAdapter中,多次使用的情况下除了最后的getView方法不同,其他三个方法getCount、getItem、getItemId基本相同。那么,我们就可以将它们抽取出来生成一个抽象类,只将它的getView方法公布出来。

具体代码如下:

public abstract class CommonAdapter<T> extends BaseAdapter {
protected Context mContext;
protected List<T> mDatas;
protected LayoutInflater mInflater;

public CommonAdapter(Context context, List<T> datas) {
this.mContext = context;
this.mDatas = datas;
mInflater = LayoutInflater.from(context);
}

@Override
public int getCount() {
return mDatas.size();
}

@Override
public Object getItem(int i) {
return mDatas.get(i);
}

@Override
public long getItemId(int i) {
return i;
}

@Override
public abstract View getView(int i, View view, ViewGroup viewGroup);
}

然后我们的MyAdapter可以进一步的简化:

public class MyAdapter extends CommonAdapter<Bean> {

public MyAdapter(Context context, List<Bean> Datas) {
super(context, Datas);
}

@Override
public View getView(int i, View view, ViewGroup viewGroup) { ViewHolder viewHolder = ViewHolder.get(mContext, view, viewGroup, R.layout.list_item, i); //为各个控件加载数据 Bean bean = mDatas.get(i); TextView title = viewHolder.getView(R.id.title); title.setText(bean.getTitle()); TextView desc = viewHolder.getView(R.id.desc); desc.setText(bean.getDesc()); TextView time = viewHolder.getView(R.id.time); time.setText(bean.getTime()); return viewHolder.getConvertView(); }
}

到此,一个通用适配器的雏形就基本完成了,我们依然可以对其进行简化升级。

6).简化:

分析MyAdapter中的getView方法,每次需要真正实现的只是控件的加载与设置,开始的ViewHolder对象实例化以及最后的返回convertView都可以抽取到CommonAdapter中,那么我们就将getView方法也抽取到CommonAdapter中,只在其中公布一个供用户设置控件的方法

具体实现:

public View getView(int i, View view, ViewGroup viewGroup) {
ViewHolder viewHolder = ViewHolder.get(mContext, view, viewGroup, R.layout.list_item, i);
convert(viewHolder, getItem(i));
return viewHolder.getConvertView();
}

public abstract void convert(ViewHolder viewHolder, T t);

MyAdapter中的进一步简化:

public class MyAdapter extends CommonAdapter<Bean> {

public MyAdapter(Context context, List<Bean> Datas) {
super(context, Datas);
}

@Override
public void convert(ViewHolder viewHolder, Bean bean) {
TextView title = viewHolder.getView(R.id.title);
title.setText(bean.getTitle());
TextView desc = viewHolder.getView(R.id.desc);
desc.setText(bean.getDesc());
TextView time = viewHolder.getView(R.id.time);
time.setText(bean.getTime());
}
}

我们甚至可以直接在Activity中直接以匿名内部类的方式完成这些操作。

7).进一步优化:

分析convert方法中每个TextView的setText方法,我们是不是可以把它也抽取到ViewHolder中,只需要我们传入控件Id和要设置的文本参数就行呢?

//设置TextView显示文本
public ViewHolder setText(int viewId, String text) {
TextView view = getView(viewId);
view.setText(text);
return this;
}

像这样,在ViewHolder中写一个setText方法,我们就可以在MyAdapter中进一步简化:

public void convert(ViewHolder viewHolder, Bean bean) {
viewHolder.setText(R.id.title, bean.getTitle())
.setText(R.id.desc, bean.getDesc())
.setText(R.id.time, bean.getTime());
}

甚至一需要一条语句就完成。当然,不仅仅是TextView的setText方法,我们可以根据实际需求在ViewHolder中封装任何我们需要的方法,例如给ImageView设置图片:

//设置ImageView显示图片
public ViewHolder setImageResource(int viewId, int resId) {
ImageView view = getView(viewId);
view.setImageResource(resId);
return this;
}

回顾代码发现有一个很僵硬的地方,我把Item的布局文件在CommonAdapter中写死了,做一下修改,定义一个layoutId的变量,在构造方法中设置layoutId参数,这样只需在创建My Adapter实例的时候传入布局文件即可,我会在最后贴上完整代码以供参考。

8).BUG修复:

在实际使用中,我们会发现ListView的Item项会出现抢占焦点问题,例如我在布局中添加一个CheckBox控件,运行之后会发生checkBox可以点击,但Item项不能正常点击的问题,这就是checkBox抢占焦点导致的。那么,我们如何解决呢?

第一种解决方案:

在XML文件中给CheckBox设置 andoroid:focusable = "false" ,然后运行会发现Item和CheckBox都能正常点击

第二种解决方案:

在XML文件中给最外层布局容器设置 android:descendantsFocusability = "blocksDescendants",同样可以解决问题

同样以CheckBox为例,我们实际运行时会发现,当我选中了第一个Item中的CheckBox之后,向下滑动列表会发现有Item项中的CheckBox明明之前没有选中,但是它呈现的是选中状态,这就是convert的复用机制导致的。

解决办法:

在Bean中设置isChecked变量,生成对应get、set方法;在MyAdapter的convert方法中写入:

final CheckBox cb = viewHolder.getView(R.id.checkbox);
cb.setChecked(bean.isChecked());
cb.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
bean.setChecked(cb.isChecked());
}
});
对每一个checkBox的选中状态进行记录,这样问题就解决了。

至此,一个通用的适配器就打造完成。下面贴上源码:

View Holder:

public class ViewHolder {
private SparseArray<View> mViews;
private View mConvertView;
private int mPosition;

/**
* @param context 上下文
* @param parent 父容器
* @param layoutId 每一个Item项的布局文件
* @param position Item项的位置
*/
public ViewHolder(Context context, ViewGroup parent, int layoutId, int position) {
this.mViews = new SparseArray<View>();
this.mPosition = position;
//为mConvertView加载布局
mConvertView = LayoutInflater.from(context).inflate(layoutId, parent, false);
//为mConvertView实现布局
mConvertView.setTag(this);
}

/**
* 入口方法,对传入的convertView进行判断,如果convertView为空 -> new ViewHolder,如果不为空直接复用convertView
*
* @param context
* @param convertView Adapter中传入的参数,表示系统缓存View
* @param parent
* @param layoutId
* @param position
* @return
*/
public static ViewHolder get(Context context, View convertView, ViewGroup parent, int layoutId, int position) {
if (convertView == null) {
return new ViewHolder(context, parent, layoutId, position);
}
ViewHolder viewHolder = (ViewHolder) convertView.getTag();
viewHolder.mPosition = position;
return viewHolder;
}

public View getConvertView() { return mConvertView; }

/**
* 通过viewId获取控件,灵活使用泛型
*
* @param viewId
* @param <T>
* @return
*/
public <T extends View> T getView(int viewId) { View view = mViews.get(viewId); if (view == null) { view = mConvertView.findViewById(viewId); mViews.put(viewId, view); } return (T) view; }

//设置TextView显示文本 public ViewHolder setText(int viewId, String text) { TextView view = getView(viewId); view.setText(text); return this; }

//设置ImageView显示图片 public ViewHolder setImageResource(int viewId, int resId) { ImageView view = getView(viewId); view.setImageResource(resId); return this; }
}

CommonAdapter:

public abstract class CommonAdapter<T> extends BaseAdapter {
protected Context mContext;
protected List<T> mDatas;
protected LayoutInflater mInflater;
protected int mLayoutId;

public CommonAdapter(Context context, List<T> datas, int layoutId) {
this.mContext = context;
this.mDatas = datas;
mInflater = LayoutInflater.from(context);
this.mLayoutId = layoutId;
}

@Override
public int getCount() {
return mDatas.size();
}

@Override
public T getItem(int i) {
return mDatas.get(i);
}

@Override
public long getItemId(int i) {
return i;
}

@Override
public View getView(int i, View view, ViewGroup viewGroup) {
ViewHolder viewHolder = ViewHolder.get(mContext, view, viewGroup, mLayoutId, i);
convert(viewHolder, getItem(i));
return viewHolder.getConvertView();
}

public abstract void convert(ViewHolder viewHolder, T t);
}

MyAdapter:

public class MyAdapter extends CommonAdapter<Bean> {

public MyAdapter(Context context, List<Bean> Datas, int layoutId) {
super(context, Datas, layoutId);
}

@Override
public void convert(ViewHolder viewHolder, final Bean bean) {
viewHolder.setText(R.id.title, bean.getTitle())
.setText(R.id.desc, bean.getDesc())
.setText(R.id.time, bean.getTime());
final CheckBox cb = viewHolder.getView(R.id.checkbox); cb.setChecked(bean.isChecked()); cb.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { bean.setChecked(cb.isChecked()); } });
}
}

Bean:

public class Bean { private String title; private String desc; private String time; private boolean isChecked; public boolean isChecked() { return isChecked; } public void setChecked(boolean checked) { isChecked = checked; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } public String getTime() { return time; } public void setTime(String time) { this.time = time; } public Bean(String title, String time, String desc) { this.title = title; this.desc = desc; this.time = time; } public Bean() { } }

Activity:

public class TestActivity extends AppCompatActivity {
private ListView mListView;
private List<Bean> beanList;
private MyAdapter mAdapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
mListView = (ListView) findViewById(R.id.listView);
initData();
mAdapter = new MyAdapter(this, beanList, R.layout.list_item);
//ListView绑定Adapter
mListView.setAdapter(mAdapter);
}

//模拟添加数据源
private void initData() {
beanList = new ArrayList<Bean>();
for (int i = 0; i < 20; i++) {
Bean bean = new Bean("标题" + i, "2014-12-" + (i + 11), "内容描述" + i);
beanList.add(bean);
}
}
}

欢迎各位学习中的朋友交流探讨,也希望大神们能够不吝指导。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android Adapter ViewHolder