實現一個最簡單圖片列表所引發的問題,實現圖片列表引發
前一陣看了些Universal-Image-Loager的源碼。我覺得看源碼很累的一個原因就是除了看怎麼實現,就是去揣測為什麼這麼實現。這個揣測的過程很容易走馬觀花,看到後面似懂非懂。
人懶到一個地步一句話來說是能躺著就絕對不坐著,能坐著就絕對不蹲著,能蹲著就絕對不站著。有時候看源碼也是,能看懂就不會想著去debug,debug能看明白的就懶得去動手寫寫。
看和寫的感受是不一樣的。看的是結果,寫的是過程。
第三方庫的使用讓開發變得很方便,大量圖片請求的實現,大多數不再是說實現的核心,而是直接說使用什麼第三方。即便如此也沒關係,只要知道別人是怎麼實現的就好了,這很重要。Universal-Image-Loader是一個強大的圖片載入開源架構,應該都被說爛用爛了吧。這篇主要是為了去發現問題,從0開始。
看代碼的時候很多為什麼,為什麼要這麼寫,怎麼就想到會有這些問題,怎麼就想到用這種方法去解決。想找原因還是從最簡單的實現一個圖片列表開始找吧。不用任何架構,也不考慮什麼緩衝,單純的去寫一個網路請求顯示圖片列表這樣一個功能。
建立一個PhotoListActivity,這個類只顯示一個listview:
package com.aliao.learninguil.activity;import android.os.Bundle;import android.support.v7.app.AppCompatActivity;import android.widget.ListView;import com.aliao.learninguil.Constants;import com.aliao.learninguil.R;import com.aliao.learninguil.adapter.PhotoListAdapter;import com.aliao.learninguil.entity.ImageInfo;import java.util.ArrayList;import java.util.List;/** * Created by ALiao on 2015/7/13. */public class PhotoListActivity extends AppCompatActivity { private ListView mListView; private PhotoListAdapter mAdapter; private List<ImageInfo> mImageInfos = new ArrayList<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_photolist); for (int i = 0; i< Constants.IMAGES.length; i++){ ImageInfo imageInfo = new ImageInfo(); imageInfo.setUrl(Constants.IMAGES[i]); imageInfo.setName("item-" + i); mImageInfos.add(imageInfo); } mListView = (ListView) findViewById(R.id.photoList); mAdapter = new PhotoListAdapter(mImageInfos, mListView); mListView.setAdapter(mAdapter); }}listview的item的布局是左邊顯示一個ImageVeiw,右邊顯示一個textview:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/iv_img" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:id="@+id/tv_imagename" android:layout_width="wrap_content" android:layout_height="wrap_content" /></LinearLayout>
要想在listview的每個item中顯示一張網路請求的圖片,那麼在PhotoListAdapter中要啟動線程進行網路請求。
package com.aliao.learninguil.adapter;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.ListView;import android.widget.TextView;import com.aliao.learninguil.R;import com.aliao.learninguil.entity.ImageInfo;import com.aliao.learninguil.utils.L;import java.io.IOException;import java.net.HttpURLConnection;import java.net.MalformedURLException;import java.net.URL;import java.util.List;public class PhotoListAdapter extends BaseAdapter { private List<ImageInfo> imageInfos; private ListView mListView; public PhotoListAdapter(List<ImageInfo> imageInfos, ListView listView) { this.imageInfos = imageInfos; mListView = listView; } @Override public int getCount() { return imageInfos.size(); } @Override public Object getItem(int position) { return imageInfos.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null){ convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_photolist, parent, false); holder = new ViewHolder(); holder.imgView = (ImageView) convertView.findViewById(R.id.iv_img); holder.imgName = (TextView) convertView.findViewById(R.id.tv_imagename); convertView.setTag(holder); }else { holder = (ViewHolder) convertView.getTag(); } ImageInfo imageInfo = imageInfos.get(position); holder.imgName.setText(imageInfo.getName()); holder.imgView.setTag(imageInfo.getUrl()); loadAndSetImage(imageInfo.getUrl()); return convertView; } class ViewHolder{ ImageView imgView; TextView imgName; } private void loadAndSetImage(String url) { new LoadImageAsyncTask().execute(url); } class LoadImageAsyncTask extends AsyncTask<String, Void, Bitmap>{ private String mImageUrl; @Override protected Bitmap doInBackground(String... params) { mImageUrl = params[0]; Bitmap bitmap = loadBitmap(mImageUrl); return bitmap; } @Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); ImageView imageView = (ImageView) mListView.findViewWithTag(mImageUrl); if (imageView != null){ imageView.setImageBitmap(bitmap); } } } private Bitmap loadBitmap(String imageUrl) { HttpURLConnection connection = null; Bitmap bitmap = null; try { URL url = new URL(imageUrl); connection = (HttpURLConnection) url.openConnection(); bitmap = BitmapFactory.decodeStream(connection.getInputStream()); return bitmap; } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally { if (connection != null){ connection.disconnect(); } } return bitmap; }}這是實現一個圖片列表最基本的代碼了。實現的如下:
看起來顯示的效果還不錯,但是當我們上下滑動的時候明顯會感受到不夠流暢。
回到PhotoListAdapter類,每調用一次getView方法,就會去啟動線程進行網路請求。我們期望的效果是當頁面顯示5個item的時候,就進行5次網路請求,這是最理想的狀態。但是實際上getView調用的次數比預想的要多得多,也就意味著會伴隨多餘的網路請求。有哪些情況會導致多餘的網路請求呢(這裡的多餘是表示,除了當前螢幕顯示的圖片以外,進行了額外的其他圖片的網路請求)
情況一:沒有設定item的高度
listview item的布局檔案中看到,並沒有去設定ImageView的高度,item的高度是自動擴充的:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/iv_img" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:id="@+id/tv_imagename" android:layout_width="wrap_content" android:layout_height="wrap_content" /></LinearLayout>
所以,當進入圖片列表頁時,由於還未載入出圖片,預設是textview的高度,一屏能夠顯示了27個item,即調用了27次getView,也就是開啟線程進行網路請求的操作就要執行27次。但隨著前面圖片陸續載入完畢,一屏最終顯示5個item,但是剩餘的22次網路請求還在繼續。通常我們希望一屏顯示多少張圖片就去請求多少張,當要看更多的圖片的時候再去請求。而這22次網路請求既然都執行了,是不是往下滑動的時候他就可以直接顯示出已載入完的圖片?答案是否定的,因為除了當前螢幕顯示的item,其他的item都被回收了,通過findViewWithTag(imageUrl)已經找不到對應的imageView(為null)。即使圖片已經請求成功,但是由於當前螢幕沒有對應的imageview,也無法設定圖片。
@Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); ImageView imageView = (ImageView) mListView.findViewWithTag(mImageUrl); if (imageView != null){ imageView.setImageBitmap(bitmap); } }所以繼續向下滑的時候,還是會重複請求先前已經請求過的圖片。
如果提前已知item的顯示高度,例如設定ImageView的高度為100dp或者設定預設圖片,那麼一屏能夠顯示的item數量是確定的,就可以做到一屏顯示多少個item,進行相應數量的網路請求。
情況二:如果想看第三屏的圖片列表,滑過去的前兩屏的圖片已經進行了網路請求,但並沒必要
對於我們不想查看而快速滑過的圖片是沒有必要浪費資源去載入的。但是由於把載入圖片的操作放在了getView()中,只要滑動螢幕都會調用getView(),我們希望當listview滑動停止時再去載入。listview的滾動監聽事件的回呼函數可以監聽到滾動的狀態,onScrollStateChanged(AbsListView view, int scrollState)中的scrollState有三種表示listvuew的滾動狀態,分別是:
SCROLL_STATE_IDLE( = 0 ) :停止滾動
SCROLL_STATE_TOUCH_SCROLL( = 1 ) :正在滾動
SCROLL_STATE_FLING( = 2 ) :手指做了拋的動作
當scrollState的狀態為SCROLL_STATE_IDLE的時候,去下載圖片。圖片下載的時機確定了,那麼當滑動停止時當前螢幕顯示的可視圖片的地址該怎麼擷取。imageInfos對象列表存放了所有圖片資訊,知道了當前螢幕可視圖片的position位置,也就可以通過索引擷取到圖片地址了。另一個滾動監聽的回調onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)(滾動時一直回調)協助我們擷取到當前螢幕圖片的位置資訊。參數中
firstVisibleItem :當前螢幕第一張可見item的下標(從0開始)
visibleItemCount :當前螢幕所有可見item的總數
totalItemCount :清單項目總數
具體的代碼實現如下:
package com.aliao.learninguil.adapter;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.view.ViewTreeObserver;import android.widget.AbsListView;import android.widget.BaseAdapter;import android.widget.ImageView;import android.widget.ListView;import android.widget.TextView;import com.aliao.learninguil.R;import com.aliao.learninguil.entity.ImageInfo;import com.aliao.learninguil.utils.L;import java.io.IOException;import java.net.HttpURLConnection;import java.net.MalformedURLException;import java.net.URL;import java.util.HashSet;import java.util.List;import java.util.Set;public class PhotoListAdapter extends BaseAdapter implements AbsListView.OnScrollListener{ private List<ImageInfo> imageInfos; private ListView mListView; private int mFirstVisibleItem; private int mVisibleItemCount; private boolean mFirstEnter; private Set<LoadImageAsyncTask> taskCollection; public PhotoListAdapter(List<ImageInfo> imageInfos, ListView listView) { this.imageInfos = imageInfos; mListView = listView; mListView.setOnScrollListener(this); mFirstEnter = true; taskCollection = new HashSet<>(); } @Override public int getCount() { return imageInfos.size(); } @Override public Object getItem(int position) { return imageInfos.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null){ convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_photolist, parent, false); holder = new ViewHolder(); holder.imgView = (ImageView) convertView.findViewById(R.id.iv_img); holder.imgName = (TextView) convertView.findViewById(R.id.tv_imagename); convertView.setTag(holder); }else { holder = (ViewHolder) convertView.getTag(); } ImageInfo imageInfo = imageInfos.get(position); holder.imgName.setText(imageInfo.getName()); holder.imgView.setTag(imageInfo.getUrl());// L.d("position = "+position+", url = "+imageInfo.getUrl());// loadAndSetImage(imageInfo.getUrl()); return convertView; } class ViewHolder{ ImageView imgView; TextView imgName; } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { /** * scrollState = SCROLL_STATE_IDLE( = 0 )停止混動 * scrollState = SCROLL_STATE_TOUCH_SCROLL( = 1 )正在滾動 * scrollState = SCROLL_STATE_FLING( = 2 ) 手指做了拋的動作 */ L.d("-------》onScrollStateChanged scrollState = "+scrollState); if (scrollState == SCROLL_STATE_IDLE){ loadAndSetImage(mFirstVisibleItem, mVisibleItemCount); }else { //當listview再次滑動時取消所有正在下載的任務// cancelAllTasks(); } } public void cancelAllTasks() { if (taskCollection != null){ for (LoadImageAsyncTask task : taskCollection){ task.cancel(false); } } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { L.d("onScroll firstVisibleItem = "+firstVisibleItem+", visibleItemCount = "+visibleItemCount+", totalItemCount = "+totalItemCount); mFirstVisibleItem = firstVisibleItem;//第一張可見圖片的下標 mVisibleItemCount = visibleItemCount;//一屏可見圖片的總數 //首次進入程式時,onScrollStateChanged方法並不會被調用,所以在這裡首次進入程式時啟動下載任務 if (mFirstEnter && visibleItemCount > 0){ loadAndSetImage(mFirstVisibleItem, mVisibleItemCount); mFirstEnter = false; } } private void loadAndSetImage(int firstVisibleItem, int visibleItemCount) { for (int i = firstVisibleItem; i< firstVisibleItem + visibleItemCount; i++){ ImageInfo imageInfo = imageInfos.get(i); L.d("position = "+i+", url = "+imageInfo.getUrl()); LoadImageAsyncTask task = new LoadImageAsyncTask(); task.execute(imageInfo.getUrl()); taskCollection.add(task); } } class LoadImageAsyncTask extends AsyncTask<String, Void, Bitmap>{ private String mImageUrl; @Override protected Bitmap doInBackground(String... params) { mImageUrl = params[0]; Bitmap bitmap = loadBitmap(mImageUrl); return bitmap; } @Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); ImageView imageView = (ImageView) mListView.findViewWithTag(mImageUrl); if (imageView != null){ imageView.setImageBitmap(bitmap); } } } private Bitmap loadBitmap(String imageUrl) { HttpURLConnection connection = null; Bitmap bitmap = null; try { URL url = new URL(imageUrl); connection = (HttpURLConnection) url.openConnection(); bitmap = BitmapFactory.decodeStream(connection.getInputStream()); L.d("------------------------------------loadBitmap "); return bitmap; } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally { if (connection != null){ connection.disconnect(); } } return bitmap; }}除了當listview再次滑動的時候,取消所有正在下載的任務,當Activity銷毀時,在onDestroy方法中可以調用PhotoListAdapter中的cancelAllTasks()來取消所有還未完成的下載任務。
到目前為止通過確定list item的高度以及對啟動圖片下載任務的時機的修改,做到了只下載所要查看到的圖片,避免多餘的網路請求,減少了網路請求的負荷和節省了手機流量。
解決了上面的問題後,代碼又完善健壯了一步,想想接下來還會有什麼問題。
參考:
使用記憶體緩衝實現圖片牆
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。