有時會有在加載ListView的時候,包含用戶頭像或其他需要到網絡獲取的圖片信息,這時如果等待全部獲取完成再顯示會比較慢,很影響用戶體驗,所以這時就需要利用到異步加載圖片的方法。
今天整理的方法,是用Thread來進行加載,沒有利用ThreadPool的方法,後面的方法以後再慢慢學一下吧,先把學會的這個記下來。
具體的效果是,加入每個ListView的項只需要顯示一個圖片,每張圖片都是本地沒有的,則先把他們都顯示成一張默認的圖片(我用的是程序圖標),然後開啓後臺線程去網上取圖片,最後再一個一個加載出來。
下面是具體的步驟:
步驟一:寫一個異步加載類,我的叫AsyncImageLoader
package com.carter.asynchronousimage;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.net.URL;
import java.util.HashMap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
/**
* 異步加載圖片類,內部有緩存,可以通過後臺線程獲取網絡圖片。首先生成一個實例,並調用loadDrawableByTag方法來獲取一個Drawable對象
*/
public class AsyncImageLoader {
/**
* 使用軟引用SoftReference,可以由系統在恰當的時候更容易的回收
*/
private HashMap<String, SoftReference<Drawable>> imageCache;
public AsyncImageLoader(){
imageCache = new HashMap<String, SoftReference<Drawable>>();
}
/**
* 通過傳入的TagInfo來獲取一個網絡上的圖片
* @param tag TagInfo對象,保存了position、url和一個待獲取的Drawable對象
* @param callback ImageCallBack對象,用於在獲取到圖片後供調用側進行下一步的處理
* @return drawable 從網絡或緩存中得到的Drawable對象,可爲null,調用側需判斷
*/
public Drawable loadDrawableByTag(final TagInfo tag, final ImageCallBack callback){
Drawable drawable;
/**
* 先在緩存中找,如果通過URL地址可以找到,則直接返回該對象
*/
if(imageCache.containsKey(tag.getUrl())){
drawable = imageCache.get(tag.getUrl()).get();
if(null!=drawable){
return drawable;
}
}
/**
* 用於在獲取到網絡圖片後,保存圖片到緩存,並觸發調用側的處理
*/
final Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
TagInfo info = (TagInfo)msg.obj;
imageCache.put(info.url, new SoftReference<Drawable>(info.drawable));
callback.obtainImage(info);
super.handleMessage(msg);
}
};
/**
* 如果在緩存中沒有找到,則開啓一個線程來進行網絡請求
*/
new Thread(new Runnable() {
@Override
public void run() {
TagInfo info = getDrawableIntoTag(tag);
Message msg = new Message();
msg.what = 0;
msg.obj = info;
handler.sendMessage(msg);
}
}).start();
return null;
}
/**
* 通過傳入的TagInfo對象,利用其URL屬性,到網絡請求圖片,獲取到圖片後保存在TagInfo的Drawable屬性中,並返回該TagInfo
* @param info TagInfo對象,需要利用裏面的url屬性
* @return TagInfo 傳入的TagInfo對象,增加了Drawable屬性後返回
*/
public TagInfo getDrawableIntoTag(TagInfo info){
URL request;
InputStream input;
Drawable drawable = null;
try{
request = new URL(info.getUrl());
input = (InputStream)request.getContent();
drawable = Drawable.createFromStream(input, "src"); // 第二個屬性可爲空,爲DEBUG下使用,網上的說明
}
catch(Exception e){
e.printStackTrace();
}
info.drawable = drawable;
return info;
}
/**
* 獲取圖片的回調接口,裏面的obtainImage方法在獲取到圖片後進行調用
*/
interface ImageCallBack{
/**
* 獲取到圖片後在調用側執行具體的細節
* @param info TagInfo對象,傳入的info經過處理,增加Drawable屬性,並返回給傳入者
*/
public void obtainImage(TagInfo info);
}
}
裏面基本每個重要的地方都在我理解的情況下加了註釋,應該很多人都能看懂的。
步驟二:寫一個Activity用於展示這些內容,我的是AsynImageActivity
package com.carter.asynchronousimage;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import com.carter.asynchronousimage.AsyncImageLoader.ImageCallBack;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
public class AsynImageActivity extends Activity {
Context context;
ListView list_lv;
List<ImageEntry> mList;
MyAdapter adapter;
// 測試數據,網上找的圖片
String[] urls = new String[]{
"http://pic2.sc.chinaz.com/Files/pic/icons128/2328/%E7%BB%8F%E5%85%B8%E7%94%B5%E8%84%91%E6%A1%8C%E9%9D%A2%E5%9B%BE%E6%A0%87%E4%B8%8B%E8%BD%BD9.png",
"http://pic1.sc.chinaz.com/Files/pic/icons128/2328/%E7%BB%8F%E5%85%B8%E7%94%B5%E8%84%91%E6%A1%8C%E9%9D%A2%E5%9B%BE%E6%A0%87%E4%B8%8B%E8%BD%BD0.png",
"http://pic2.sc.chinaz.com/Files/pic/icons128/2328/%E7%BB%8F%E5%85%B8%E7%94%B5%E8%84%91%E6%A1%8C%E9%9D%A2%E5%9B%BE%E6%A0%87%E4%B8%8B%E8%BD%BD7.png",
"http://pic2.sc.chinaz.com/Files/pic/icons128/2328/%E7%BB%8F%E5%85%B8%E7%94%B5%E8%84%91%E6%A1%8C%E9%9D%A2%E5%9B%BE%E6%A0%87%E4%B8%8B%E8%BD%BD6.png",
"http://pic2.sc.chinaz.com/Files/pic/icons128/2328/%E7%BB%8F%E5%85%B8%E7%94%B5%E8%84%91%E6%A1%8C%E9%9D%A2%E5%9B%BE%E6%A0%87%E4%B8%8B%E8%BD%BD5.png",
"http://pic.sc.chinaz.com/Files/pic/icons128/2328/%E7%BB%8F%E5%85%B8%E7%94%B5%E8%84%91%E6%A1%8C%E9%9D%A2%E5%9B%BE%E6%A0%87%E4%B8%8B%E8%BD%BD4.png",
"http://pic.sc.chinaz.com/Files/pic/icons128/2328/%E7%BB%8F%E5%85%B8%E7%94%B5%E8%84%91%E6%A1%8C%E9%9D%A2%E5%9B%BE%E6%A0%87%E4%B8%8B%E8%BD%BD3.png",
"http://pic2.sc.chinaz.com/Files/pic/icons128/2328/%E7%BB%8F%E5%85%B8%E7%94%B5%E8%84%91%E6%A1%8C%E9%9D%A2%E5%9B%BE%E6%A0%87%E4%B8%8B%E8%BD%BD7.png",
"http://pic.sc.chinaz.com/Files/pic/icons128/2328/%E7%BB%8F%E5%85%B8%E7%94%B5%E8%84%91%E6%A1%8C%E9%9D%A2%E5%9B%BE%E6%A0%87%E4%B8%8B%E8%BD%BD14.png",
"http://pic.sc.chinaz.com/Files/pic/icons128/2328/%E7%BB%8F%E5%85%B8%E7%94%B5%E8%84%91%E6%A1%8C%E9%9D%A2%E5%9B%BE%E6%A0%87%E4%B8%8B%E8%BD%BD13.png",
"http://pic.sc.chinaz.com/Files/pic/icons128/2328/%E7%BB%8F%E5%85%B8%E7%94%B5%E8%84%91%E6%A1%8C%E9%9D%A2%E5%9B%BE%E6%A0%87%E4%B8%8B%E8%BD%BD12.png",
"http://pic.sc.chinaz.com/Files/pic/icons128/2328/%E7%BB%8F%E5%85%B8%E7%94%B5%E8%84%91%E6%A1%8C%E9%9D%A2%E5%9B%BE%E6%A0%87%E4%B8%8B%E8%BD%BD11.png",
"http://pic2.sc.chinaz.com/Files/pic/icons128/2328/%E7%BB%8F%E5%85%B8%E7%94%B5%E8%84%91%E6%A1%8C%E9%9D%A2%E5%9B%BE%E6%A0%87%E4%B8%8B%E8%BD%BD5.png",
"http://pic.sc.chinaz.com/Files/pic/icons128/2328/%E7%BB%8F%E5%85%B8%E7%94%B5%E8%84%91%E6%A1%8C%E9%9D%A2%E5%9B%BE%E6%A0%87%E4%B8%8B%E8%BD%BD1.png",
"http://pic1.sc.chinaz.com/Files/pic/icons128/2328/%E7%BB%8F%E5%85%B8%E7%94%B5%E8%84%91%E6%A1%8C%E9%9D%A2%E5%9B%BE%E6%A0%87%E4%B8%8B%E8%BD%BD0.png"
};
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
context = this;
list_lv = (ListView) findViewById(R.id.list_lv);
mList = new ArrayList<ImageEntry>();
for(int i=0; i<urls.length; i++){
ImageEntry entry = new ImageEntry();
entry.setUrl(urls[i]);
mList.add(entry);
}
adapter = new MyAdapter(context, mList);
list_lv.setAdapter(adapter);
}
/**
* 一個ImageEntry代表了一個帶有圖片地址url等其他屬性的實例,爲提高可擴展性,封裝了一個對象。目前只包含url這一個屬性。
*/
class ImageEntry{
String url;
public String getUrl(){
return this.url;
}
public void setUrl(String url){
this.url = url;
}
}
/**
* 重寫的Adapter
*/
class MyAdapter extends BaseAdapter{
Context context;
List<ImageEntry> mList;
HashMap<String, Drawable> imgCache; // 圖片緩存
HashMap<Integer, TagInfo> tag_map; // TagInfo緩存
AsyncImageLoader loader; // 異步加載圖片類
/**
* 構造函數
* @param context 上下文
* @param list 包含了所有要顯示的圖片的ImageEntry對象的列表
*/
public MyAdapter(Context context, List<ImageEntry> list){
this.context = context;
this.mList = list;
imgCache = new HashMap<String, Drawable>();
loader = new AsyncImageLoader();
tag_map = new HashMap<Integer, TagInfo>();
}
@Override
public int getCount() {
// TODO Auto-generated method stub
return mList.size();
}
@Override
public Object getItem(int position) {
// TODO Auto-generated method stub
return mList.get(position);
}
@Override
public long getItemId(int position) {
// TODO Auto-generated method stub
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// TODO Auto-generated method stub
ViewHolder holder = null;
if(null==convertView){
convertView = LayoutInflater.from(context).inflate(R.layout.adapter_item, null, false);
holder = new ViewHolder();
holder.img = (ImageView) convertView.findViewById(R.id.img);
convertView.setTag(holder);
}else{
holder = (ViewHolder) convertView.getTag();
}
String imgurl = mList.get(position).getUrl(); // 得到該項所代表的url地址
Drawable drawable = imgCache.get(imgurl); // 先去緩存中找
TagInfo tag = new TagInfo();
tag.setPosition(position); // 保存了當前在adapter中的位置
tag.setUrl(imgurl); // 保存當前項所要加載的url
holder.img.setTag(tag); // 爲ImageView設置Tag,爲以後再獲取圖片後好能找到它
tag_map.put(position, tag); // 把該TagInfo對應position放入Tag緩存中
if(null!=drawable){ // 找到了直接設置爲圖像
holder.img.setImageDrawable(drawable);
}else{ // 沒找到則開啓異步線程
drawable = loader.loadDrawableByTag(tag, new ImageCallBack() {
@Override
public void obtainImage(TagInfo ret_info) {
imgCache.put(ret_info.getUrl(), ret_info.getDrawable()); // 首先把獲取的圖片放入到緩存中
// 通過返回的TagInfo去Tag緩存中找,然後再通過找到的Tag來獲取到所對應的ImageView
ImageView tag_view = (ImageView) list_lv.findViewWithTag(tag_map.get(ret_info.getPosition()));
Log.i("carter", "tag_view: " + tag_view + " position: " + ret_info.getPosition());
if(null!=tag_view)
tag_view.setImageDrawable(ret_info.getDrawable());
}
});
if(null==drawable){ // 如果獲取的圖片爲空,則默認顯示一個圖片
holder.img.setImageResource(R.drawable.ic_launcher);
}
}
return convertView;
}
class ViewHolder{
ImageView img;
}
}
}
在Activity中需要用到一個類叫做TagInfo,這個是作爲一個Tag,保存一些例如位置、地址和圖像等的信息
package com.carter.asynchronousimage;
import android.graphics.drawable.Drawable;
public class TagInfo {
String url;
int position;
Drawable drawable;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public int getPosition() {
return position;
}
public void setPosition(int position) {
this.position = position;
}
public Drawable getDrawable() {
return drawable;
}
public void setDrawable(Drawable drawable) {
this.drawable = drawable;
}
}
步驟三:只要在Adapter的getView方法中調用loadDrawableByTag方法就可以了。
工程需要兩個佈局文件,一個是主界面main.xml,一個是每項所顯示的佈局文件adapter_item.xml
main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<ListView
android:id="@+id/list_lv"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
</LinearLayout>
adapter_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<ImageView
android:id="@+id/img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</LinearLayout>
佈局很簡單,不難懂。
其他問題:
在adapter中使用了一個HashMap叫做tag_map,它的作用是保存每個position位置的TagInfo。其實這樣做在顯示很少項的時候還好,但是如果有幾千幾萬個項需要顯示,則會造成大量的內存浪費。
不過使用這個方法實在是逼不得已,原來的版本,是在obtainImage方法中利用url的值來找到ImageView的,例如:
holder.img.setTag(imgurl);
public void obtainImage(){
// ...
ImageView tag_view = (ImageView) list_lv.findViewWithTag(imgurl);
// ...
}
這樣做有一個問題,例如我有兩個項利用的是一個url,則在findViewWithTag(imgurl)時只會找到一個ImageView,則另外一個就沒有處理了,所以會造成只有一個顯示了正確的圖片,另外一個顯示默認的。
所以我進行了另外一個嘗試,利用ListView的getChildAt方法得到那個位置的View,再通過getTag方法得到ViewHolder,然後再設置圖片。但有時通過getChildAt()方法得到的View爲null,如果不處理就崩了,就算處理了肯定也得不到預先想要的結果。查了一下網上的解釋,getChildAt()方法返回的只是當前顯示出來的View,對於已經隱藏掉的也就只能返回null了。所以這個方法也廢了。
所以我又進行了另外一個嘗試,還是利用Tag,但是利用position這個固定的值。後來還是報了NullPointerException,原因是setTag方法傳入的參數是一個Object類型,把position這個int類型傳入,會自動生成Integer對象。在findViewWithTag的時候,由於convertView的複用機制,有時會使原來設置該Tag的項,動態的設置成了一個新的Tag,這樣就找不到了,所以返回的View也是空的。
沒辦法了,我只能先這樣進行處理,以後再進一步解決這個問題。如果有朋友有方法,請告知,不勝感激。