首页 > 代码库 > 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;
	}


(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下载


求赞求评论


Android好奇宝宝_04_一个有3个功能的Adapter