一.概述

在ListView中,如果是同步加载图片还好,但是如果异步加载图片就会出现问题,下面我们来分析一下。

先准备一些图片作为数据源,我们新建一个Images类保存图片:

然后是我们的重点,适配器:

public class ImageAdapter extends ArrayAdapter<String>{
LruCache<String, BitmapDrawable> memoryCache ;
public ImageAdapter(Context context, int resource,
String[] objects) {
super(context, resource, objects);
//返回系统可用的最大堆内存,单位是byte,我的手机算下来是256MB
int maxMemory = (int) Runtime.getRuntime().maxMemory();
//手机:268435456B = 256MB
//模拟器:16777216B = 16MB
int cacheSize = maxMemory/8;//指定缓存大小为内存的八分之一
memoryCache = new LruCache<String,BitmapDrawable>(cacheSize){
//重写此方法,返回每张图片的大小
protected int sizeOf(String key, BitmapDrawable value) {
//每张图片大小:108000B = 105KB
return value.getBitmap().getByteCount();};
};
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
//获取对应位置图片的url地址
String url = getItem(position);
View view;
if(convertView == null){
view = View.inflate(getContext(), R.layout.image_item, null);
}else{
view = convertView;
}
ImageView imageView = (ImageView) view.findViewById(R.id.imageitem);
imageView.setImageResource(R.drawable.ic_launcher);
//根据url从内存中取
BitmapDrawable drawable = getBitmapFromMemory(url);
if(drawable!=null){
//内存中有,直接设置给imageview
imageView.setImageDrawable(drawable);
}else{
//内存没有,开启异步任务去下载
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(url);
}
return view;
}
/**
* 获取对应位置图片的url地址
*/
@Override
public String getItem(int position) {
return Images.imageUrls[position];
}
/**
* 将图片添加到lrucache中
* @param key
* @param drawable
*/
public void addBitmapToMemory(String key,BitmapDrawable drawable){
if(getBitmapFromMemory(key)==null){
memoryCache.put(key, drawable);
}
}
/**
* 根据url从内存中获取图片
* @param key
* @return
*/
public BitmapDrawable getBitmapFromMemory(String key){
return memoryCache.get(key);
}
/**
* String:启动任务执行的输入参数的类型
*Void:后台任务执行的百分比
*BitmapDrawable:后台执行任务最终的返回结果
*/
public class BitmapWorkerTask extends AsyncTask<String, Void, BitmapDrawable>{
private ImageView imageView;
private String imageUrl;
public BitmapWorkerTask(ImageView imageView) {
this.imageView = imageView;
}
@Override
protected BitmapDrawable doInBackground(String... params) {
imageUrl = params[0];//获取当前图片的地址
Bitmap bitmap = downloadBitmap(imageUrl);
BitmapDrawable bitmapDrawable = new BitmapDrawable(getContext().getResources(),bitmap);
//将下载好的图片添加到缓存中
addBitmapToMemory(imageUrl, bitmapDrawable);
return bitmapDrawable;
}
@Override
protected void onPostExecute(BitmapDrawable result) {
super.onPostExecute(result);
if(imageView!=null&&result!=null){
imageView.setImageDrawable(result);
}
}
}
/**
* 下载图片
* @param imageUrl
* @return
*/
public Bitmap downloadBitmap(String imageUrl){
Bitmap bitmap = null;
HttpURLConnection connection = null;
try {
URL url = new URL(imageUrl);
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(5*000);
connection.setReadTimeout(3*1000);
bitmap = BitmapFactory.decodeStream(connection.getInputStream());
} catch (Exception e) {
e.printStackTrace();
}finally{
if(connection!=null){
connection.disconnect();
}
}
return bitmap;
}
}

最后在代码中我们使用这个适配器,给ListView设置数据

public class MainActivity extends Activity {

private ListView listView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView = (ListView) findViewById(R.id.listview);
listView.setAdapter(new ImageAdapter(this, 0, Images.imageUrls));
}

}

以上就简单实现了ListView图片异步加载,我们看看效果吧:

ListView异步加载图片分析_.net

可以看到图片出现了错位和乱序的情况,为什么会这样呢?这就和ListView的工作原理有关了。

那么这里我们就可以思考一下了,目前数据源当中大概有60个图片的URL地址,而根据ListView的工作原理,显然不可能为每张图片都单独分配一个ImageView控件,ImageView控件的个数其实就比一屏能显示的图片数量稍微多一点而已,移出屏幕的ImageView控件会进入到RecycleBin当中,而新进入屏幕的元素则会从RecycleBin中获取ImageView控件。
那么,每当有新的元素进入界面时就会回调getView()方法,而在getView()方法中会开启异步请求从网络上获取图片,注意网络操作都是比较耗时的,也就是说当我们快速滑动ListView的时候就很有可能出现这样一种情况,某一个位置上的元素进入屏幕后开始从网络上请求图片,但是还没等图片下载完成,它就又被移出了屏幕。这种情况下会产生什么样的现象呢?根据ListView的工作原理,被移出屏幕的控件将会很快被新进入屏幕的元素重新利用起来,而如果在这个时候刚好前面发起的图片请求有了响应,就会将刚才位置上的图片显示到当前位置上,因为虽然它们位置不同,但都是共用的同一个ImageView实例,这样就出现了图片乱序的情况。
但是还没完,新进入屏幕的元素它也会发起一条网络请求来获取当前位置的图片,等到图片下载完的时候会设置到同样的ImageView上面,因此就会出现先显示一张图片,然后又变成了另外一张图片的情况,那么刚才我们看到的图片会自动变来变去的情况也就得到了解释。

那么如何来解决这个问题呢,下面介绍一种比较简单的方法,当然还有其他的方法,这里就不一一介绍了。

public class ImageAdapter extends ArrayAdapter<String>{
LruCache<String, BitmapDrawable> memoryCache ;
private ListView mListView;
public ImageAdapter(Context context, int resource,
String[] objects) {
super(context, resource, objects);
//返回系统可用的最大堆内存,单位是byte,我的手机算下来是256MB
int maxMemory = (int) Runtime.getRuntime().maxMemory();
//手机:268435456B = 256MB
//模拟器:16777216B = 16MB
int cacheSize = maxMemory/8;//指定缓存大小为内存的八分之一
memoryCache = new LruCache<String,BitmapDrawable>(cacheSize){
//重写此方法,返回每张图片的大小
protected int sizeOf(String key, BitmapDrawable value) {
//每张图片大小:108000B = 105KB
return value.getBitmap().getByteCount();};
};
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
//获取对应位置图片的url地址
String url = getItem(position);
if(mListView == null){
mListView = (ListView) parent;
}
View view;
if(convertView == null){
view = View.inflate(getContext(), R.layout.image_item, null);
}else{
view = convertView;
}
ImageView imageView = (ImageView) view.findViewById(R.id.imageitem);
imageView.setTag(url);
imageView.setImageResource(R.drawable.ic_launcher);
//根据url从内存中取
BitmapDrawable drawable = getBitmapFromMemory(url);
if(drawable!=null){
//内存中有,直接设置给imageview
imageView.setImageDrawable(drawable);
}else{
//内存没有,开启异步任务去下载
BitmapWorkerTask task = new BitmapWorkerTask();
task.execute(url);
}
return view;
}
/**
* 获取对应位置图片的url地址
*/
@Override
public String getItem(int position) {
return Images.imageUrls[position];
}
/**
* 将图片添加到lrucache中
* @param key
* @param drawable
*/
public void addBitmapToMemory(String key,BitmapDrawable drawable){
if(getBitmapFromMemory(key)==null){
memoryCache.put(key, drawable);
}
}
/**
* 根据url从内存中获取图片
* @param key
* @return
*/
public BitmapDrawable getBitmapFromMemory(String key){
return memoryCache.get(key);
}
/**
* String:启动任务执行的输入参数的类型
*Void:后台任务执行的百分比
*BitmapDrawable:后台执行任务最终的返回结果
*/
public class BitmapWorkerTask extends AsyncTask<String, Void, BitmapDrawable>{
private ImageView imageView;
private String imageUrl;
// public BitmapWorkerTask(ImageView imageView) {
// this.imageView = imageView;
// }

@Override
protected BitmapDrawable doInBackground(String... params) {
imageUrl = params[0];//获取当前图片的地址
Bitmap bitmap = downloadBitmap(imageUrl);
BitmapDrawable bitmapDrawable = new BitmapDrawable(getContext().getResources(),bitmap);
//将下载好的图片添加到缓存中
addBitmapToMemory(imageUrl, bitmapDrawable);
return bitmapDrawable;
}
@Override
protected void onPostExecute(BitmapDrawable result) {
imageView = (ImageView) mListView.findViewWithTag(imageUrl);
super.onPostExecute(result);
if(imageView!=null&&result!=null){
imageView.setImageDrawable(result);
}
}
}
/**
* 下载图片
* @param imageUrl
* @return
*/
public Bitmap downloadBitmap(String imageUrl){
Bitmap bitmap = null;
HttpURLConnection connection = null;
try {
URL url = new URL(imageUrl);
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(5*000);
connection.setReadTimeout(3*1000);
bitmap = BitmapFactory.decodeStream(connection.getInputStream());
} catch (Exception e) {
e.printStackTrace();
}finally{
if(connection!=null){
connection.disconnect();
}
}
return bitmap;
}
}

这里我们使用findViewWithTag来解决这个问题,这里我们可以尝试分析一下findViewWithTag的工作原理,其实顾名思义,这个方法就是通过Tag的名字来获取具备该Tag名的控件,我们先要调用控件的setTag()方法来给控件设置一个Tag,然后再调用ListView的findViewWithTag()方法使用相同的Tag名来找回控件。
那么为什么用了findViewWithTag()方法之后,图片就不会再出现乱序情况了呢?其实原因很简单,由于ListView中的ImageView控件都是重用的,移出屏幕的控件很快会被进入屏幕的图片重新利用起来,那么getView()方法就会再次得到执行,而在getView()方法中会为这个ImageView控件设置新的Tag,这样老的Tag就会被覆盖掉,于是这时再调用findVIewWithTag()方法并传入老的Tag,就只能得到null了,而我们判断只有ImageView不等于null的时候才会设置图片,这样图片乱序的问题也就不存在了