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

来源:互联网 发布:js根据id获取对象的值 编辑:程序博客网 时间:2024/05/20 03:45

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类:

//自定义的Adapterpublic 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);        }    }}

欢迎各位学习中的朋友交流探讨,也希望大神们能够不吝指导。




阅读全文
1 0