先看下效果图
![]()
【引入】
我们一般编写listView的时候顺序是这样的: •需要展示的数据集List<T> •为这个数据集编写一个ListView •为这个ListView编写一个Adapter,一般继承自BaseAdapter •在BaseAdapter内部编写一个ViewHolder类,对应ListView里面的item控件,提高控件的查询效率
分析:
List<T>:ListView --> Adapter extends BaseAdapter --> ViewHolder
一般情况下,一个ListView对应一个Adapter类,对应一个ViewHolder类,那如果一个app中有20个ListView,我们岂不是要写20遍?所以的做法是: •抽取ViewHolder,作为公共的类。 •将Adapter封装成CommonAdapter,作为公共的类。 处理多条目android给我们的方法是设置setViewTypeCount传入类型个数,RecycleBin会创建对应数量的mScrapViews集合数组,每种类型的View在对应的集合中管理。当要告诉ListView我要显示什么样的UI布局时就得调用getItemViewType,给每个position指定要使用的ViewType类型。 但是要注意如果返回错误就会有问题,例如你不能返回超过setViewTypeCount的值,否则会数组脚本越界。ListView根据getItemViewType就能找到缓存该ViewTypewhichScrap然后来渲染UI这就是android给我们提供的ListView多条目混排的实现方案,如果我们不进行封装,扩展的话,会导致一个Adapter中有N多个ViewType,getView的时候会根据positon返回N多种,然后再在getItemViewType方法中根据position返回不同的ViewType,还有定义不同的ViewHolder来管理。这样就会导致我们的一个Adapter类爆棚,满屏都是if else想想就可怕。 下面就是用传统的方法实现三种Item的adapter,我已经精简了很多。每次添加新条目,就要改动这个Adapter,扩展性很差,当代码多了,很容易出bug,也不好查找。不符合开放封闭原则
public class ChatAdapter extends BaseAdapter { //3种不同的布局 public static final int VALUE_TIME_TIP = 0; public static final int VALUE_LEFT_TEXT = 1; public static final int VALUE_LEFT_IMAGE = 2; public ChatAdapter(Context context, List<Message> myList) { this.myList = myList; mInflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); } @Override public int getCount() { return myList.size(); } @Override public Object getItem(int arg0) { return myList.get(arg0); } @Override public long getItemId(int arg0) { return arg0; } @Override public View getView(int position, View convertView, ViewGroup arg2) { if (convertView == null) { switch (type) { case VALUE_TIME_TIP: .. case VALUE_LEFT_TEXT: ... case VALUE_LEFT_IMAGE: ... } else { Log.d("baseAdapter", "Adapter_:"+(convertView == null) ); switch (type) { case VALUE_TIME_TIP: ... case VALUE_LEFT_TEXT: ... case VALUE_LEFT_IMAGE: ... } } return convertView; } //根据数据源的position返回需要显示的的layout的type @Override public int getItemViewType(int position) { Message msg = myList.get(position); int type = msg.getType(); return type; } //返回所有的layout的数量 @Override public int getViewTypeCount() { return 3; } class ViewHolderTime { ... } class ViewHolderRightText { ... } class ViewHolderRightImg { ... } }
首先分析下ViewHolder的作用,通过convertView.setTag与convertView进行绑定,然后当convertView复用时,直接从与之对应的ViewHolder(getTag)中拿到convertView布局中的控件,省去了findViewById的时间。这里我们先打造一个通用的Adapter并抽出一个公用的ViewHolder
public abstract class CommonListAdapter<T> extends BaseAdapter { protected Context mContext; protected List<T> mDatas; public CommonListAdapter(Context context, List<T> datas) { mDatas = datas; mContext = context; } /** * @return 这里应理解为UI的条目数量 */ @Override public int getCount() { return mDatas.size(); } @Override public T 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) { return getItemListView(position, convertView, parent); } private View getItemListView(int position, View convertView, ViewGroup parent) { ViewHolder holder = ViewHolder.get(mContext, convertView, parent, getItemLayoutId(position)); onBindHolder(holder, position, parent); return holder.getItemView(); } /** * 返回创建条目view所需的res * * @param position * @return */ protected abstract int getItemLayoutId(int position); /** * 这里可以实现数据的加载,事件的添加,界面的显隐等操作 * @param holder * @param position 对应的数据的位置 * @param parent */ public abstract void onBindHolder(ViewHolder holder, int position, ViewGroup parent); }
这样我们就打造了一个通用的adapter,并把ViewHolder与Adapter分离,我们再看看这个通用的ViewHolder。既然是通用的,那我们就不能像以前一样将成员变量写进去了,因为每个Item的布局是不同的,那我们怎么办呢?既然没有,那我们就提供一个方法,帮调用者查找,同时维护一个集合,键为控件的Id,值为控件引用,集合里找不到我们再去findViewByid,这里我们用SparseArray,他比Map集合更高效,内部是二分查找法。但是key只能是Integer,我们给每个mItemView设置一个Tag,再绑定一个ViewHolder对象(this)。
public class ViewHolder { private SparseArray<View> mViews; private View mItemView; private ViewHolder(Context context, ViewGroup parent, int layoutId) { LayoutInflater inflater = LayoutInflater.from(context); mItemView = inflater.inflate(layoutId, parent, false); mViews = new SparseArray<View>(); mItemView.setTag(R.id.item_holder, this); } public View getItemView() { return mItemView; } public static ViewHolder get(Context context, View itemView, ViewGroup parent, int layoutId) { if (itemView == null) return new ViewHolder(context, parent, layoutId); else return (ViewHolder) itemView.getTag(R.id.item_holder); } public <T extends View> T getView(int id) { View view = mViews.get(id); if (view == null) { view = mItemView.findViewById(id); mViews.put(id, view); } return (T) view; } public ViewHolder setText(int id, CharSequence sequence) { TextView view = getView(id); if (view != null) view.setText(sequence); return this; } public ViewHolder show(int id) { View view = getView(id); if (view != null) ViewUtils.setGone(view, false); return this; } public ViewHolder hide(int id) { View view = getView(id); if (view != null) ViewUtils.setGone(view, true); return this; } }
这样我们就打造了一个通用的ViewHolder,它负责管理每个ItemView控件,同时也可以看成一个工具类,以后可以随意添加。
有了通用的Adapter与通用的ViewHolder,那我们以后写代码就方便多了,接下来我们就开始实现多条目功能
其实我们仔细想想,不同的条目也就是我们的数据不同,需要的UI不同而已,每个数据对应一个UI,但是我们怎才能让ListView知道加载什么UI?它只认识getViewTypeCount与getItemViewType。那我们只有将不同UI的layoutId转换成ListView认识的getItemViewType,这就需要一个转换ViewTypeGenerator
class ViewTypeGenerator implements IViewTypeInfo{ private Map<Integer, Integer> mAdapterTypeMap; private int mNextType = 1; private boolean mNeedReGenerate; public ViewTypeGenerator() { mAdapterTypeMap = new HashMap<Integer, Integer>(); mNeedReGenerate = true; } @Override public void setNeedReGenerate(boolean needReGenerate) { mNeedReGenerate = needReGenerate; } public int reGenViewType(int key) { if (!mNeedReGenerate) { return key; } Integer resultType = mAdapterTypeMap.get(key); if (resultType == null || resultType <= 0) { resultType = mNextType++; mAdapterTypeMap.put(key, resultType); } return resultType; } }
ViewTypeGenerator维护一个集合,它可以将我们的layoutId转换成连续的int值,满足getItemViewType方法
我们来看看使用方法:
public class MixListAdapter extends CommonListAdapter<BaseBean> { private ViewTypeGenerator mTypeGenerator; private IViewHandler viewHandler; private BaseBean itemData; public MixListAdapter(Context context, List datas) { super(context, datas); mTypeGenerator = new ViewTypeGenerator(); } /** * 那么此处的adapter只要保证返回一个独有的int值即可,不需要限制在[0, getViewTypeCount)之间 * @return */ @Override public int getItemViewType(int position) { IViewHandler viewHandler = ViewHandlerFactory.getViewHandler(getItem(position).getViewHandlerType()); int viewHandlerItemViewType = viewHandler.getItemViewType(); int itemViewType = mTypeGenerator.reGenViewType(viewHandlerItemViewType); if (itemViewType > getViewTypeCount()) throw new RuntimeException("条目类型数量超过了预设值:" + getViewTypeCount() + ",请扩大预设值"); return itemViewType ; } public int getViewTypeCount() { return 20;//根据需要,预设一个值,不要小于你的Item类型总数 } @Override protected int getItemLayoutId(int position) { //注释1.获取数据 itemData = getItem(position); //注释2.获取ViewHandler viewHandler = ViewHandlerFactory.getViewHandler(itemData.getViewHandlerType()); return viewHandler.getItemViewType(); } @Override public void onBindHolder(ViewHolder holder, int position, ViewGroup parent) { viewHandler.handle(parent, holder, itemData, position); } }
注释2处大家可能有点不明白,这也是最重要,最想说明的地方,我是不想把View绑定数据(handle方法),返回布局layoutId(getItemViewLayoutId方法)、设置ItemViewType(getItemViewType方法)都放在子类Adapter中所以抽出一个IViewHandler接口,统一处理.item数据绑定一个viewHandlerType属性,是IViewHandler子类的Class名,我们就可以在IViewHandler子类中返回LayoutId给View绑定数据,以后新加item类型,也不用改动adapter,只要
新建一个IViewHandler的子类再给数据绑定该类的Class文件名就可以了。
/** * 处理ListView的不同类型条目的显示 */ public interface IViewHandler<T> { /** * 回调函数,用于处理ListView的条目显示 */ void handle(ViewGroup parent, ViewHolder holder, T data, int position); /** * 返回Item对应的布局资源 */ int getItemViewLayoutId(); /** * 设置ItemViewType,用于ListView的标记回收 */ int getItemViewType(); }
看看反射类
/** * 统一生成ViewHandler的工厂类 */ public class ViewHandlerFactory { private static final String TAG = "[ViewHandlerFactory:wangsai]"; /** * 缓存已存在的handler */ private static Map<String, IViewHandler> mHandlerMap = new HashMap<String, IViewHandler>(); /** * 通过类名动态加载创建、加载对应的类对象 */ public static IViewHandler getViewHandler(String viewHandlerClazz) { IViewHandler result = mHandlerMap.get(viewHandlerClazz); if (result == null) { try { Class clazz = Class.forName(viewHandlerClazz); result = (IViewHandler) clazz.newInstance(); mHandlerMap.put(viewHandlerClazz, result); } catch (Exception e) { DebugLog.e(TAG, e.toString()); } } if(result == null) throw new RuntimeException("IViewHandler创建失败:" + viewHandlerClazz); return result; } }
贴一个子类看看
public class LeftViewHandler implements IViewHandler<BaseBean> { @Override public void handle(ViewGroup parent, ViewHolder holder, BaseBean data, int position) { holder.setText(R.id.tv,data.getName()); } @Override public int getItemViewLayoutId() { return R.layout.simple_common_left; } @Override public int getItemViewType() { return R.layout.simple_common_left; } } 对数据的处理 public List<BaseBean> getData(){ data = new ArrayList<BaseBean>(); for (int i = 0 ; i < 100 ; i++){ BaseBean b = new BaseBean(); b.setViewHandlerType(LeftViewHandler.class.getName()); b.setName("item:"+i); data.add(b); } return data; }
我们以后对于多条目的混排,只需要给每个Data设定不同的Class文件名,然后在这个文件中绑定数据就Ok了
最后我们通过UML来梳理下
![]()
源码可能跟文章里面的不一样,注释换成英文的了
作者:yangzai
链接:https://www.jianshu.com/p/db482efac590
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。