我們在使用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;
}
}