android中ListView异步加载图片时的图片错位问题解决方案

分类:android实例android基础 196人阅读 评论(0) 收藏 举报

Android中的ListView是一个非常常用的控件,但是它却并不像想象中的那么简单。特别是当你需要在ListView中展示大量网络图片的时候,处理不好轻则用户体验不佳,重则OOM,异步线程丢失或者图片错位。

关于其中的OOM和异步线程丢失的问题,是一个很庞大的话题,本人能力有限,无法说清,只有遇到的时候临时找原因,想办法解决了。但是对于图片错位,却是可以避免的,今天我们就来说一说ListView异步加载图片中的图片错位问题。

为什么会出现图片错位的问题呢?一般是重用了convertView导致的。如果你重用了convertView,此时convertView中的ImageViewid值是相等的,而我们在设置ImageView的图片时,是根据id来设置的,此时就出现了图片错位问题。这里童鞋们可以自己去测试一下,不重用convertView,也就是每次getView的时候,都使用findViewById(R.id.xx)去得到每一个ItemImageView,异步下载图片的方法也只是简单的开一个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 plain copy
  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:tools="http://schemas.android.com/tools"
  4. android:layout_width="fill_parent"
  5. android:layout_height="fill_parent"
  6. android:orientation="vertical"
  7. android:background="@android:color/darker_gray"
  8. tools:context=".MainActivity">
  9. <ListView
  10. android:layout_width="fill_parent"
  11. android:layout_height="wrap_content"
  12. android:cacheColorHint="@null"
  13. android:id="@+id/listview"
  14. />
  15. </LinearLayout>


[java] view plain copy
  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="fill_parent"
  4. android:layout_height="wrap_content"
  5. android:background="@android:color/white"
  6. >
  7. <ImageView
  8. android:layout_width="150dp"
  9. android:layout_height="150dp"
  10. android:scaleType="fitXY"
  11. android:id="@+id/image_view"
  12. android:background="@drawable/ic_launcher"
  13. />
  14. <TextView
  15. android:layout_width="wrap_content"
  16. android:layout_height="wrap_content"
  17. android:layout_alignTop="@id/image_view"
  18. android:layout_alignBottom="@id/image_view"
  19. android:layout_marginLeft="20dp"
  20. android:layout_alignParentRight="true"
  21. android:gravity="center_vertical"
  22. android:layout_toRightOf="@id/image_view"
  23. android:singleLine="true"
  24. android:ellipsize="end"
  25. android:text="@string/hello"
  26. android:id="@+id/text_view"
  27. />
  28. </RelativeLayout>


加入访问网络和读取,写入sdcard的权限。

[java] view plain copy
  1. <uses-permissionandroid:name="android.permission.INTERNET"/>
  2. <uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  3. <uses-permissionandroid:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>


接下来,我们来看看MainActivity.java。性能考虑,我们使用convertViewViewHolder来重用控件。这里涉及到比较关键的一步,我们会在getView的时候给ViewHolder中的ImageView设置tag,其值为要放置在该ImageView上的图片的url地址。这个tag很重要,在异步下载图片完成回调的方法中,我们使用findViewWithTag(Stringurl)来找到ListView中对应的ImagView,然后给该ImageView设置图片即可。其他的就是设置adapter的一般操作了。

[java] view plain copy
  1. publicclassMainActivityextendsActivity{
  2. ListViewmListView;
  3. ImageDownloadermDownloader;
  4. MyListAdaptermyListAdapter;
  5. privatestaticfinalStringTAG="MainActivity";
  6. intm_flag=0;
  7. privatestaticfinalString[]URLS={
  8. //图片地址就不贴了,自己去这篇帖子中找吧:http://www.cnblogs.com/liongname/articles/2345087.html
  9. //其中有几张图片访问不了。
  10. };
  11. @Override
  12. publicvoidonCreate(BundlesavedInstanceState){
  13. super.onCreate(savedInstanceState);
  14. setContentView(R.layout.main);
  15. Util.flag=0;
  16. mListView=(ListView)findViewById(R.id.listview);
  17. myListAdapter=newMyListAdapter();
  18. mListView.setAdapter(myListAdapter);
  19. }
  20. privateclassMyListAdapterextendsBaseAdapter{
  21. privateViewHoldermHolder;
  22. @Override
  23. publicintgetCount(){
  24. returnURLS.length;
  25. }
  26. @Override
  27. publicObjectgetItem(intposition){
  28. returnURLS[position];
  29. }
  30. @Override
  31. publiclonggetItemId(intposition){
  32. returnposition;
  33. }
  34. @Override
  35. publicViewgetView(intposition,ViewconvertView,ViewGroupparent){
  36. //只有当convertView不存在的时候才去inflate子元素
  37. if(convertView==null){
  38. convertView=getLayoutInflater().inflate(R.layout.single_data,
  39. null);
  40. mHolder=newViewHolder();
  41. mHolder.mImageView=(ImageView)convertView.findViewById(R.id.image_view);
  42. mHolder.mTextView=(TextView)convertView.findViewById(R.id.text_view);
  43. convertView.setTag(mHolder);
  44. }else{
  45. mHolder=(ViewHolder)convertView.getTag();
  46. }
  47. finalStringurl=URLS[position];
  48. mHolder.mTextView.setText(url!=null?url.substring(url.lastIndexOf("/")+1):"");
  49. mHolder.mImageView.setTag(URLS[position]);
  50. if(mDownloader==null){
  51. mDownloader=newImageDownloader();
  52. }
  53. //这句代码的作用是为了解决convertView被重用的时候,图片预设的问题
  54. mHolder.mImageView.setImageResource(R.drawable.ic_launcher);
  55. if(mDownloader!=null){
  56. //异步下载图片
  57. mDownloader.imageDownload(url,mHolder.mImageView,"/yanbin",MainActivity.this,newOnImageDownload(){
  58. @Override
  59. publicvoidonDownloadSucc(Bitmapbitmap,
  60. Stringc_url,ImageViewmimageView){
  61. ImageViewimageView=(ImageView)mListView.findViewWithTag(c_url);
  62. if(imageView!=null){
  63. imageView.setImageBitmap(bitmap);
  64. imageView.setTag("");
  65. }
  66. }
  67. });
  68. }
  69. returnconvertView;
  70. }
  71. /**
  72. *使用ViewHolder来优化listview
  73. *@authoryanbin
  74. *
  75. */
  76. privateclassViewHolder{
  77. ImageViewmImageView;
  78. TextViewmTextView;
  79. }
  80. }
  81. }


上面的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 plain copy
  1. publicinterfaceOnImageDownload{
  2. voidonDownloadSucc(Bitmapbitmap,Stringc_url,ImageViewimageView);
  3. }


ImageDownloader.java的代码(有两百多行,拷贝到eclipse中看会舒服一点)

[java] view plain copy
  1. publicclassImageDownloader{
  2. privatestaticfinalStringTAG="ImageDownloader";
  3. privateHashMap<String,MyAsyncTask>map=newHashMap<String,MyAsyncTask>();
  4. privateMap<String,SoftReference<Bitmap>>imageCaches=newHashMap<String,SoftReference<Bitmap>>();
  5. /**
  6. *
  7. *@paramurl该mImageView对应的url
  8. *@parammImageView
  9. *@parampath文件存储路径
  10. *@parammActivity
  11. *@paramdownloadOnImageDownload回调接口,在onPostExecute()中被调用
  12. */
  13. publicvoidimageDownload(Stringurl,ImageViewmImageView,Stringpath,ActivitymActivity,OnImageDownloaddownload){
  14. SoftReference<Bitmap>currBitmap=imageCaches.get(url);
  15. BitmapsoftRefBitmap=null;
  16. if(currBitmap!=null){
  17. softRefBitmap=currBitmap.get();
  18. }
  19. StringimageName="";
  20. if(url!=null){
  21. imageName=Util.getInstance().getImageName(url);
  22. }
  23. Bitmapbitmap=getBitmapFromFile(mActivity,imageName,path);
  24. //先从软引用中拿数据
  25. if(currBitmap!=null&&mImageView!=null&&softRefBitmap!=null&&url.equals(mImageView.getTag())){
  26. mImageView.setImageBitmap(softRefBitmap);
  27. }
  28. //软引用中没有,从文件中拿数据
  29. elseif(bitmap!=null&&mImageView!=null&&url.equals(mImageView.getTag())){
  30. mImageView.setImageBitmap(bitmap);
  31. }
  32. //文件中也没有,此时根据mImageView的tag,即url去判断该url对应的task是否已经在执行,如果在执行,本次操作不创建新的线程,否则创建新的线程。
  33. elseif(url!=null&&needCreateNewTask(mImageView)){
  34. MyAsyncTasktask=newMyAsyncTask(url,mImageView,path,mActivity,download);
  35. if(mImageView!=null){
  36. Log.i(TAG,"执行MyAsyncTask-->"+Util.flag);
  37. Util.flag++;
  38. task.execute();
  39. //将对应的url对应的任务存起来
  40. map.put(url,task);
  41. }
  42. }
  43. }
  44. /**
  45. *判断是否需要重新创建线程下载图片,如果需要,返回值为true。
  46. *@paramurl
  47. *@parammImageView
  48. *@return
  49. */
  50. privatebooleanneedCreateNewTask(ImageViewmImageView){
  51. booleanb=true;
  52. if(mImageView!=null){
  53. Stringcurr_task_url=(String)mImageView.getTag();
  54. if(isTasksContains(curr_task_url)){
  55. b=false;
  56. }
  57. }
  58. returnb;
  59. }
  60. /**
  61. *检查该url(最终反映的是当前的ImageView的tag,tag会根据position的不同而不同)对应的task是否存在
  62. *@paramurl
  63. *@return
  64. */
  65. privatebooleanisTasksContains(Stringurl){
  66. booleanb=false;
  67. if(map!=null&&map.get(url)!=null){
  68. b=true;
  69. }
  70. returnb;
  71. }
  72. /**
  73. *删除map中该url的信息,这一步很重要,不然MyAsyncTask的引用会“一直”存在于map中
  74. *@paramurl
  75. */
  76. privatevoidremoveTaskFormMap(Stringurl){
  77. if(url!=null&&map!=null&&map.get(url)!=null){
  78. map.remove(url);
  79. System.out.println("当前map的大小=="+map.size());
  80. }
  81. }
  82. /**
  83. *从文件中拿图片
  84. *@parammActivity
  85. *@paramimageName图片名字
  86. *@parampath图片路径
  87. *@return
  88. */
  89. privateBitmapgetBitmapFromFile(ActivitymActivity,StringimageName,Stringpath){
  90. Bitmapbitmap=null;
  91. if(imageName!=null){
  92. Filefile=null;
  93. Stringreal_path="";
  94. try{
  95. if(Util.getInstance().hasSDCard()){
  96. real_path=Util.getInstance().getExtPath()+(path!=null&&path.startsWith("/")?path:"/"+path);
  97. }else{
  98. real_path=Util.getInstance().getPackagePath(mActivity)+(path!=null&&path.startsWith("/")?path:"/"+path);
  99. }
  100. file=newFile(real_path,imageName);
  101. if(file.exists())
  102. bitmap=BitmapFactory.decodeStream(newFileInputStream(file));
  103. }catch(Exceptione){
  104. e.printStackTrace();
  105. bitmap=null;
  106. }
  107. }
  108. returnbitmap;
  109. }
  110. /**
  111. *将下载好的图片存放到文件中
  112. *@parampath图片路径
  113. *@parammActivity
  114. *@paramimageName图片名字
  115. *@parambitmap图片
  116. *@return
  117. */
  118. privatebooleansetBitmapToFile(Stringpath,ActivitymActivity,StringimageName,Bitmapbitmap){
  119. Filefile=null;
  120. Stringreal_path="";
  121. try{
  122. if(Util.getInstance().hasSDCard()){
  123. real_path=Util.getInstance().getExtPath()+(path!=null&&path.startsWith("/")?path:"/"+path);
  124. }else{
  125. real_path=Util.getInstance().getPackagePath(mActivity)+(path!=null&&path.startsWith("/")?path:"/"+path);
  126. }
  127. file=newFile(real_path,imageName);
  128. if(!file.exists()){
  129. Filefile2=newFile(real_path+"/");
  130. file2.mkdirs();
  131. }
  132. file.createNewFile();
  133. FileOutputStreamfos=null;
  134. if(Util.getInstance().hasSDCard()){
  135. fos=newFileOutputStream(file);
  136. }else{
  137. fos=mActivity.openFileOutput(imageName,Context.MODE_PRIVATE);
  138. }
  139. if(imageName!=null&&(imageName.contains(".png")||imageName.contains(".PNG"))){
  140. bitmap.compress(Bitmap.CompressFormat.PNG,90,fos);
  141. }
  142. else{
  143. bitmap.compress(Bitmap.CompressFormat.JPEG,90,fos);
  144. }
  145. fos.flush();
  146. if(fos!=null){
  147. fos.close();
  148. }
  149. returntrue;
  150. }catch(Exceptione){
  151. e.printStackTrace();
  152. returnfalse;
  153. }
  154. }
  155. /**
  156. *辅助方法,一般不调用
  157. *@parampath
  158. *@parammActivity
  159. *@paramimageName
  160. */
  161. privatevoidremoveBitmapFromFile(Stringpath,ActivitymActivity,StringimageName){
  162. Filefile=null;
  163. Stringreal_path="";
  164. try{
  165. if(Util.getInstance().hasSDCard()){
  166. real_path=Util.getInstance().getExtPath()+(path!=null&&path.startsWith("/")?path:"/"+path);
  167. }else{
  168. real_path=Util.getInstance().getPackagePath(mActivity)+(path!=null&&path.startsWith("/")?path:"/"+path);
  169. }
  170. file=newFile(real_path,imageName);
  171. if(file!=null)
  172. file.delete();
  173. }catch(Exceptione){
  174. e.printStackTrace();
  175. }
  176. }
  177. /**
  178. *异步下载图片的方法
  179. *@authoryanbin
  180. *
  181. */
  182. privateclassMyAsyncTaskextendsAsyncTask<String,Void,Bitmap>{
  183. privateImageViewmImageView;
  184. privateStringurl;
  185. privateOnImageDownloaddownload;
  186. privateStringpath;
  187. privateActivitymActivity;
  188. publicMyAsyncTask(Stringurl,ImageViewmImageView,Stringpath,ActivitymActivity,OnImageDownloaddownload){
  189. this.mImageView=mImageView;
  190. this.url=url;
  191. this.path=path;
  192. this.mActivity=mActivity;
  193. this.download=download;
  194. }
  195. @Override
  196. protectedBitmapdoInBackground(String...params){
  197. Bitmapdata=null;
  198. if(url!=null){
  199. try{
  200. URLc_url=newURL(url);
  201. InputStreambitmap_data=c_url.openStream();
  202. data=BitmapFactory.decodeStream(bitmap_data);
  203. StringimageName=Util.getInstance().getImageName(url);
  204. if(!setBitmapToFile(path,mActivity,imageName,data)){
  205. removeBitmapFromFile(path,mActivity,imageName);
  206. }
  207. imageCaches.put(url,newSoftReference<Bitmap>(data.createScaledBitmap(data,100,100,true)));
  208. }catch(Exceptione){
  209. e.printStackTrace();
  210. }
  211. }
  212. returndata;
  213. }
  214. @Override
  215. protectedvoidonPreExecute(){
  216. super.onPreExecute();
  217. }
  218. @Override
  219. protectedvoidonPostExecute(Bitmapresult){
  220. //回调设置图片
  221. if(download!=null){
  222. download.onDownloadSucc(result,url,mImageView);
  223. //该url对应的task已经下载完成,从map中将其删除
  224. removeTaskFormMap(url);
  225. }
  226. super.onPostExecute(result);
  227. }
  228. }
  229. }


Util.java类涉及到判断sdcard,获取图片存放路径以及从url中得到图片名称的操作,很简单,如下:

[java] view plain copy
  1. publicclassUtil{
  2. privatestaticUtilutil;
  3. publicstaticintflag=0;
  4. privateUtil(){
  5. }
  6. publicstaticUtilgetInstance(){
  7. if(util==null){
  8. util=newUtil();
  9. }
  10. returnutil;
  11. }
  12. /**
  13. *判断是否有sdcard
  14. *@return
  15. */
  16. publicbooleanhasSDCard(){
  17. booleanb=false;
  18. if(Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())){
  19. b=true;
  20. }
  21. returnb;
  22. }
  23. /**
  24. *得到sdcard路径
  25. *@return
  26. */
  27. publicStringgetExtPath(){
  28. Stringpath="";
  29. if(hasSDCard()){
  30. path=Environment.getExternalStorageDirectory().getPath();
  31. }
  32. returnpath;
  33. }
  34. /**
  35. *得到/data/data/yanbin.imagedownload目录
  36. *@parammActivity
  37. *@return
  38. */
  39. publicStringgetPackagePath(ActivitymActivity){
  40. returnmActivity.getFilesDir().toString();
  41. }
  42. /**
  43. *根据url得到图片名
  44. *@paramurl
  45. *@return
  46. */
  47. publicStringgetImageName(Stringurl){
  48. StringimageName="";
  49. if(url!=null){
  50. imageName=url.substring(url.lastIndexOf("/")+1);
  51. }
  52. returnimageName;
  53. }
  54. }


至此,代码就全部贴完了。代码中我用了47张图片做测试,MyAsyncTask.java执行了47次,当最后listView中的最后一张图片展示出来的的时候,mapsize0。上面的一个程序主要解决了图片错位和AsyncTask重 复创建的问题。但是还是有不少需要完善的地方,比如同步,比如图片的定期清理(这个可以通过拿每张图片的最后更新时间,根据与当前时间的间隔将缓存图片删 除即可)。今天就到这里了,有更好的方法请推荐,有不懂的地方可以回复交流。自己动手丰衣足食,代码已经全部给出来了,希望童鞋们可以自己多写写,一起学 习。需要demo的就留下邮箱吧。

更多相关文章

  1. Android(安卓)实现全屏 去掉标题栏
  2. Android(安卓)View绘制回调方法流程
  3. 在Android上使用Phonegap的个人经验总结及项目中的优化方案
  4. Android(安卓)时区设置以及设置系统属性的分析
  5. windows中下载android源码的方法 附下载脚本
  6. Android(安卓)JNI概述
  7. android去掉EditView的默认焦点问题
  8. Google Admob广告Android(安卓)、简单应用
  9. Android日记之2012\01\13

随机推荐

  1. Android GPS架构分析
  2. android 定时器的实现 (转)
  3. [置顶] android 菜单的详细介绍
  4. android中src和background区别
  5. Android 性能优化系列总篇 (五)
  6. 系出名门Android(9) - 数据库支持(SQLite
  7. Android中backgroundDimEnabled的作用
  8. android:属性 layout_alignParentRight an
  9. android 组件属性描述
  10. android 导入项目报错