感觉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也是可以用的,不过我还没测试过。
求赞求评论
-----------------------------------------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。