Android好奇宝宝_04_一个有3个功能的Adapter
2014-12-27 16:23
302 查看
感觉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
然后给Adapter两个构造方法,其中第一个在只有一种布局时使用,第二个在有多种布局时使用。
接下来修改getView使用什么布局
最后,因为不同layoutId的Viewholder肯定不能混着用,所以再修改一下ViewHolder
完成。
来实现一个简单聊天界面看下怎么用。
效果图:
(纯属虚构,如有雷同,你来打我啊)
看看关键代码:
(1)首先模拟聊天消息类,没啥好说的:
(2)模拟数据,也没啥好说的,就不贴码了
(3)new 一个布局提供者,就是通过判断positiong或该position的数据来决定那个item要使用什么布局:
(4)new一个SmartAdapter:
这里因为左右布局的控件我用了一样的id,所以不用按不同布局进行不同处理。但更多时候还是得用注释掉的那种方式来。
(5)lv.setAdapter(mAdapter);
直接设置mAdapter,最后面有源码下载。
(3)局部刷新
先说下应用场景,一般要对某个item中的控件的属性进行修改时的做法是先修改数据源,然后在调用notifyDataSetChanged()进行刷新。
这种做法虽然可以满足需求,但是也存在一些问题,除了那个我们想修改的item外,其它的item也进行了不必要的刷新,即所有可见的item的getView方法都会被调用。如果item中有图片的话,还可能造成图片一闪一闪的,影响用户体验。
解决办法是想办法直接获取到我们要修改的控件的引用,而控件又是被包裹在item的view中,所以可以先想办法获得item的view。
那么现在问题来了,在知道position的情况下怎么获得对应item的view?
答:
不知道为什么的请参考我另一篇博文:传送门
获得item的view之后,如果你用的是这一篇讲的Adapter,那么每个item的View都保存了一个ViewHolder,所以可以直接用ViewHolder里保存的引用:
如果没有用我们的Adapter的话也可以用findViewById,或者是自定义的holder的话也可以直接取出holder中的引用。
取得引用之后就可以直接改变widgetView的属性了。
但是存在类型转化的问题。这里我用反射写了一个通用的方法:
当然这样调用起来比较麻烦,也可以像ViewHolder一样写几个常用的:
这里我省略了判断position的合法性,可以把position的合法性判断和获得控件引用都独立一个方法出来。
好了下面来看看怎么用,把上面那个显示多布局的例子改了一下,加上一个点击红色感叹号可以重发信息,此时要隐藏掉感叹号并显示progressbar出来。
(1)设置监听
(2)修改控件属性
特别需要注意的是我们更改控件属性只是改变了显示的外观,记得要修改数据源,不然一滚动就会变成getView产生的View,数据源没改的话,产生的View也没改。
效果图:
可以在getView里打印信息,会发现getView在我们改变控件属性时没有被调用。
理论上GridView也是可以用的,不过我还没测试过。
DEMO下载
求赞求评论
-----------------------------------------2015-01-23 编辑添加-----------------------------------------
关于第二点的多布局有一点可以改善:关于View复用的。
下面是对SmartAdapter的修改:
简单说下原理就是,ListView在对View进行缓存时会分两种情况:
(1)只有一种布局,即getViewTypeCount返回1,那么用一个List容器来保存缓存View,当需要缓存View时会直接从这个List中取。
(2)有多种布局,即getViewTypeCount返回大于1,那么用一个List的数组来缓存View,数组的长度为布局的类型数量。当需要缓存View时会先调用getItemViewType来获取这种布局的View存放在数组里的那个List中,再从该List中取缓存View。
伪代码实现类似于:
所以现在在SmartAdapter加上getViewTypeCount和getItemViewType的重写,让复用View总是能对应相同的layout。
这一篇呢来分享点好东西
一个自定义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; }
(2)模拟数据,也没啥好说的,就不贴码了
(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); } });
(2)修改控件属性
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产生的View,数据源没改的话,产生的View也没改。
效果图:
可以在getView里打印信息,会发现getView在我们改变控件属性时没有被调用。
理论上GridView也是可以用的,不过我还没测试过。
DEMO下载
求赞求评论
-----------------------------------------2015-01-23 编辑添加-----------------------------------------
关于第二点的多布局有一点可以改善:关于View复用的。
下面是对SmartAdapter的修改:
@Override public int getViewTypeCount() { if (mLayoutIdProvider != null) { return mLayoutIdProvider.getLayoutTypeCount(); } return 1; } @Override public int getItemViewType(int position) { if (mLayoutIdProvider != null) { int layoutId = mLayoutIdProvider.getLayoutId(position, mDatas.get(position)); int result = mLayoutIdArray.get(layoutId, -1); if (result < 0) { mLayoutIdArray.put(layoutId, mLayoutIdArray.size()); result = mLayoutIdArray.size() - 1; } return result; } return super.getItemViewType(position); } public interface LayoutIdProvider<T> { int getLayoutId(int position, T itemData); int getLayoutTypeCount(); }
简单说下原理就是,ListView在对View进行缓存时会分两种情况:
(1)只有一种布局,即getViewTypeCount返回1,那么用一个List容器来保存缓存View,当需要缓存View时会直接从这个List中取。
(2)有多种布局,即getViewTypeCount返回大于1,那么用一个List的数组来缓存View,数组的长度为布局的类型数量。当需要缓存View时会先调用getItemViewType来获取这种布局的View存放在数组里的那个List中,再从该List中取缓存View。
伪代码实现类似于:
List<View> scarpViews; List<View>[] scarpViewArrays; if(getViewTypeCount()==1){ getScarpView from scarpViews; }else if(getViewTypeCount()>1){ getScarpView from scarpViewArrays[getItemViewType(position)]; }
所以现在在SmartAdapter加上getViewTypeCount和getItemViewType的重写,让复用View总是能对应相同的layout。
相关文章推荐
- Android好奇宝宝_番外篇_看脸的世界_04
- 【Android游戏开发十六】Android Gesture之【触摸屏手势识别】操作!利用触摸屏手势实现一个简单切换图片的功能!
- Android应用程序开发教程:实现一个功能比较完善的登录对话框
- Android开发中的一个小功能 清空搜索框的文字
- android实现一个简单的加法功能
- 一个Android程序中新手引导功能实现方式的变迁
- 【Android游戏开发十六】Android Gesture之【触摸屏手势识别】操作!利用触摸屏手势实现一个简单切换图片的功能!
- android中清空一个表。类似truncate table 表名 这样的功能 android sqlite 清空数据库的某个表
- 一个Android程序中新手引导功能实现方式的变迁
- Android网络编程之一个Android下菜单系统模块的实现(服务器端—开桌功能))
- Android应用程序开发教程:实现一个功能比较完善的登录对话框
- 【Android游戏开发十六】Android Gesture之【触摸屏手势识别】操作!利用触摸屏手势实现一个简单切换图片的功能!
- 【Android2D游戏开发十六】(上文之触摸屏手势)详解Android Gesture 手势操作!利用手势实现一个简单切换图片的功能!
- 实现一个可以发送表单内容转换成pdf并发送功能的android 程序
- Android2.3.3系统开发一个在线OTA功能下载文件功能
- 【Android游戏开发十六】Android Gesture之【触摸屏手势识别】操作!利用触摸屏手势实现一个简单切换图片的功能!
- 一个android带可变图标以及checkbox的ListView的Adapter的实现,用于工厂测试
- Android网络编程之一个Android下菜单系统模块的实现(客户端—开桌功能(下部))
- 【Android游戏开发十六】Android Gesture之【触摸屏手势识别】操作!利用触摸屏手势实现一个简单切换图片的功能!
- Android ListAdapter的高级功能