Android中ListView异步加载图片错位、重复、闪烁问题分析及解决方案

时间:2022-09-17 20:53:38

我们在使用ListView异步加载图片的时候,在快速滑动或者网络不好的情况下,会出现图片错位、重复、闪烁等问题,其实这些问题总结起来就是一个问题,我们需要对这些问题进行ListView的优化。

比如ListView上有100个Item,一屏只显示10个Item,我们知道getView()中convertView是用来复用View对象的,因为一个Item的对应一个View对象,而ImageView控件就是View对象通过findViewById()获得的,而我们在复用View对象时,同时这个ImageView对象也被复用了。比如第11个Item的View复用了第1个Item View对象,那么ImageView就同时被复用了,所以当图片没下载出来,这个ImageView(第11个Item)显示的数据就是复用(第1个Item)的数据。

1:Item图片显示重复

这个显示重复是指当前行Item显示了之前某行Item的图片。

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

2. Item图片显示错乱

这个显示错乱是指某行Item显示了不属于该行Item的图片。

跟上面的原因一样。

3. Item图片显示闪烁

上面介绍的另外一种情况,如果第14行图片又很快加载结束,所以我们看到第14行先显示了复用的第2行的图片,立马又显示了自己的图片进行覆盖造成闪烁错乱。

解决方案:

通过上面的分析我们知道了出现错乱的原因是异步加载及对象被复用造成的,如果每次getView能给对象一个标识,在异步加载完成时比较标识与当前行Item的标识是否一致,一致则显示,否则不做处理即可。

 

下面给个简单的实例(此实例没有对图片做缓存!!!!!!)

MainActivity --------------------------------------------------------------------------------------------------

package com.example.day001;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.widget.AbsListView;
import android.widget.ListView;
import android.widget.AbsListView.OnScrollListener;
/**
* @author echo
*
* ListView的使用步骤:
* 1.在xml中使用ListView标签,并找到该listview。
* 2.创建一个数据源(ListView要展示的数据)
* 3.自定义一个BaseAdapter并实例化它。
* 4.lv.setAdapter(adapter);
*
*/
public class MainActivity extends Activity {

// 集合,用来放数据源
List<PeopleBean> list;
// 自定义的BaseAdpater
MyBaseAdapter adapter;
// 当前的页数
int page = 1;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// 1.找到listview
ListView lv = (ListView) findViewById(R.id.lv);

// 2.创建一个数据源
// 用来装数据的集合
list = new ArrayList<PeopleBean>();
// 联网请求数据
MyTask myTask = new MyTask();
myTask.execute("http://www.ytmfdw.com/coupon/index.php?c=user&a=getstudent&page="
+ page);

// 3.实例化adapter
adapter = new MyBaseAdapter(list, this);

// 4.设置adapter
lv.setAdapter(adapter);

// 设置listview的滚动监听事件
lv.setOnScrollListener(new OnScrollListener() {

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == 0) {
// 当listview滑动到底部的时候,需要联网加载更多数据
if (view.getLastVisiblePosition() == view.getCount() - 1) {
page++;
MyTask task = new MyTask();
task.execute("http://www.ytmfdw.com/coupon/index.php?c=user&a=getstudent&page="
+ page);
}
}
}

@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
// TODO Auto-generated method stub
}
});

}

/**
* 联网获取接口返回的数据
*
* 异步任务的使用步骤:
* 1.新建类,继承自AsyncTask<Params, Progress, Result>
*          三个参数:(1)Params:代表输入到任务的参数类型,也即是doInBackground()的参数类型

*                        (2)Progress:代表处理过程中的参数类型,也就是doInBackground()执行过程中的
*                                             产出参数类型,通过publishProgress()发消息,传递给
*                                             onProgressUpdate(),一般用来更新界面.

*                        (3)Result:任务结束的产出类型,也就是doInBackground()的返回值类型,
*                                          和onPostExecute()的参数类型

*           这三个参数你需要什么类型的就传入什么类型的,如果是图片,可以传入Bitmap
*
* 2.重写doInBackground()方法,在这个方法中进行联网请求数据
*           它返回的值会传给onPostExecute()。
*
* 3.重写onPostExecute()方法,在这个方法中进行UI的更新
*
* 4.重写onProgressUpdate()方法,这个方法可以用于更新进度,比如下载电影,下载到百分之几。
* 它的进度值则是通过publishProgress()这个方法发布出来的。

*/

class MyTask extends AsyncTask<String, String, String> {
/**
* 此方法是在子线程中
* ------注意:联网请求只能写在子线程中
*/
@Override
protected String doInBackground(String... params) {
/**
* String... params:泛型
*        如果调用异步任务类的时候---->
*             MyTask myTask = new MyTask();
               myTask.execute(url1,url2,……);
    execute里面的参数有多个值,这些值会被放在params里面,成为一个数组,
    取值的时候就像数组一样取值,想要第几个,就用params[index]取出来就行。
    如果只有一个参数,则直接用params[0]即可。
*/
String http = params[0];
HttpURLConnection conn = null;
StringBuilder sb = new StringBuilder();
try {
URL url = new URL(http);
conn = (HttpURLConnection) url.openConnection();
InputStream in = conn.getInputStream();
BufferedReader reader = new BufferedReader(
new InputStreamReader(in));
String tmp = null;
while ((tmp = reader.readLine()) != null) {
sb.append(tmp);
}
in.close();
} catch (MalformedURLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if (conn != null) {
conn.disconnect();
}
}
Log.d("TAG", sb.toString());

return sb.toString();
}

/**
* 此方法是在主线程中。
* ----注意:UI更新只能在主线程中进行,不能在子线程中更新UI
*/
@Override
protected void onPostExecute(String result) {
// TODO Auto-generated method stub
super.onPostExecute(result);
try {
// json解析
JSONObject json = new JSONObject(result);
JSONArray jsons = json.getJSONArray("data");
int len = jsons.length();
for (int i = 0; i < len; i++) {
JSONObject obj = jsons.getJSONObject(i);
PeopleBean bean = new PeopleBean();
bean.img = obj.getString("image");
bean.name = obj.getString("name");
list.add(bean);
}

// 刷新适配器,当数据改变的时候调用该方法,listview显示的数据就改变
adapter.notifyDataSetChanged();
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

activity_main.xml----------------------------------------------------------------------------------------------------

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.day001.MainActivity" >

<ListView
android:id="@+id/lv"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</RelativeLayout>

PeopleBean---------------------------------------------------------------------------------

public class PeopleBean {
String img;
String name;
String sex;
}

MyBaseAdapter---------------------------------------------------------------------------------

package com.example.day001;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
/**
* @author echo
*
* 一、自定义BaseAdapter步骤:
* 1.新建一个类,继承BaseAdapter
* 2.重写4个方法;
* 3.修改getCount()返回的值(listview要展示的数据总个数)
* 4.在getView()方法里面去展示数据。
*        ---convertView复用:内存空间优化
*        ---ViewHolder:运行时间优化
*
* 二、ListView异步加载图片错位、重复、闪烁
*
解决办法:为了防止ListView异步加载图片错位、重复、闪烁等情况,我们需要给ImageView设置一个Tag,
这个Tag中设置的是图片的url,在异步加载完成时我们可以比较下图片的url与当前行Item的标识是否一致,
一致则显示,否则不做处理即可。

解决步骤:
1.给当前ImageView对象设置标识tag
2.给当前ImageView设置一个默认的图片
3.联网下载到图片后判断下tag值和当前的图片地址是否相等,相等就展示出来。

*
*/
public class MyBaseAdapter extends BaseAdapter {

List<PeopleBean> data;
Context context;

// 有参构造方法,传递值过来
public MyBaseAdapter(List<PeopleBean> data, Context context) {
super();
this.data = data;
this.context = context;
}

// 返回数据长度
@Override
public int getCount() {
// TODO Auto-generated method stub
return data.size();
}

// 返回一个item的view
@Override
public View getView(int position, View convertView, ViewGroup parent) {

ViewHolder holder = null;

/**
* ListView中的每一个Item显示都需要Adapter调用一次getView的方法, 这个方法会传入一个convertView的参数,
* 返回的View就是这个Item显示的View
* 。如果当Item的数量足够大,再为每一个Item都创建一个View对象,必将占用很多内存,创建View对象
* (mInflater.inflate(R.layout.lv_item,
* null);从xml中生成View,这是属于IO操作)也是耗时操作
* ,所以必将影响性能。Android提供了一个叫做Recycler(反复循环器
* )的构件,就是当ListView的Item从上方滚出屏幕视角之外
* ,对应Item的View会被缓存到Recycler中,相应的会从下方生成一个Item
* ,而此时调用的getView中的convertView参数就是滚出屏幕的Item的View
* ,所以说如果能重用这个convertView,就会大大改善性能。
*/
if (convertView == null) {

// 1.布局容器
LayoutInflater inflater = LayoutInflater.from(context);
// 2.把布局文件装进布局容器里面,得到item的view
convertView = inflater.inflate(R.layout.items, null);
holder = new ViewHolder();
// holder设置tag(标记)值
convertView.setTag(holder);
} else {
/**
* 如果convertView不为空,说明该view之前加载进来过,所以直接将其赋
* 给view,即反复使用,避免再创建新的view浪费资源
*/
holder = (ViewHolder) convertView.getTag();
}

// 找到当前的对象
PeopleBean PeopleBean = data.get(position);
// 获取当前对象里面的图片
String img_url = PeopleBean.img;
// 获取当前对象里面的文字
String text = PeopleBean.name;

// 找到控件
holder.tv = (TextView) convertView.findViewById(R.id.tv);
holder.iv = (ImageView) convertView.findViewById(R.id.iv);

/**
* 为了防止ListView异步加载图片错位、重复、闪烁等情况,我们需要给ImageView设置一个Tag,
* 这个Tag中设置的是图片的url,在异步加载完成时我们可以比较下图片的url与当前行Item的标识是否一致,
* 一致则显示,否则不做处理即可。
*/

/**
* 防止ListView异步加载图片错位、重复、闪烁等情况
*
* ---1.给当前ImageView对象设置标识
*/
holder.iv.setTag(img_url);
/**
* 防止ListView异步加载图片错位、重复、闪烁等情况
*
* ---2.给当前ImageView设置一个默认的值(因为控件是复用的, 用完之后其中的值还存在,所以需要清洗下,
* 这里可以给随意给一张默认的图片)
*/
holder.iv.setImageResource(R.drawable.ic_launcher);

// 异步任务--下载图片
DownImgTask downImgTask = new DownImgTask(holder.iv);
downImgTask.execute(img_url);

// 给控件设置值
holder.tv.setText(text);

return convertView;
}

/**
* 联网下载图片
* @author echo
*
*/
class DownImgTask extends AsyncTask<String, Void, Bitmap> {

ImageView iv;
String img_url;// 图片的地址

public DownImgTask(ImageView iv) {
super();
this.iv = iv;
}

@Override
protected Bitmap doInBackground(String... params) {
img_url = params[0];
HttpURLConnection conn = null;
Bitmap bm = null;
try {
URL url = new URL(img_url);
conn = (HttpURLConnection) url.openConnection();
InputStream in = conn.getInputStream();
bm = BitmapFactory.decodeStream(in);
in.close();
} catch (MalformedURLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if (conn != null) {
conn.disconnect();
}
}
return bm;
}

@Override
protected void onPostExecute(Bitmap result) {
// TODO Auto-generated method stub
super.onPostExecute(result);

/**
* 防止ListView异步加载图片错位、重复、闪烁等情况
*
* ---3.判断下tag值和图片地址是否相等,若相等,则说明该控件在当前的位置所需要展示的图片 是对的,就可以直接展示出来啦
*/

// 防止图片错位,判断下tag值和图片地址是否相等
if (result != null && img_url.equals(iv.getTag())) {
// 设置图片
iv.setImageBitmap(result);
}

}
}

// 自定义一个ViewHolder
class ViewHolder {
TextView tv;
ImageView iv;
}

@Override
public Object getItem(int position) {
// TODO Auto-generated method stub
return null;
}

@Override
public long getItemId(int position) {
// TODO Auto-generated method stub
return 0;
}

}