ListViewにHTTPで取得した画像を表示する方法

マッシュアップ系のアプリなどを作っていると、サーバー上にある画像を取得して表示するっていう内容の処理が結構あるとおもいます。
ちょっと前に結構はまったので、記載しておくことにします。


ListViewに表示するので、ArrayAdapterを継承したクラスを作ります。
画像を取得するので、getViewの中で、スレッドを起動して別のタスクで画像を取得するようにします。以下のような感じです。

public class HogeAdapter extends ArrayAdapter<Hoge> {
	private LayoutInflater inflater;

	// コンストラクタ
	public HogeAdapter(Context context, int textViewResourceId) {
		super(context, textViewResourceId);
		inflater = (LayoutInflater)super.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
	}
	
	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		if (convertView == null) {
			convertView = inflater.inflate(R.layout.child, parent, false);
		}
		// Hogeオブジェクトを取得
		Hoge hoge = super.getItem(position);
		
		// imageを取得
		ImageView image = (ImageView)convertView.findViewById(R.id.child_image);
		// 一旦仮の画像を入れておく
		image.setImageDrawable(getContext().getResources().getDrawable(R.drawable.load));
		// 画像取得スレッド起動
		ImageGetTask task = new ImageGetTask(image);
		task.execute(hoge.imageUrl);

		return convertView;
	}

	
	// Image取得用スレッドクラス
	class ImageGetTask extends AsyncTask<String,Void,Bitmap> {
		private ImageView image;
		
		public ImageGetTask(ImageView _image) {
			image = _image;
		}
		@Override
		protected Bitmap doInBackground(String... params) {
			// ここでHttp経由で画像を取得します。取得後Bitmapで返します。
			return null;
		}
		@Override
	    protected void onPostExecute(Bitmap result) {
			// 取得した画像をImageViewに設定します。
			image.setImageBitmap(result);
	    }
	}
}

スレッドで画像を取得後、onPostExecuteでImageViewに画像を設定しています。
一見うまく動いているように見えるのですが、ListViewを上下に動かしていると画像がちらちら入れ替わったりしてしまいます。
これは、ListViewがリソースを最小限に抑えるようにうまくできているようで、子供のViewを使いまわしていることが原因です。
スレッド開始時のImageViewと終了時のImageViewが必ずしも同じViewだとは限らないのですね(参照が変わってしまっている可能性がある)


回避方法としては、ImageViewにTagをつけてやることで参照が変わったかを判断してやります。以下のような形です。

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		if (convertView == null) {
			convertView = inflater.inflate(R.layout.child, parent, false);
		}
		// Hogeオブジェクトを取得
		Hoge hoge = super.getItem(position);
		
		// imageを取得
		ImageView image = (ImageView)convertView.findViewById(R.id.child_image);
		// 一旦仮の画像を入れておく
		image.setImageDrawable(getContext().getResources().getDrawable(R.drawable.load));
		// Tagを設定しておく
		image.setTag(hoge.imageUrl);
		// 画像取得スレッド起動
		ImageGetTask task = new ImageGetTask(image);
		task.execute(hoge.imageUrl);

		return convertView;
	}

	
	// Image取得用スレッドクラス
	class ImageGetTask extends AsyncTask<String,Void,Bitmap> {
		private ImageView image;
		private String tag;
		
		public ImageGetTask(ImageView _image) {
			image = _image;
			tag = image.getTag().toString();
		}
		@Override
		protected Bitmap doInBackground(String... params) {
			// ここでHttp経由で画像を取得します。取得後Bitmapで返します。
			return null;
		}
		@Override
	    protected void onPostExecute(Bitmap result) {
			// Tagが同じものが確認して、同じであれば画像を設定する
			if (tag.equals(image.getTag())) {
				image.setImageBitmap(result);
			}
	    }
	}

getViewの中で、ImageViewにsetTagでurlを設定しておきます。
ImageGetTaskの中で、そのタグを保持しておいて、onPostExecuteでTagが同じかを確認します。同じであれば画像を設定します。
他にも方法はあるかと思いますが、とりあえずこの方法で、画像が入れ替わってしまうような現象は回避できるかとおもいます。


あと注意点があるのですが、このままだと、getViewが呼ばれるたびにスレッドを起動してしまいます。なので、ListViewをすごい勢いでスクロールとかさせると速攻でOOMが出ます(汗)
対策としては、画像をキャッシュするとか、スレッドの起動数を最大いくつまでにするとかの制御が必要になります。
今回は説明の為にそういう処理は端折ってます。


追記 2010/10/24
別の方法として、イメージのURLを登録するテーブルを作成して、getViewの中では、そのテーブルにデータを登録だけしておいて、ListViewのスクロールが止まった段階とかでデータを取得しにいくなんて方法もありそうです。とゆーかそっちのほうがいいかも。その内サンプル作ります。