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

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);
        }
    }
}

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




  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值