Androidで、ネットにある画像をImageViewで表示する(UrlImageView)

AndroidのImageViewで、インターネット上にある画像を表示する独自ImageViewを作成しました。
ソースはすべてgithubで管理してます。MIT Licenseで、自由にお使いください。
名前、UrlImageView。適当でごめんなさい。
https://github.com/sharakova/UrlImageView

利用方法とソースの内容も書きますが、若干違いがあるかも・・・。

このクラスの利点

  • 画像のURLを指定するだけで、ネットにある画像を非同期で取得、キャッシュ、表示を簡単に実装できます。
  • 本当に、ほとんど何も気にしなくても、利用できます。
  • layoutファイルで、ImageViewからこのクラス(UrlImageView)に変更して、setImageUrl(String url);を呼び出すだけです。
  • OnImageLoadListener のリスナ登録で、ネットから画像を読み込む前後で処理を実行できます。
  • 読み込んだ画像は、一時的にAndroid内にキャッシュし、2度目の表示では高速に読み込む事ができます。
  • サンプルソースのonDestroyで実行している ImageCache.deleteAll(getCacheDir()); で、Android内に保存したキャッシュを削除いたします。実行しなければ、アプリをアンインストールもしくはユーザの操作でキャッシュを削除するまでデータを保持します。


ちょっと内部的な話

  • 複数のUrlImageViewを利用されると、画像の読み込み順番がLIFOの方がなにかと良かったので、画像を読み込む優先順位を変更しました。
  • WorkerThreadを実装しました。スレッド数は5つ。
  • GridViewなどで利用した場合、ユーザ操作で見たい画像がある程度先に読まれるようになった。
  • 画像のキャッシュの圧縮率は90です。また、アニメーションGIFなどには対応してません。
  • 画像のキャッシュデータをファイルに書き出す際に若干遅いので、今後の課題です。
  • ファイルが存在しない場合(404 Not Found)とかの場合、処理が分けられないのも課題です。


利用方法
Android マニフェスト

<uses-permission android:name="android.permission.INTERNET"/>

layoutファイル

  • レイアウトファイルでは、ImageViewと同様の設定が可能。設定例。
<jp.sharakova.android.urlimageview.UrlImageView
    android:id="@+id/imageView"
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent"
    />

Activity サンプル

package jp.sharakova.android.urlimageview.sample;

import jp.sharakova.android.urlimageview.ImageCache;
import jp.sharakova.android.urlimageview.R;
import jp.sharakova.android.urlimageview.UrlImageView;
import jp.sharakova.android.urlimageview.UrlImageView.OnImageLoadListener;
import android.app.Activity;
import android.os.Bundle;
import android.widget.Toast;

public class UrlImageViewSampleActivity extends Activity {	
    UrlImageView mImageView;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);        
        mImageView = (UrlImageView)findViewById(R.id.imageView);
        mImageView.setImageUrl("http://k.yimg.jp/images/top/sp/logo.gif", imageLoadListener);
    }   

    @Override
    public void onDestroy() {
    	ImageCache.deleteAll(getCacheDir());
    	super.onDestroy();
    }

    final private OnImageLoadListener imageLoadListener = new OnImageLoadListener() {
		@Override
		public void onStart(String url) {
			Toast.makeText(getApplicationContext(), "start", Toast.LENGTH_SHORT).show();
		}
		@Override
		public void onComplete(String url) {
			Toast.makeText(getApplicationContext(), "end", Toast.LENGTH_SHORT).show();
		}
    };  
}


各ソースの説明

  • UrlImageView.java
    • ImageViewを拡張したViewクラス
  • Channel.java
    • WorkerThreadの生成
    • Requestキュー管理 (Request Queue) LIFOを実装
  • ImageCache.java
    • メモリで画像データをキャッシュ。
    • キャッシュディレクトリにファイルとして保存
    • キャッシュデータの読み出し
  • Request.java
    • 読み込む画像のURL
    • キャッシュディレクトリの保存
  • WorkerThread.java
    • ChannelからRequestを取得し、ネットから画像を読み込む
    • Threadクラス
    • ステータスの保持


UrlImageViewクラス

package jp.sharakova.android.urlimageview;

import java.lang.ref.SoftReference;

import android.content.Context;
import android.graphics.Bitmap;
import android.os.Handler;
import android.util.AttributeSet;
import android.widget.ImageView;


public class UrlImageView extends ImageView {
	private final Context context;
	
	private Request request;
	private String url;
	private final Handler handler = new Handler();
	private OnImageLoadListener listener = new OnImageLoadListener() {
		public void onStart(String url) {
		}
		public void onComplete(String url) {
		}		
	};
	
    public static interface OnImageLoadListener {
        public void onStart(String url);
        public void onComplete(String url);
    }
	
	private final Runnable threadRunnable = new Runnable() {
		public void run() {
			handler.post(imageLoadRunnable);
		}
	};
	
	private final Runnable imageLoadRunnable = new Runnable(){
		public void run(){
			setImageLocalCache();
		}
	};
	
	private boolean setImageLocalCache() {
		SoftReference<Bitmap> image = ImageCache.getImage(context.getCacheDir(),url);
		if(image != null && image.get() != null) {
			setImageBitmap(image.get());
			listener.onComplete(url);
			return true;
		}
		return false;
	}
	
	public UrlImageView(Context context) {
		super(context);
		this.context = context;
	}
	
	public UrlImageView(Context context, AttributeSet attrs) {
		super(context, attrs);
		this.context = context;
	}

	public UrlImageView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		this.context = context;
	}
	
	public void setOnImageLoadListener(OnImageLoadListener listener) {
		this.listener = listener;
	}
		
	public void setImageUrl(String url, OnImageLoadListener listener) {
		setOnImageLoadListener(listener);
		setImageUrl(url);
	}
	
	public void setImageUrl(String url) {
		this.url = url;
		request = new Request(url, context.getCacheDir(), threadRunnable);
		if(setImageLocalCache()){
			return;
		}
		
		listener.onStart(url);
		Channel.getInstance().putRequest(request);
	}
}


Channelクラス

package jp.sharakova.android.urlimageview;

import java.util.LinkedList;

public class Channel {
	private static Channel instance;
	private static final int MAX_THREAD = 5;
	private final LinkedList<Request> requestQueue = new LinkedList<Request>();
	private final WorkerThread[] threadPool;
	
	private Channel() {
		threadPool = new WorkerThread[MAX_THREAD];
		for (int i = 0; i < threadPool.length; i++) {
			threadPool[i] = new WorkerThread("Worker-" + i, this);
		}
	}
	
	public static Channel getInstance() {
		if(instance == null) {
			instance = new Channel();
			instance.startWorkers();
		}
		return instance;
	}
	
	public synchronized void removeQueueAll() {
		requestQueue.clear();
	}
	
	public void startWorkers() {
		for (WorkerThread thread : threadPool) {
			thread.start();
		}
	}
	
	public void stopWorkers() {
		for (WorkerThread thread : threadPool) {
			thread.stop();
		}		
	}
	
	public synchronized void putRequest(Request request) {
		requestQueue.addFirst(request);
		notifyAll();
	}
	
	public synchronized Request takeRequest() {
		while (requestQueue.size() <= 0) {
			try {
				wait();
			} catch (InterruptedException e) {
			}
		}		
		return requestQueue.poll();
	}
}

ImageCacheクラス

package jp.sharakova.android.urlimageview;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.HashMap;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;

public class ImageCache {
	
	private static HashMap<String,SoftReference<Bitmap>> cache = new HashMap<String,SoftReference<Bitmap>>();
	
	private static String getFileName(String url) {
		int hash = url.hashCode();
		return String.valueOf(hash);
	}
		
	public static void saveBitmap(File cacheDir, String url, Bitmap bitmap) {
		cache.put(url, new SoftReference<Bitmap>(bitmap));
		
		String fileName = getFileName(url);
		File localFile = new File(cacheDir, fileName);
		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(localFile);
			bitmap.compress(Bitmap.CompressFormat.PNG, 90, fos);
		} catch (Exception e) {
			e.printStackTrace();
		} catch (OutOfMemoryError e) {
			e.printStackTrace();
		} finally {
			if (fos != null) {
				try {
					fos.close();
				} catch (IOException e1) {
					Log.w("save", "finally");
				}
			}
		}
	}
	
	public static SoftReference<Bitmap> getImage(File cacheDir, String url) {
		
		SoftReference<Bitmap> ref = cache.get(url);
		if(ref != null && ref.get() != null) {
			return ref;
		}
		
		String fileName = getFileName(url);
		File localFile = new File(cacheDir, fileName);
		SoftReference<Bitmap> bitmap = null;
		try {
			bitmap = new SoftReference<Bitmap>(BitmapFactory.decodeFile(localFile.getPath()));
		} catch (Exception e) {
			e.printStackTrace();
		} catch (OutOfMemoryError e) {
			e.printStackTrace();
		}
		return bitmap;
	}
	
	public static void deleteAll(File cacheDir) {
		if (!cacheDir.isDirectory()){
			return;
		}
		File[] files = cacheDir.listFiles();
		for (File file : files) {
			if (file.isFile()){
				file.delete();
			}			
		}
	}
}


Requestクラス

package jp.sharakova.android.urlimageview;

import java.io.File;


public class Request {
	private final String url;
	private final File cacheDir;
	private final Runnable runnable;
	private int status = Status.WAIT;
		
    public interface Status {
        int WAIT = 0;
        int LOADING = 1;
        int LOADED = 2;
    }
    	
	public Request(String url, File cacheDir, Runnable runnable) {
		this.url = url;
		this.cacheDir = cacheDir;
		this.runnable = runnable;
	}
	
	public synchronized void setStatus(int status) {
		this.status = status;
	}
	
	public int getStatus() {
		return status;
	}
	
	public String getUrl() {
		return url;
	}
	
	public File getCacheDir() {
		return cacheDir;
	}
	
	public Runnable getRunnable() {
		return (runnable != null)? runnable : getDefaultRunnable();
	}
	
	private Runnable getDefaultRunnable() {
		return new Runnable() {
			public void run() {
			}
		};
	}
}


WorkerThreadクラス

package jp.sharakova.android.urlimageview;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.net.HttpURLConnection;
import java.net.URL;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

public class WorkerThread extends Thread {
	private final Channel channel;

	public WorkerThread(String name, Channel channel) {
		super(name);
		this.channel = channel;
	}
	
	public void run() {
		while (true) {
			Request request = channel.takeRequest();
			request.setStatus(Request.Status.LOADING);
			SoftReference<Bitmap> image = ImageCache.getImage(request.getCacheDir(), request.getUrl());
			if(image == null || image.get() == null) {
				image = getImage(request.getUrl());
				if(image != null && image.get() != null) {
					ImageCache.saveBitmap(request.getCacheDir(), request.getUrl(), image.get());
				}
			}
            request.setStatus(Request.Status.LOADED);
        	request.getRunnable().run();
		}
	}
	
    private SoftReference<Bitmap> getImage(String url) {
    	try {
	   		byte[] byteArray = getByteArrayFromURL(url);
	    	return new SoftReference<Bitmap>(BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length));
    	} catch (Exception e) {
    		e.printStackTrace();
    		return null;
    	} catch (OutOfMemoryError e) {
    		e.printStackTrace();
    		return null;
    	}
    }
    
    private byte[] getByteArrayFromURL(String strUrl) {
        byte[] byteArray = new byte[1024];
        HttpURLConnection con = null;
        InputStream in = null;
        ByteArrayOutputStream out = null;
        int size = 0;
        try {
            URL url = new URL(strUrl);
            con = (HttpURLConnection) url.openConnection();
            con.setUseCaches(true);
            con.setRequestMethod("GET");
            con.connect();
            in = con.getInputStream();

            out = new ByteArrayOutputStream();
            while ((size = in.read(byteArray)) != -1) {
                out.write(byteArray, 0, size);
            }
            return out.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
            return new byte[0];
        } catch (OutOfMemoryError e) {
        	e.printStackTrace();
        	return new byte[0];
        } finally {
            try {
                if (con != null)
                    con.disconnect();
                if (in != null)
                    in.close();
                if (out != null)
                    out.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}