08/06
2013

Android ListView滑动过程中图片显示重复错位闪烁问题解决

主要分析Android ListView滚动过程中图片显示重复、错乱、闪烁的原因及解决方法,顺带提及ListView的缓存机制。
1、原因分析
ListView item缓存机制:为了使得性能更优,ListView会缓存行item(某行对应的View)。ListView通过adapter的getView函数获得每行的item。滑动过程中,

a. 如果某行item已经滑出屏幕,若该item不在缓存内,则put进缓存,否则更新缓存;
b. 获取滑入屏幕的行item之前会先判断缓存中是否有可用的item,如果有,做为convertView参数传递给adapter的getView。
更具体可见源代码ListView.obtainView

 

这样,如下的getView写法就可以充分利用缓存大大提升ListView的性能。即便上万个行item,最多inflate的次数为n,n为一屏最多显示ListView 行item的个数。

@Override
public View getView(int position, View convertView, ViewGroup parent) {
	ViewHolder holder;
	if (convertView == null) {
		convertView = inflater.inflate(R.layout.list_item, null);
		holder = new ViewHolder();
		……
		convertView.setTag(holder);
	} else {
		holder = (ViewHolder)convertView.getTag();
	}
}

/**
 * ViewHolder
 * 
 * @author trinea@trinea.cn 2013-08-01
 */
private static class ViewHolder {

	ImageView appIcon;
	TextView  appName;
	TextView  appInfo;
}

这样提升了性能,但同时也会造成另外一些问题:

a. 行item图片显示重复
这个显示重复是指当前行item显示了之前某行item的图片。
比如ListView滑动到第2行会异步加载某个图片,但是加载很慢,加载过程中listView已经滑动到了第14行,且滑动过程中该图片加载结束,第2行已不在屏幕内,根据上面介绍的缓存原理,第2行的view可能被第14行复用,这样我们看到的就是第14行显示了本该属于第2行的图片,造成显示重复。

 

b. 行item图片显示错乱
这个显示错乱是指某行item显示了不属于该行item的图片。
比如ListView滑动到第2行会异步加载某个图片,但是加载很慢,加载过程中listView已经滑动到了第14行,第2行已不在屏幕内,根据上面介绍的缓存原理,第2行的view可能被第14行复用,第14行显示了第2行的View,这时之前的图片加载结束,就会显示在第14行,造成错乱。

 

c. 行item图片显示闪烁
上面b的情况,第14行图片又很快加载结束,所以我们看到第14行先显示了第2行的图片,立马又显示了自己的图片进行覆盖造成闪烁错乱。

 

2、解决方法
通过上面的分析我们知道了出现错乱的原因是异步加载及对象被复用造成的,如果每次getView能给对象一个标识,在异步加载完成时比较标识与当前行item的标识是否一致,一致则显示,否则不做处理即可。
下面以使用ImageCache为ListView提供图片获取缓存为例,ListView中强烈推荐使用ImageCache
首先在listview adapter的getView中添加

@Override
public View getView(int position, View convertView, ViewGroup parent) {
	ViewHolder holder;
	if (convertView == null) {
		convertView = inflater.inflate(R.layout.list_item, null);
		holder = new ViewHolder();
		……
		convertView.setTag(holder);
	} else {
		holder = (ViewHolder)convertView.getTag();
	}

	……
	// add tag for image, to compare it when image loaded finish
	imageView.setTag(imageUrl);
	// if not in cache, restore default
	if (!Cache.ICON_CACHE.get(imageUrl, imageView)) {
		imageView.setImageDrawable(null);
	}
}

其中setTag表示设置标识,方便下面进行标志比对

if (!Cache.ICON_CACHE.get(imageUrl, imageView))

Cache.ICON_CACHE为ImageCache的实例,表示如果不在缓存内则设置drawable为null(当然你可以可以设置为你自己的默认资源),防止显示了之前某个行item的图片,解决了a. 行item图片显示重复问题。

 

在ImageCache的OnImageCallbackListener的onGetSuccess函数中添加

public void onGetSuccess(String imageUrl, Drawable imageDrawable, View view, boolean isInCache) {
	// can be another view child, like textView and so on
	if (view != null && imageDrawable != null) {
		ImageView imageView = (ImageView)view;
		// add tag judge, avoid listView cache and so on
		String imageUrlTag = (String)imageView.getTag();
		if (ObjectUtils.isEquals(imageUrlTag, imageUrl)) {
			imageView.setImageDrawable(imageDrawable);
		}
	}
};

在上面用String imageUrlTag = (String)imageView.getTag();取得之前设置的tag,然后和当前的url进行比较,如果相等则显示,解决了b. 行item图片显示错乱,c. 行item图片显示错乱的两个问题。其中ObjectUtils可见ObjectUtils@Github.

其他异步加载过程解决原理类似。

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

37 thoughts on “Android ListView滑动过程中图片显示重复错位闪烁问题解决

  1. Pingback: Android面试题目总结 | happyhls's space

  2. Pingback: Android ImageCache图片缓存 – findbug

  3. LZ :
    我这遇到个问题,我发现如果链接里面的图片地址包含汉字就无法获取,这个应该怎么解决呢?
    restaurantlogo1996d2.jpg这种的图片能获取,restaurant无名火锅冒菜99de3.jpg 这种就不行,会报错

    02-27 17:15:16.591: E/ImageSDCardCache(5779): get image exception, imageUrl is:http://112.124.21.210:8081/UploadFile/Restaurant/logo/restaurant无名火锅冒菜99de3.jpg
    02-27 17:15:16.591: E/ImageSDCardCache(5779): java.lang.RuntimeException: IOException occurred.
    02-27 17:15:16.591: E/ImageSDCardCache(5779): at cn.trinea.android.common.util.ImageUtils.getInputStreamFromUrl(ImageUtils.java:148)
    02-27 17:15:16.591: E/ImageSDCardCache(5779): at cn.trinea.android.common.service.impl.ImageSDCardCache$3.onGetData(ImageSDCardCache.java:813)
    02-27 17:15:16.591: E/ImageSDCardCache(5779): at cn.trinea.android.common.service.impl.ImageSDCardCache$3.onGetData(ImageSDCardCache.java:1)
    02-27 17:15:16.591: E/ImageSDCardCache(5779): at cn.trinea.android.common.service.impl.PreloadDataCache$GetDataThread.run(PreloadDataCache.java:569)
    02-27 17:15:16.591: E/ImageSDCardCache(5779): at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1076)
    02-27 17:15:16.591: E/ImageSDCardCache(5779): at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:569)
    02-27 17:15:16.591: E/ImageSDCardCache(5779): at java.lang.Thread.run(Thread.java:864)
    02-27 17:15:16.591: E/ImageSDCardCache(5779): Caused by: java.io.FileNotFoundException: http://112.124.21.210:8081/UploadFile/Restaurant/logo/restaurant无名火锅冒菜99de3.jpg
    02-27 17:15:16.591: E/ImageSDCardCache(5779): at libcore.net.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:177)
    02-27 17:15:16.591: E/ImageSDCardCache(5779): at cn.trinea.android.common.util.ImageUtils.getInputStreamFromUrl(ImageUtils.java:142)
    02-27 17:15:16.591: E/ImageSDCardCache(5779): … 6 more

    谢谢!

  4. public void onGetSuccess(String imageUrl, Drawable imageDrawable, View view, boolean isInCache) { if (view != null && imageDrawable != null) { ImageView imageView = (ImageView) view; // add tag judge, avoid listView cache and so on String imageUrlTag = (String) imageView.getTag(); if (ObjectUtils.isEquals(imageUrlTag, imageUrl)) { imageView.setImageDrawable(imageDrawable); imageView.setScaleType(ScaleType.FIT_XY); } } @Override public View getView(int position, View converView, ViewGroup parent) { // TODO Auto-generated method stub Viewholder holder; CouponList item = mList.get(position); if (converView == null) { holder = new Viewholder(); converView = listContainer.inflate(R.layout.sd_item_yjshc, null); holder.shopname = (TextView) converView .findViewById(R.id.text_fj_shopname); holder.youhuineirong = (TextView) converView .findViewById(R.id.text_youhuijuan_xiangmu); holder.shopyuanjia = (TextView) converView .findViewById(R.id.text_youhuijuan_yuanjia); holder.shopxianjia = (TextView) converView .findViewById(R.id.text_youhuijuan_xianjia); holder.youhuitime = (TextView) converView .findViewById(R.id.text_youhui_renshu); holder.shopurl = (ImageView) converView .findViewById(R.id.image_dds_jiantu); converView.setTag(holder); } else { holder = (Viewholder) converView.getTag(); } holder.shopname.setText(item.getCpnname() + “”); holder.youhuineirong.setText(item.getCpnname()); holder.shopyuanjia.getPaint().setFlags( Paint.STRIKE_THRU_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG);// 字体加横线样式 holder.shopyuanjia.setText(“原价:” + item.getCpYuanJia()); holder.shopxianjia.setText(item.getCpjine() + “”); SimpleDateFormat sfd = new SimpleDateFormat( “yyyy.MM.dd”); Date date = Util .toDate(item.getCpshenhedate()); holder.youhuitime.setText(“发布日期:”+ sfd.format(date)); String imgSmall = item.getCpnimageurl(); // add tag for image, to compare it when image loaded finish holder.shopurl.setTag(imgSmall); // if not in cache, restore default if (!MyappContext.IMAGE_CACHE.get(imgSmall, holder.shopurl)) { // holder.shopurl.setImageResource(R.drawable.sd_back_jiaoche_p); holder.shopurl.setImageDrawable(null); } return converView; } }