首页 > 代码库 > Android好奇宝宝_04_一个有3个功能的Adapter
Android好奇宝宝_04_一个有3个功能的Adapter
感觉Android好奇宝宝这个系列是脱离不了ListView和GridView了。。。
这一篇呢来分享点好东西
一个自定义Adapter,可以快速实现三个功能:
(1)自动缓存处理
好吧,这个功能不是我实现的。我只是照搬鸿洋大大的,我会简单说下,不过还是请先看下他的原文,再来看我添加的两个功能,传送门
(2)支持item的不同布局
提供一个接口来通过position和该position的数据来设置不同的布局
(3)局部刷新
只刷新指定item的某个子View,避免一直调用notifyDataSetChanged()造成不必要的整体刷新。
(1)自动缓存处理
基本有点android开发经验的都知道在自定义Adapter时可以用ViewHolder来缓存item的view,从而提高运行效率。
反正现在我的Eclipse在我没用ViewHolder的时候还会像个小婊砸一样提醒我:大爷,要ViewHolder吗?很爽的哦!
虽然程序员一般只关注错误,不怎么鸟警告,但有些警告还是挺有用的,所以大爷我一般都会屈服,你说怎样就怎样嘛。
不过写得多了,就会发现很多重复的代码。
对于重复的代码,我们应该像对待马赛克一样对待,坚决消灭!
现在,就让我们一点点的来消灭它们,还这世界一片高清。
详情请关注鸿洋大大的博文:这是传送门,我真心懒得重新造车轮
(2)支持不同的item布局
这个其实只是在鸿洋大大的源码上做了一点修改,请看高清源码:
首先,定义一个接口,用来提供不同的布局id
public interface LayoutIdProvider<T> { int getLayoutId(int position, T itemData); }
然后给Adapter两个构造方法,其中第一个在只有一种布局时使用,第二个在有多种布局时使用。
/** * 一种布局时使用 * @param context 上下文 * @param data 数据源 * @param layoutId item的布局id */ public SmartAdapter(Context context, ArrayList<T> data, int layoutId) { this.mContext = context; this.mDatas = data; this.mLayoutId = layoutId; } /** * 多种布局时使用 * @param context 上下文 * @param data 数据源 * @param layoutIdProvider 布局提供者 */ public SmartAdapter(Context context, ArrayList<T> data, LayoutIdProvider<T> layoutIdProvider) { this.mContext = context; this.mDatas = data; this.mLayoutIdProvider = layoutIdProvider; }
接下来修改getView使用什么布局
public View getView(int position, View convertView, ViewGroup parent) { Log.e("SmartAdapter-getView", "" + position); int layoutId; if (mLayoutId == -1) { // mLayoutId==-1说明是多种布局模式 // 使用mLayoutIdProvider来获得相应的布局 layoutId = mLayoutIdProvider.getLayoutId(position, mDatas.get(position)); } else { // 单布局模式 layoutId = mLayoutId; } //取得itemView中保存的ViewHolder,类似我们经常做的 //convertView。getTag() //只是ViewHolder.get在取得为空时会自动new一个然后setTag ViewHolder holder = ViewHolder.get(mContext, convertView, parent, layoutId, position); //由外部实现,产生item的view makeItemView(layoutId, position, holder, mDatas.get(position)); return holder.getConvertView(); }
最后,因为不同layoutId的Viewholder肯定不能混着用,所以再修改一下ViewHolder
int layoutID;// 记录ViewHolder对应的布局 public static ViewHolder get(Context context, View convertView, ViewGroup parent, int layoutId, int position) { if (convertView == null) { // convertView为空,直接new一个 return new ViewHolder(context, parent, layoutId, position); } // convertView不为空,取出convertView中的ViewHolder ViewHolder holder = (ViewHolder) convertView.getTag(); // 如果是相同布局的ViewHolder,可以复用,直接返回holder if (holder.layoutID == layoutId) { return holder; } // 如果是不同布局的,new一个返回 else { return new ViewHolder(context, parent, layoutId, position); } }
完成。
来实现一个简单聊天界面看下怎么用。
效果图:
(纯属虚构,如有雷同,你来打我啊)
看看关键代码:
(1)首先模拟聊天消息类,没啥好说的:
public class ChatMessage { public int fromUserId; public int headResId; public String content; public boolean sendSuccess; }
(3)new 一个布局提供者,就是通过判断positiong或该position的数据来决定那个item要使用什么布局:
LayoutIdProvider<ChatMessage> mLayoutIdProvider = new LayoutIdProvider<ChatMessage>() { @Override public int getLayoutId(int position, ChatMessage itemData) { if (itemData.fromUserId == myUserId) { // 是本人发的消息用右布局 return R.layout.list_item_right; } else { // 非本人发的消息用左布局 return R.layout.list_item_left; } } };
(4)new一个SmartAdapter:
mAdapter = new SmartAdapter<ChatMessage>(this, datas, mLayoutIdProvider) { @Override public void makeItemView(int layoutId, int positon, ViewHolder holder, ChatMessage itemData) { holder.setBackGroundResourceToView(R.id.img_head, itemData.headResId); holder.setTextToTextView(R.id.lv_item_tv, itemData.content); if (itemData.sendSuccess) { holder.setVisibility(R.id.img_gth, View.GONE); } else { holder.setVisibility(R.id.img_gth, View.VISIBLE); } // 也可以用switch对不同布局进行不同设置 // switch (layoutId) { // case R.layout.list_item_left: // break; // case R.layout.list_item_right: // break; // } } };
这里因为左右布局的控件我用了一样的id,所以不用按不同布局进行不同处理。但更多时候还是得用注释掉的那种方式来。
(5)lv.setAdapter(mAdapter);
直接设置mAdapter,最后面有源码下载。
(3)局部刷新
先说下应用场景,一般要对某个item中的控件的属性进行修改时的做法是先修改数据源,然后在调用notifyDataSetChanged()进行刷新。
这种做法虽然可以满足需求,但是也存在一些问题,除了那个我们想修改的item外,其它的item也进行了不必要的刷新,即所有可见的item的getView方法都会被调用。如果item中有图片的话,还可能造成图片一闪一闪的,影响用户体验。
解决办法是想办法直接获取到我们要修改的控件的引用,而控件又是被包裹在item的view中,所以可以先想办法获得item的view。
那么现在问题来了,在知道position的情况下怎么获得对应item的view?
答:
View itemView=absListView.getChildAt(posotion-absListView.getFirstVisiblePosition());
不知道为什么的请参考我另一篇博文:传送门
获得item的view之后,如果你用的是这一篇讲的Adapter,那么每个item的View都保存了一个ViewHolder,所以可以直接用ViewHolder里保存的引用:
ViewHolder holder = (ViewHolder) itemView.getTag(); View widgetView = holder.getView(widgetViewId);
如果没有用我们的Adapter的话也可以用findViewById,或者是自定义的holder的话也可以直接取出holder中的引用。
取得引用之后就可以直接改变widgetView的属性了。
但是存在类型转化的问题。这里我用反射写了一个通用的方法:
/** * 局部刷新AbsListView中某个item里的某个控件的属性, 避免notifyDataSetChanged()时不需要刷新的也被刷新 * * @param absListView * 要被局部刷新的absListView(一般为ListView或GridView) * @param posotion * 要被刷新的item的位置(相对于所有的item的位置而不是可见的) * @param widgetViewId * 要刷新的控件的资源id * @param methodName * 要调用改控件的方法名 * @param paramValues * 参数的值 * @param paramType * 参数的类型 */ public void updateSpecialItem(AbsListView absListView, int posotion, int widgetViewId, String methodName, Object[] paramValues, Class<?> paramType) throws Throwable { if (absListView == null) { throw new Throwable("absListView==null,are you kidding me?"); } if (posotion < absListView.getFirstVisiblePosition() || posotion > absListView.getLastVisiblePosition()) { // 不在可见范围内的item,只需修改数据源,重新显示时会调用getView进行重新赋值 return; } else { int index = posotion - absListView.getFirstVisiblePosition(); if (index >= absListView.getChildCount() || index < 0) { throw new Throwable( "posotion is out of bounds,are you kidding me?"); } View itemView = absListView.getChildAt(index); ViewHolder holder = (ViewHolder) itemView.getTag(); if (holder == null) { throw new Throwable( "holder==null,make sure you was set SmartAdapter for this absListView"); } View widgetView = holder.getView(widgetViewId); if (widgetView == null) { throw new Throwable( "widgetView==null,make sure is the right widgetViewId"); } Method method = null; method = getDeclaredMethod(widgetView, methodName, paramType); if (method == null) { throw new Throwable( "Not method found,make sure is the right methodName and paramType"); } // 如果更新的效果不是你预想的,可能是你传的paramValue出错了 method.invoke(widgetView, paramValues); } }
当然这样调用起来比较麻烦,也可以像ViewHolder一样写几个常用的:
public void changeTextToTextView(AbsListView absListView, int position, int resId, String newString) { ViewHolder holder = (ViewHolder) absListView.getChildAt( position - absListView.getFirstVisiblePosition()).getTag(); ((TextView) holder.getView(resId)).setText(newString); }
这里我省略了判断position的合法性,可以把position的合法性判断和获得控件引用都独立一个方法出来。
好了下面来看看怎么用,把上面那个显示多布局的例子改了一下,加上一个点击红色感叹号可以重发信息,此时要隐藏掉感叹号并显示progressbar出来。
(1)设置监听
holder.getView(R.id.img_gth).setOnClickListener( new OnClickListener() { @Override public void onClick(View arg0) { if (datas.get(positon).sendState == SEND_STATE_ERROR) reSendMessage(positon); } });
private void reSendMessage(int pos) { try { // 局部刷新,隐藏红色感叹号 mAdapter.updateSpecialItem(lv, pos, R.id.img_gth, "setVisibility", new Object[] { View.GONE }, int.class); // 局部刷新,显示progressbar mAdapter.updateSpecialItem(lv, pos, R.id.pro, "setVisibility", new Object[] { View.VISIBLE }, int.class); } catch (Throwable e) { // TODO Auto-generated catch block e.printStackTrace(); } //更改数据源 datas.get(pos).sendState = SEND_STATE_SENDING; }
效果图:
可以在getView里打印信息,会发现getView在我们改变控件属性时没有被调用。
理论上GridView也是可以用的,不过我还没测试过。
DEMO下载
求赞求评论
Android好奇宝宝_04_一个有3个功能的Adapter