android中ListView异步加载图片时的图片错位问题解决方案
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
android中ListView异步加载图片时的图片错位问题解决方
案
android中ListView异步加载图片时的图片错位问题解决方案分类:
android实例
android基础
2012-12-12 21:21
196人阅读
评论(0)
收藏
举报Android中的ListView是一个非常常用的控件,但是它却并不像想象中的那么简单。
特别是当你需要在ListView中展示大量网络图片的时候,处理不好轻则用户体验不佳,重则OOM,异步线程丢失或者图片错位。
关于其中的OOM和异步线程丢失的问题,是一个很庞大的话题,本人能力有限,无法说清,只有遇到的时候临时找原因,想办法解决了。
但是对于图片错位,却是可以避免的,今天我们就来说一说ListView异步加载图片中的图片错位
问题。
为什么会出现图片错位的问题呢?一般是重用了
convertView导致的。
如果你重用了convertView,此时convertView中的ImageView的id值是相等的,而我们在设置ImageView的图片时,是根据id来设置的,此时就出现
了图片错位问题。
这里童鞋们可以自己去测试一下,不重用convertView,也就是每次getView的时候,都使用findViewById(R.id.xx)去得到每一个Item的ImageView,异步下载图片的方法也只是简单的开一个AsyncTask执行下载。
在这种情况下,图片一般是不会产生错位的。
原因很简单,认真读一读前面的内容就明白了。
但是你如果真的在使用这种方法来使用getView的话,并且图片量比较大的时候,你程序的性能肯定不会好到哪里去了。
因此,重用convertView还是很有必要的。
这里需要注意,convertView是否为null会根据ListView的中布局标签值的不同有区别,具体的内容请参见这两篇文章:android listview 连续调用getview问题分析及解决[Android] ListView中getView的原理+如何在ListView中
放置多个item
这也就是说,某种情况下你界面中的第一张和第二张图片之间就有可能产生错位,因为有可能第二个可见的ImageView 就来自共用的convertView。
处理像这种图片的异步加载的问题,我们的一般思路是:下载的图片根据图片名称存入到SDCard中,最新加载的图片
存入到软引用中。
我们在getView中给ImageView设置图片的时候,首先根据url,从软引用中读取图片数据;如果软引用中没用,则根据url(对应图片名)从SDCard中读取图片数据;如果SDCard中也没有,则从网络上下载图片,在图片下载完成后,回调主线中的方法更新ImageView。
下面我们就照着上面的思路,先把程序整出来再说吧。
先看下效果图:
布局文件有两个,很简单,一个表示ListView(main.xml),一个表示ListView中的元素(single_data.xml),如下:[java] view plaincopy<?xml version="1.0"
encoding="utf-8"?> <LinearLayout
xmlns:android="/apk/res/andro id" xmlns:tools="/tools" android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:background="@android:color/darker_gray" tools:context=".MainActivity" > <ListView android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:cacheColorHint="@null"
android:id="@+id/listview" />
</LinearLayout>
[java] view plaincopy<?xml version="1.0"
encoding="utf-8"?> <RelativeLayout
xmlns:android="/apk/res/andro id" android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@android:color/white" > <ImageView android:layout_width="150dp" android:layout_height="150dp"
android:scaleType="fitXY"
android:id="@+id/image_view"
android:background="@drawable/ic_launcher"
/> <TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@id/image_view"
android:layout_alignBottom="@id/image_view" android:layout_marginLeft="20dp"
android:layout_alignParentRight="true"
android:gravity="center_vertical"
android:layout_toRightOf="@id/image_view"
android:singleLine="true"
android:ellipsize="end"
android:text="@string/hello"
android:id="@+id/text_view" />
</RelativeLayout>
加入访问网络和读取,写入sdcard的权限。
[java] view plaincopy<uses-permission
android:name="android.permission.INTERNET"/>
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_S TORAGE"/> <uses-permission
android:name="android.permission.MOUNT_UNMOUNT_ FILESYSTEMS"/> 接下来,我们来看看
MainActivity.java。
性能考虑,我们使用convertView和ViewHolder来重用控件。
这里涉及到比较关键的一步,我们会在getView的时候给ViewHolder中的ImageView设置tag,其值为要放置在该ImageView上的图片的url地址。
这个tag 很重要,在异步下载图片完成回调的方法中,我们使用findViewWithTag(String url)来找到ListView中对应的ImagView,然后给该ImageView设置图片即可。
其他的就是设置adapter的一般操作了。
[java] view plaincopypublic class MainActivity extends
Activity { ListView mListView;
ImageDownloader mDownloader; MyListAdapter myListAdapter; private static final String TAG = "MainActivity"; int m_flag = 0; private static final String[] URLS = { //图片地址就不贴了,自己去这篇帖子中找吧:
/liongname/articles/2345087.html //其中有几张图片访问不了。
};
@Override public void onCreate(Bundle savedInstanceState)
{ super.onCreate(savedInstanceState); setContentView(yout.main); Util.flag = 0; mListView = (ListView) findViewById(R.id.listview); myListAdapter = new MyListAdapter();
mListView.setAdapter(myListAdapter); }
private class MyListAdapter extends BaseAdapter
{ private ViewHolder mHolder;
@Override public int getCount()
{ return URLS.length; }
@Override public Object getItem(int position) { return URLS[position]; }
@Override public long getItemId(int position)
{ return position; }
@Override public View getView(int position, View convertView, ViewGroup parent) { //只有当convertView不存在的时候才去inflate子元素
if (convertView == null) { convertView = getLayoutInflater().inflate(yout.single_data,
null); mHolder = new ViewHolder(); mHolder.mImageView = (ImageView)
convertView.findViewById(R.id.image_view);
mHolder.mTextView = (TextView)
convertView.findViewById(R.id.text_view); convertView.setTag(mHolder); }else
{ mHolder = (ViewHolder) convertView.getTag(); }
final String url = URLS[position];
mHolder.mTextView.setText(url != null ?
url.substring(stIndexOf("/") + 1) : "");
mHolder.mImageView.setTag(URLS[position]);
if (mDownloader == null)
{ mDownloader = new ImageDownloader(); } //这句代码的作用是为了解决convertView被重用的时候,图片预
设的问题
mHolder.mImageView.setImageResource(R.drawable.ic_l auncher); if (mDownloader != null)
{ //异步下载图片
mDownloader.imageDownload(url, mHolder.mImageView, "/yanbin",MainActivity.this, new OnImageDownload()
{ @Override
public void onDownloadSucc(Bitmap bitmap,
String c_url,ImageView mimageView)
{ ImageView imageView = (ImageView)
mListView.findViewWithTag(c_url);
if (imageView != null)
{ imageView.set ImageBitmap(bitmap);
imageView.setTag("");
} }
}); } return convertView; } /** *
使用ViewHolder来优化listview * @author yanbin * */ private class ViewHolder { ImageView mImageView;
TextView mTextView; } } } 上面的mDownloader.imageDownload()就是异步下载图片比较核
心的方法,该方法在ImageDownloader.java类下。
其中的
五个参数分别为:要设置在当前ImageView 上的图片的url 地址,当前ImageView,文件缓存地址,当前的activity以
及图片回调接口。
在ImageDownloader类中,我们首先根据url从软引用中获取图片,如果不存在,从sdcard中读取图片,如果还不存在,则启动一个AsyncTask异步下载图片。
注意注意:这里我们做了一个这样的操作:用一个map将当前的url及其对应的MyAsyncTask存放起来了。
由于getView会执行至少一次,这一步的操作是为了相同的url创建相同的AsyncTask。
在onPostExecute()方法中,将该url对应的信息从map中
删除,一定要记得执行这一步。
看到很多的异步图片下载的例子中,重复创建AsyncTask都是普遍存在的,这里我们使用上面的思路解决掉了这一问题。
更详细的代码自己看ImageDownloader.java类吧,首先给出OnImageDownload.java接口的代码:
[java] view plaincopypublic interface OnImageDownload { void onDownloadSucc(Bitmap bitmap,String
c_url,ImageView imageView); } ImageDownloader.java
的代码(有两百多行,拷贝到eclipse中看会舒服一点):
[java] view plaincopypublic class ImageDownloader
{ private static final String TAG = "ImageDownloader"; private HashMap<String, MyAsyncTask> map = new HashMap<String, MyAsyncTask>(); private Map<String, SoftReference<Bitmap>> imageCaches = new HashMap<String, SoftReference<Bitmap>>();
/** * * @param url 该mImageView对应的url * @param mImageView * @param path 文件存储路径* @param mActivity * @param download OnImageDownload回调接口,在onPostExecute()中被调用*/ public void imageDownload(String url,ImageView mImageView,String path,Activity
mActivity,OnImageDownload
download){ SoftReference<Bitmap> currBitmap = imageCaches.get(url); Bitmap softRefBitmap = null; if(currBitmap !=
null){ softRefBitmap =
currBitmap.get(); } String imageName = ""; if(url !=
null){ imageName =
Util.getInstance().getImageName(url); }
Bitmap bitmap =
getBitmapFromFile(mActivity,imageName,path);
//先从软引用中拿数据if(currBitmap != null
&& mImageView != null && softRefBitmap != null &&
url.equals(mImageView.getTag())){ mImag eView.setImageBitmap(softRefBitmap); }
//软引用中没有,从文件中拿数据else
if(bitmap != null && mImageView != null
&&
url.equals(mImageView.getTag())){ mImag eView.setImageBitmap(bitmap); } //文件中也没有,此时根据mImageView的tag,即url去判断该url对应的task是否已经在执行,如果在执行,本次操作不创建新的线程,否则创建新的线程。
else if(url != null &&
needCreateNewTask(mImageView)){ MyA syncTask task = new MyAsyncTask(url, mImageView, path,mActivity,download);
if(mImageView != null){ Log.i(TAG, "执行MyAsyncTask --> " + Util.flag);
Util.flag ++; task.execute();
//将对应的url对应的任务存起来
map.put(url, task); } } }
/** * 判断是否需要重新创建线程下载图片,如果需要,返回值为true。
* @param url * @param mImageView * @return */ private boolean needCreateNewTask(ImageView
mImageView){ boolean b = true;
if(mImageView != null){ String curr_task_url = (String)mImageView.getTag();
if(isTasksContains(curr_task_url)){ b = false; } } return
b; } /** * 检查该url(最终反映的
是当前的ImageView的tag,tag会根据position的不同而
不同)对应的task是否存在* @param url *
@return */ private boolean
isTasksContains(String url){ boolean b = false;
if(map != null && map.get(url) !=
null){ b = true; } return b; } /** * 删除map中该url的信息,这一步很重要,不然MyAsyncTask的引用会“一直”存在于map中* @param url */ private void removeTaskFormMap(String url){ if(url != null
&& map != null && map.get(url) != null){ map.remove(url);
System.out.println("当前map的大小
=="+map.size()); } } /**
* 从文件中拿图片* @param mActivity *
@param imageName 图片名字* @param path 图片路径* @return */ private Bitmap getBitmapFromFile(Activity mActivity,String imageName,String path){ Bitmap bitmap = null; if(imageName != null){ File file = null; String real_path = ""; try
{ if(Util.getInstance().hasSDCard()){
real_path =
Util.getInstance().getExtPath() + (path != null && path.startsWith("/") ? path : "/" +
path); }else{ re al_path = Util.getInstance().getPackagePath(mActivity) + (path != null && path.startsWith("/") ? path : "/" + path); } file = new File(real_path, imageName);
if(file.exists()) bitmap = BitmapFactory.decodeStream(new
FileInputStream(file)); } catch (Exception e) { e.printStackTrace();
bitmap = null; } }
return bitmap; } /** * 将下载好的图片存放到文件中* @param path 图片路径* @param mActivity * @param imageName 图片名字* @param bitmap 图片* @return */
private boolean setBitmapToFile(String path,Activity mActivity,String imageName,Bitmap
bitmap){ File file = null; String
real_path = ""; try
{ if(Util.getInstance().hasSDCard()){
real_path = Util.getInstance().getExtPath() + (path != null && path.startsWith("/") ? path : "/" + path); }else{ real_path = Util.getInstance().getPackagePath(mActivity) + (path != null && path.startsWith("/") ? path : "/" + path); } file = new
File(real_path, imageName);
if(!file.exists()){ File file2 = new
File(real_path + "/");
file2.mkdirs(); }
file.createNewFile(); FileOutputStream fos = null;
if(Util.getInstance().hasSDCard()){ fos = new
FileOutputStream(file); }else{
fos = mActivity.openFileOutput(imageName, Context.MODE_PRIVATE); }
if (imageName != null &&
(imageName.contains(".png") ||
imageName.contains(".PNG"))){ bitma press(pressFormat.PNG, 90,
fos); }
else{ press(pr essFormat.JPEG, 90, fos); }
fos.flush(); if(fos !=
null){ fos.close(); } return true; } catch (Exception e)
{ e.printStackTrace(); return false; } } /** * 辅助方法,一般不调用* @param path * @param mActivity * @param imageName */
private void removeBitmapFromFile(String path,Activity
mActivity,String imageName){ File file = null; String real_path = ""; try
{ if(Util.getInstance().hasSDCard()){
real_path = Util.getInstance().getExtPath() + (path != null && path.startsWith("/") ? path : "/" + path); }else{ real_path = Util.getInstance().getPackagePath(mActivity) + (path != null && path.startsWith("/") ? path : "/" + path); } file = new
File(real_path, imageName); if(file != null) file.delete(); } catch (Exception e)
{ e.printStackTrace(); } } /** * 异步下载图片的方法* @author yanbin * */ private class MyAsyncTask extends AsyncTask<String, Void, Bitmap>{ private ImageView mImageView; private String url; private OnImageDownload download; private String path; private Activity mActivity;
public MyAsyncTask(String url,ImageView mImageView,String path,Activity
mActivity,OnImageDownload
download){ this.mImageView =
mImageView; this.url = url;
this.path = path; this.mActivity = mActivity; this.download = download; }
@Override protected Bitmap
doInBackground(String... params) { Bitmap data = null; if(url !=
null){ try { URL c_url = new URL(url); InputStream bitmap_data = c_url.openStream();
data = BitmapFactory.decodeStream(bitmap_data);
String imageName = Util.getInstance().getImageName(url); if(!setBitmapToFile(path,mActivity,imageName,
data)){ removeBitmapFromFil e(path,mActivity,imageName); } imageCaches.put(url, new
SoftReference<Bitmap>(data.createScaledBitmap(da ta, 100, 100, true))); } catch (Exception e)
{ e.printStackTrace();
} } return
data; } @Override
protected void onPreExecute()
{ super.onPreExecute(); }
@Override protected void
onPostExecute(Bitmap result) { //回调设置图片if(download !=
null){ download.onDownloadSucc(resu lt,url,mImageView); //该url对应的task 已经下载完成,从map中将其删除removeTaskFormMap(url); }
super.onPostExecute(result); }
} } Util.java类涉及到判断sdcard,获取图片存放路径以及从url中得到图片名称的操作,很简单,如下:
[java] view plaincopypublic class Util { private static Util util; public static int flag = 0; private
Util(){ } public static Util getInstance(){ if(util == null){ util = new Util(); } return util; }
/** * 判断是否有sdcard * @return */ public boolean hasSDCard(){ boolean b = false; if(Environment.MEDIA_MOUNTED.equals(Environment.g etExternalStorageState())){ b =
true; } return b; }
/** * 得到sdcard路径* @return */
public String getExtPath(){ String path = "";
if(hasSDCard()){ path =
Environment.getExternalStorageDirectory().getPath();
} return path; } /**
* 得到/data/data/yanbin.imagedownload目录*
@param mActivity * @return */ public String getPackagePath(Activity
mActivity){ return
mActivity.getFilesDir().toString(); } /**
* 根据url得到图片名* @param url * @return */ public String getImageName(String url)
{ String imageName = ""; if(url != null){ imageName =
url.substring(stIndexOf("/") + 1); }
return imageName; } } 至此,代码就全部贴完了。
代码中我用了47张图片做测试,MyAsyncTask.java执行了47次,当最后listView中的最后一张图片展示出来的的时候,map的size为0。
上面的一个程序主要解决了图片错位和AsyncTask重
复创建的问题。
但是还是有不少需要完善的地方,比如同步,比如图片的定期清理(这个可以通过拿每张图片的最后更新时间,根据与当前时间的间隔将缓存图片删
除即可)。
今天就到这里了,有更好的方法请推荐,有不懂的地方可以回复交流。
自己动手丰衣足食,代码已经全部给出来了,希望童鞋们可以自己多写写,一起学
习。
需要demo的就留下邮箱吧。