ListView在Android應用裡扮演非常重要的角色,但很多開發人員在使用ListView時都遇到過不少麻煩。一個常見的問題是:列表中要顯示一系列記錄,每條記錄帶有一張縮圖(產品照片、帳戶圖片等等),而這個縮圖是通過一個遠程URL地址來標識的。這樣的應用情境該如何?呢?
為了避免下載圖片帶來的延遲,所有遠程圖片都應該使用非同步方式載入,即使用單獨的線程下載圖片,待圖片下載完畢後顯示在ImageView裡。Android裡可以像普通Java一樣啟動新線程,但當這個線程要更新介面時,必須使用Handler來請求,否則會為應用程式帶來潛在危害。
RemoteImageHelper
為了將複雜的邏輯分離,我們單獨寫一個名為RemoteImageHelper的類來處理“非同步下載圖片並更新到介面”這個問題,這個類能夠實現以下功能:
- 圖片開始下載前,ImageView裡顯示一個表示“正在載入”的佔位圖;
- 圖片在後台下載,下載完成後顯示在ImageView裡;
- 若圖片下載失敗,ImageView顯示一個表示下載失敗的佔位圖;
下面讓我們來看一下實現代碼:
首先需要有一個方法下載遠程圖片,這裡我們不用把圖片下載到手機上,直接返回一個InputStream類型的結果即可。如果運行時這個方法報錯,請檢查是否在AndroidManifest.xml裡添加了android.permission.INTERNET許可權。
private InputStream download(String urlString) throws MalformedURLException, IOException { InputStream inputStream = (InputStream) new URL(urlString).getContent(); return inputStream;}
然後是最主要的非同步載入圖片方法,“正在下載”和“下載失敗”的圖片可根據需要自己替換。代碼如下所示:
private final Map<String, Drawable> cache = new HashMap<String, Drawable>();public void loadImage(final ImageView imageView, final String urlString, boolean useCache) { if (useCache && cache.containsKey(urlString)) { imageView.setImageDrawable(cache.get(urlString)); } //Show a "Loading" image here imageView.setImageResource(R.drawable.image_indicator); Log.d(this.getClass().getSimpleName(), "Image url:" + urlString); final Handler handler = new Handler() { @Override public void handleMessage(Message message) { imageView.setImageDrawable((Drawable) message.obj); } }; Runnable runnable = new Runnable() { public void run() { Drawable drawable = null; try { InputStream is = download(urlString); drawable = Drawable.createFromStream(is, "src"); if (drawable != null) { cache.put(urlString, drawable); } } catch (Exception e) { Log.e(this.getClass().getSimpleName(), "Image download failed", e); //Show a "download fail" image drawable = imageView.getResources().getDrawable(R.drawable.image_fail); } //Notify UI thread to show this image using Handler Message msg = handler.obtainMessage(1, drawable); handler.sendMessage(msg); } }; new Thread(runnable).start();}
關於緩衝:在這個例子裡我們使用一個記憶體中的HashMap作為圖片緩衝,它實現簡單但當應用退出後緩衝就會被清除。在實際項目裡,你可以考慮實現一個基於檔案的緩衝機制,即將下載的圖片儲存到SD卡上,注意要定期清除長期不用的圖片以節約儲存空間。
使用RemoteImageHelper
如何使用這個類呢?下面是一個例子。請注意,為了達到更好的示範效果,代碼裡在調用loadImage()方法時第三個參數用false禁止了圖片緩衝功能,在實際項目中,你很可能需要改為true來避免重複下載圖片以便提高效能。
List<MyRecord> exampleRecords;LazyImageHelper lazyImageHelper = new LazyImageHelper();class MyAdapter extends ArrayAdapter<MyRecord> { public MyAdapter(Context context) { super(context, R.layout.record_row, R.id.lblLabel, exampleRecords); } @Override public View getView(int position, View convertView, ViewGroup parent) { View view = super.getView(position, convertView, parent); MyRecord record = getItem(position); TextView lblLabel = (TextView) view.findViewById(R.id.lblLabel); ImageView imageView = (ImageView) view.findViewById(R.id.img); lblLabel.setText(record.getLabel()); //For demo purpose, cache is DISABLED here. lazyImageHelper.loadImage(imageView, record.getImageUrl(), false); //To enable cache, simply use following code: //lazyImageHelper.loadImage(imageView, record.getImageUrl(), true); return view; }}
以上代碼中的MyRecord是一個簡單的POJO類,表示一個業務對象,它具有id、label和imageUrl三個屬性。你可以在完整的工程代碼中找到它。
代碼下載
上述樣本工程編譯後的APK檔案點擊這裡下載,可運行在Android 2.1或以上版本。
上述樣本工程的原始碼點擊這裡下載。
參考資料
Handler
android的訊息處理機制
How do I do a lazy load of images in ListView
Issue 13959:Make listviews more programmer friendly