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 plain copy- <?xmlversion="1.0"encoding="utf-8"?>
- <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/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>
- <?xmlversion="1.0"encoding="utf-8"?>
- <RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"
- 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 plain copy- <uses-permissionandroid:name="android.permission.INTERNET"/>
- <uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
- <uses-permissionandroid:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
接下来,我们来看看MainActivity.java。性能考虑,我们使用convertView和ViewHolder来重用控件。这里涉及到比较关键的一步,我们会在getView的时候给ViewHolder中的ImageView设置tag,其值为要放置在该ImageView上的图片的url地址。这个tag很重要,在异步下载图片完成回调的方法中,我们使用findViewWithTag(Stringurl)来找到ListView中对应的ImagView,然后给该ImageView设置图片即可。其他的就是设置adapter的一般操作了。
- publicclassMainActivityextendsActivity{
- ListViewmListView;
- ImageDownloadermDownloader;
- MyListAdaptermyListAdapter;
- privatestaticfinalStringTAG="MainActivity";
- intm_flag=0;
- privatestaticfinalString[]URLS={
- //图片地址就不贴了,自己去这篇帖子中找吧:http://www.cnblogs.com/liongname/articles/2345087.html
- //其中有几张图片访问不了。
- };
- @Override
- publicvoidonCreate(BundlesavedInstanceState){
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- Util.flag=0;
- mListView=(ListView)findViewById(R.id.listview);
- myListAdapter=newMyListAdapter();
- mListView.setAdapter(myListAdapter);
- }
- privateclassMyListAdapterextendsBaseAdapter{
- privateViewHoldermHolder;
- @Override
- publicintgetCount(){
- returnURLS.length;
- }
- @Override
- publicObjectgetItem(intposition){
- returnURLS[position];
- }
- @Override
- publiclonggetItemId(intposition){
- returnposition;
- }
- @Override
- publicViewgetView(intposition,ViewconvertView,ViewGroupparent){
- //只有当convertView不存在的时候才去inflate子元素
- if(convertView==null){
- convertView=getLayoutInflater().inflate(R.layout.single_data,
- null);
- mHolder=newViewHolder();
- 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();
- }
- finalStringurl=URLS[position];
- mHolder.mTextView.setText(url!=null?url.substring(url.lastIndexOf("/")+1):"");
- mHolder.mImageView.setTag(URLS[position]);
- if(mDownloader==null){
- mDownloader=newImageDownloader();
- }
- //这句代码的作用是为了解决convertView被重用的时候,图片预设的问题
- mHolder.mImageView.setImageResource(R.drawable.ic_launcher);
- if(mDownloader!=null){
- //异步下载图片
- mDownloader.imageDownload(url,mHolder.mImageView,"/yanbin",MainActivity.this,newOnImageDownload(){
- @Override
- publicvoidonDownloadSucc(Bitmapbitmap,
- Stringc_url,ImageViewmimageView){
- ImageViewimageView=(ImageView)mListView.findViewWithTag(c_url);
- if(imageView!=null){
- imageView.setImageBitmap(bitmap);
- imageView.setTag("");
- }
- }
- });
- }
- returnconvertView;
- }
- /**
- *使用ViewHolder来优化listview
- *@authoryanbin
- *
- */
- privateclassViewHolder{
- ImageViewmImageView;
- TextViewmTextView;
- }
- }
- }
上面的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- publicinterfaceOnImageDownload{
- voidonDownloadSucc(Bitmapbitmap,Stringc_url,ImageViewimageView);
- }
ImageDownloader.java的代码(有两百多行,拷贝到eclipse中看会舒服一点):
- publicclassImageDownloader{
- privatestaticfinalStringTAG="ImageDownloader";
- privateHashMap<String,MyAsyncTask>map=newHashMap<String,MyAsyncTask>();
- privateMap<String,SoftReference<Bitmap>>imageCaches=newHashMap<String,SoftReference<Bitmap>>();
- /**
- *
- *@paramurl该mImageView对应的url
- *@parammImageView
- *@parampath文件存储路径
- *@parammActivity
- *@paramdownloadOnImageDownload回调接口,在onPostExecute()中被调用
- */
- publicvoidimageDownload(Stringurl,ImageViewmImageView,Stringpath,ActivitymActivity,OnImageDownloaddownload){
- SoftReference<Bitmap>currBitmap=imageCaches.get(url);
- BitmapsoftRefBitmap=null;
- if(currBitmap!=null){
- softRefBitmap=currBitmap.get();
- }
- StringimageName="";
- if(url!=null){
- imageName=Util.getInstance().getImageName(url);
- }
- Bitmapbitmap=getBitmapFromFile(mActivity,imageName,path);
- //先从软引用中拿数据
- if(currBitmap!=null&&mImageView!=null&&softRefBitmap!=null&&url.equals(mImageView.getTag())){
- mImageView.setImageBitmap(softRefBitmap);
- }
- //软引用中没有,从文件中拿数据
- elseif(bitmap!=null&&mImageView!=null&&url.equals(mImageView.getTag())){
- mImageView.setImageBitmap(bitmap);
- }
- //文件中也没有,此时根据mImageView的tag,即url去判断该url对应的task是否已经在执行,如果在执行,本次操作不创建新的线程,否则创建新的线程。
- elseif(url!=null&&needCreateNewTask(mImageView)){
- MyAsyncTasktask=newMyAsyncTask(url,mImageView,path,mActivity,download);
- if(mImageView!=null){
- Log.i(TAG,"执行MyAsyncTask-->"+Util.flag);
- Util.flag++;
- task.execute();
- //将对应的url对应的任务存起来
- map.put(url,task);
- }
- }
- }
- /**
- *判断是否需要重新创建线程下载图片,如果需要,返回值为true。
- *@paramurl
- *@parammImageView
- *@return
- */
- privatebooleanneedCreateNewTask(ImageViewmImageView){
- booleanb=true;
- if(mImageView!=null){
- Stringcurr_task_url=(String)mImageView.getTag();
- if(isTasksContains(curr_task_url)){
- b=false;
- }
- }
- returnb;
- }
- /**
- *检查该url(最终反映的是当前的ImageView的tag,tag会根据position的不同而不同)对应的task是否存在
- *@paramurl
- *@return
- */
- privatebooleanisTasksContains(Stringurl){
- booleanb=false;
- if(map!=null&&map.get(url)!=null){
- b=true;
- }
- returnb;
- }
- /**
- *删除map中该url的信息,这一步很重要,不然MyAsyncTask的引用会“一直”存在于map中
- *@paramurl
- */
- privatevoidremoveTaskFormMap(Stringurl){
- if(url!=null&&map!=null&&map.get(url)!=null){
- map.remove(url);
- System.out.println("当前map的大小=="+map.size());
- }
- }
- /**
- *从文件中拿图片
- *@parammActivity
- *@paramimageName图片名字
- *@parampath图片路径
- *@return
- */
- privateBitmapgetBitmapFromFile(ActivitymActivity,StringimageName,Stringpath){
- Bitmapbitmap=null;
- if(imageName!=null){
- Filefile=null;
- Stringreal_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=newFile(real_path,imageName);
- if(file.exists())
- bitmap=BitmapFactory.decodeStream(newFileInputStream(file));
- }catch(Exceptione){
- e.printStackTrace();
- bitmap=null;
- }
- }
- returnbitmap;
- }
- /**
- *将下载好的图片存放到文件中
- *@parampath图片路径
- *@parammActivity
- *@paramimageName图片名字
- *@parambitmap图片
- *@return
- */
- privatebooleansetBitmapToFile(Stringpath,ActivitymActivity,StringimageName,Bitmapbitmap){
- Filefile=null;
- Stringreal_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=newFile(real_path,imageName);
- if(!file.exists()){
- Filefile2=newFile(real_path+"/");
- file2.mkdirs();
- }
- file.createNewFile();
- FileOutputStreamfos=null;
- if(Util.getInstance().hasSDCard()){
- fos=newFileOutputStream(file);
- }else{
- fos=mActivity.openFileOutput(imageName,Context.MODE_PRIVATE);
- }
- if(imageName!=null&&(imageName.contains(".png")||imageName.contains(".PNG"))){
- bitmap.compress(Bitmap.CompressFormat.PNG,90,fos);
- }
- else{
- bitmap.compress(Bitmap.CompressFormat.JPEG,90,fos);
- }
- fos.flush();
- if(fos!=null){
- fos.close();
- }
- returntrue;
- }catch(Exceptione){
- e.printStackTrace();
- returnfalse;
- }
- }
- /**
- *辅助方法,一般不调用
- *@parampath
- *@parammActivity
- *@paramimageName
- */
- privatevoidremoveBitmapFromFile(Stringpath,ActivitymActivity,StringimageName){
- Filefile=null;
- Stringreal_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=newFile(real_path,imageName);
- if(file!=null)
- file.delete();
- }catch(Exceptione){
- e.printStackTrace();
- }
- }
- /**
- *异步下载图片的方法
- *@authoryanbin
- *
- */
- privateclassMyAsyncTaskextendsAsyncTask<String,Void,Bitmap>{
- privateImageViewmImageView;
- privateStringurl;
- privateOnImageDownloaddownload;
- privateStringpath;
- privateActivitymActivity;
- publicMyAsyncTask(Stringurl,ImageViewmImageView,Stringpath,ActivitymActivity,OnImageDownloaddownload){
- this.mImageView=mImageView;
- this.url=url;
- this.path=path;
- this.mActivity=mActivity;
- this.download=download;
- }
- @Override
- protectedBitmapdoInBackground(String...params){
- Bitmapdata=null;
- if(url!=null){
- try{
- URLc_url=newURL(url);
- InputStreambitmap_data=c_url.openStream();
- data=BitmapFactory.decodeStream(bitmap_data);
- StringimageName=Util.getInstance().getImageName(url);
- if(!setBitmapToFile(path,mActivity,imageName,data)){
- removeBitmapFromFile(path,mActivity,imageName);
- }
- imageCaches.put(url,newSoftReference<Bitmap>(data.createScaledBitmap(data,100,100,true)));
- }catch(Exceptione){
- e.printStackTrace();
- }
- }
- returndata;
- }
- @Override
- protectedvoidonPreExecute(){
- super.onPreExecute();
- }
- @Override
- protectedvoidonPostExecute(Bitmapresult){
- //回调设置图片
- if(download!=null){
- download.onDownloadSucc(result,url,mImageView);
- //该url对应的task已经下载完成,从map中将其删除
- removeTaskFormMap(url);
- }
- super.onPostExecute(result);
- }
- }
- }
Util.java类涉及到判断sdcard,获取图片存放路径以及从url中得到图片名称的操作,很简单,如下:
- publicclassUtil{
- privatestaticUtilutil;
- publicstaticintflag=0;
- privateUtil(){
- }
- publicstaticUtilgetInstance(){
- if(util==null){
- util=newUtil();
- }
- returnutil;
- }
- /**
- *判断是否有sdcard
- *@return
- */
- publicbooleanhasSDCard(){
- booleanb=false;
- if(Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())){
- b=true;
- }
- returnb;
- }
- /**
- *得到sdcard路径
- *@return
- */
- publicStringgetExtPath(){
- Stringpath="";
- if(hasSDCard()){
- path=Environment.getExternalStorageDirectory().getPath();
- }
- returnpath;
- }
- /**
- *得到/data/data/yanbin.imagedownload目录
- *@parammActivity
- *@return
- */
- publicStringgetPackagePath(ActivitymActivity){
- returnmActivity.getFilesDir().toString();
- }
- /**
- *根据url得到图片名
- *@paramurl
- *@return
- */
- publicStringgetImageName(Stringurl){
- StringimageName="";
- if(url!=null){
- imageName=url.substring(url.lastIndexOf("/")+1);
- }
- returnimageName;
- }
- }
至此,代码就全部贴完了。代码中我用了47张图片做测试,MyAsyncTask.java执行了47次,当最后listView中的最后一张图片展示出来的的时候,map的size为0。上面的一个程序主要解决了图片错位和AsyncTask重 复创建的问题。但是还是有不少需要完善的地方,比如同步,比如图片的定期清理(这个可以通过拿每张图片的最后更新时间,根据与当前时间的间隔将缓存图片删 除即可)。今天就到这里了,有更好的方法请推荐,有不懂的地方可以回复交流。自己动手丰衣足食,代码已经全部给出来了,希望童鞋们可以自己多写写,一起学 习。需要demo的就留下邮箱吧。
更多相关文章
- Android(安卓)实现全屏 去掉标题栏
- Android(安卓)View绘制回调方法流程
- 在Android上使用Phonegap的个人经验总结及项目中的优化方案
- Android(安卓)时区设置以及设置系统属性的分析
- windows中下载android源码的方法 附下载脚本
- Android(安卓)JNI概述
- android去掉EditView的默认焦点问题
- Google Admob广告Android(安卓)、简单应用
- Android日记之2012\01\13