本文原创,转载请注明链接:http://blog.kymjs.com/
在Android开发中写Adapter是一件非常麻烦的事情,枯燥重复,却又不得不去做。 对于Adapter一般都继承BaseAdapter复写几个方法,getView里面使用ViewHolder存储,其实大部分的代码都是类似的。那么本文就带大家一起做一次将Adapter封装成一个通用的Adapter。
关于本文的完整Demo,可以参考KJFrameForAndroid开发框架2.2版本中封装的实例,KJAdapter和AdapterHolder这两个类。
那么接下来我们进入正文,下面这个类似的代码应该是我们看的最多的:
public class EmojiGridAdapter extends BaseAdapter { private List<Emojicon> datas; private final Context cxt; public EmojiGridAdapter(Context cxt, List<Emojicon> datas) { this.cxt = cxt; if (datas == null) { datas = new ArrayList<Emojicon>(0); } this.datas = datas; } public void refresh(List<Emojicon> datas) { if (datas == null) { datas = new ArrayList<Emojicon>(0); } this.datas = datas; notifyDataSetChanged(); } @Override public int getCount() { return datas.size(); } @Override public Object getItem(int position) { return datas.get(position); } @Override public long getItemId(int position) { return position; } private static class ViewHolder { ImageView image; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder = null; if (convertView == null) { holder = new ViewHolder(); ...... convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } holder.image.setImageResource(datas.get(position).getResId()); return convertView; }}
初步抽取
其中BaseAdapter的四个方法必须写,但是基本上前三个都是一模一样的, 所以可以使用泛型,写一个基类出来,把数据封装到基类里面,只需要构造方法传入就行了
public class KJBaseAdapter<T> extends BaseAdapter { List<T> datas; KJBaseAdapter(Context cxt,List<T> datas){ ...... } @Override public int getCount() { return datas.size(); } @Override public Object getItem(int position) { return datas.get(position); } @Override public long getItemId(int position) { return position; }}
然后是我们唯一需要动脑的getView()方法,首先是判断converView是否空,然后载入item布局,然后ViewHolder挨个初始化控件,然后通过tag保存holder,最后设置View的显示。
步棸都知道了,那么我们慢慢来观察:ViewHolder一定是包含了item子控件的一个静态类。那么我们就干脆把item所有的子控件都放到ViewHolder里面,但是既然我们要通用,item肯定不是固定的,这就没办法把ViewHolder写的像上面的那种属性的形式。
这里我们使用一个键值对来存储Map<id, view>全部的控件,这样就可以在需要的时候直接通过id来找到对应的子View了。
mViews = new Map<Integaer, View>();/** * 通过控件的Id获取对于的控件,如果没有则加入views * * @param viewId * @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;}
封装ViewHolder
只看getView,其他方法都一样;首先调用ViewHolder的get方法,如果convertView为null,new一个ViewHolder实例,通过使用mInflater.
inflate加载布局,然后new一个HashMap用于存储View,最后setTag(this); 如果存在那么直接getTag最后通过getView(id)获取控件,如果存在则直接返回,否则调用findViewById,返回存储,返回。
那么最后封装好的ViewHolder就是这样的
public class AdapterHolder { private final Map<Integer,View> mViews; private final int mPosition; private final View mConvertView; private AdapterHolder(ViewGroup parent, int layoutId, int position) { this.mPosition = position; this.mViews = new HashMap<Integer, View>(); mConvertView = LayoutInflater.from(parent.getContext()).inflate( layoutId, parent, false); // setTag mConvertView.setTag(this); } /** * 拿到一个ViewHolder对象 */ public static AdapterHolder get(View convertView, ViewGroup parent, int layoutId, int position) { if (convertView == null) { return new AdapterHolder(parent, layoutId, position); } else { return (AdapterHolder) convertView.getTag(); } } /** * 通过控件的Id获取对于的控件,如果没有则加入views * * @param viewId * @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; }}
结合前面的基类,我们的Adapter就变成了这样的
public class EmojiGridAdapter<T> extends KJBaseAdapter<T> { @Override public View getView(int position, View convertView, ViewGroup parent){ ViewHolder viewHolder = ViewHolder.get(mContext, convertView, parent, R.layout.item_single_str, position); TextView mTitle = viewHolder.getView(R.id.id_tv_title); mTitle.setText((String) mDatas.get(position)); return viewHolder.getConvertView(); } }
最终的封装
再仔细观察,第一行的ViewHolder.get()和最后一行的return方法肯定也是不变的,果断进一步封装。
那么就完全可以是只需要抽出getView中可变的部分————通过ViewHolder把View找到,通过Item设置值;这一块单独写出来了。那么我们写一个方法就叫convert()来做这件事。至此代码简化到这样,剩下的已经不需要单独写一个Adapter了,直接Activity匿名内部类足够了。
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mListView = (ListView) findViewById(R.id.id_lv_main); //设置适配器 mListView.setAdapter(mAdapter = new CommonAdapter<String>( getApplicationContext(), mDatas, R.layout.item_single_str) { @Override public void convert(ViewHolder c, String item) { TextView view = viewHolder.getView(R.id.id_tv_title); view.setText(item); } }); }
最后的优化
现在我们再来对比KJFrameForAndroid中的封装,可以看到使用了SparseArray来替代我们的Map,SparseArray实际上就是一个拥有两个数组的类,第一个数组是一个int[],用来当做key,第二个就是泛型这里使用的是View[],它是google推荐用来替代int作为key的Map集合的一个类。
还有一个细节就是KJFrameForAndroid中的封装,加入了一个absListView属性,并设置了滚动监听,这样就可以很方便的在基类中实现例如listview滚动过程中不加载图片等功能。
最后我们封装好以后的代码:就可以直接查看KJAdapter和AdapterHolder这两个类。
以及使用方法可以参考KJBlog中的使用,例如:这里