특정 URL 이미지를 가져와 ImageView에 보여주는 기능을 구현해보자. 먼저 이 기능을 구현하는데에는 아래 글에서 구현했던 기능들이 재사용된다. 그러니, 코드 중간에 설명되지 않는 내용들은 아래 글들을 읽어보면 이해가 될 것이다.
- 안드로이드 이미지 캐시 구현
http://javacan.tistory.com/entry/android-image-cache-implementation - 안드로이드에서 URL로 파일 다운 받아 로컬에 저장하기
http://javacan.tistory.com/entry/save-file-from-url-in-android - 유지보수를 고려한 안드로이드 비동기 처리 기반 코드 만들기
http://javacan.tistory.com/entry/maintainable-async-processing-code-based-on-AsyncTask
클래스 구성
URL로부터 이미지를 다운받아 ImaveView에 출력하는 기능을 구현하기 위한 클래스는 다음과 같다.
- ImageDownloadAsyncCallback
- AsyncExecutor에 전달되는 콜백 (AsyncExecutor 및 콜백 구현에 대한 내용은 '유지보수를 고려한 안드로이드 비동기 처리 기반 코드 만들기' 글 참고)
- 다운받은 파일을 이미지 캐시에 추가하고 ImageView에 반영
- ImageDownloader
- 기본 이미지를 ImageView에 반영 후, AsyncExecutor를 이용해서 이미지 다운로드를 실행
ImageDownloadAsyncCallback 클래스
ImageDownloadAsyncCallback 클래스는 AsyncExecutor에 전달되는 콜백 객체를 구현하고 있다.
public class ImageDownloadAsyncCallback implements AsyncCallback<File>,
AsyncExecutorAware<File> {
private String url;
private WeakReference<ImageView> imageViewReference;
private AsyncExecutor<File> asyncExecutor;
private ImageCache imageCache;
public ImageDownloadAsyncCallback(String url, ImageView imageView,
ImageCache imageCache) {
this.url = url;
this.imageViewReference = new WeakReference<ImageView>(imageView);
this.imageCache = imageCache;
}
@Override
public void setAsyncExecutor(AsyncExecutor<File> asyncExecutor) {
this.asyncExecutor = asyncExecutor;
}
public boolean isSameUrl(String url2) {
return url.equals(url2);
}
@Override
public void onResult(File bitmapFile) {
Bitmap bitmap = addBitmapToCache(bitmapFile);
applyBitmapToImageView(bitmap);
}
private Bitmap addBitmapToCache(File bitmap) {
imageCache.addBitmap(url, bitmap);
return imageCache.getBitmap(url);
}
private void applyBitmapToImageView(Bitmap bitmap) {
ImageView imageView = imageViewReference.get();
if (imageView != null) {
if (isSameCallback(imageView)) {
imageView.setImageBitmap(bitmap);
imageView.setTag(null);
}
}
}
private boolean isSameCallback(ImageView imageView) {
return this == imageView.getTag();
}
public void cancel(boolean b) {
asyncExecutor.cancel(true);
}
@Override
public void exceptionOccured(Exception e) {
}
@Override
public void cancelled() {
}
}
AsyncExecutor가 파일 다운로드를 완료하면 onResult() 메서드가 호출되는데, onResult() 메서드는 전달받은 이미지 파일을 캐시에 추가하고 ImageView에 반영한다.
applyBitmapToImageView() 메서드는
- imageViewReference로부터 ImageView를 구한다.
- imageView가 존재한다면,
- imageView의 콜백 객체가 this와 동일한지 확인한다.
- 참고로, 뒤에서 살펴볼 ImageDownloader는 이미지 다운로드가 시작되면, ImageView의 tag에 관련된 콜백 객체를 설정한다.
- 동일하다면, imageView에 이미지를 반영한다.
- 동일하지 않다면, 이 콜백이 생성된 뒤에 동일한 ImageView에 대해 다른 URL 요청이 있었다는 것이므로 이미지를 imageView에 반영하지 않는다.
ImageDownloader 클래스
ImageDownloader 클래스의 코드 중 앞 부분은 다음과 같다.
// ImageDownloader의 앞 부분
public class ImageDownloader {
static final String TAG = "ImageDownloader";
private Context context;
private ImageCache imageCache;
public ImageDownloader(Context context, String imageCacheName) {
this.context = context;
imageCache = ImageCacheFactory.getInstance().get(imageCacheName);
}
public void download(String url, ImageView imageView, Drawable noImageDrawable) {
Bitmap bitmap = imageCache.getBitmap(url);
if (bitmap == null) {
forceDownload(url, imageView, noImageDrawable);
} else {
cancelPotentialDownload(url, imageView);
imageView.setImageBitmap(bitmap);
}
}
private void forceDownload(String url, ImageView imageView, Drawable noImageDrawable) {
if (!cancelPotentialDownload(url, imageView))
return;
imageView.setImageDrawable(noImageDrawable);
runAsyncImageDownloading(url, imageView);
}
private boolean cancelPotentialDownload(String url, ImageView imageView) {
ImageDownloadAsyncCallback asyncCallback =
(ImageDownloadAsyncCallback) imageView.getTag();
if (asyncCallback == null)
return true;
if (asyncCallback.isSameUrl(url))
return false;
asyncCallback.cancel(true);
return true;
}
ImageDownloader는 '안드로이드 이미지 캐시 구현' 글에서 만들었던 ImageCache를 이용해서 다운로드 받은 이미지 파일을 캐시하는데, 생성자를 통해서 사용할 캐시 이름을 전달받는다.
download() 메서드는 다음과 같이 세 개의 파라미터를 받는다.
- download(String url, ImageView imageView, Drawable noImageDrawable)
첫 번째 파라미터는 이미지 URL이고, 두 번째 파라미터는 이미지를 보여줄 ImageView이다. 그리고 세 번째 noImageDrawable은 이미지를 다운로드 받는 동안에 imageView에 보여줄 이미지이다.
download 메서드는
- 캐시에 이미지가 존재하는 지 확인한다.
- 캐시에 존재하지 않으면 이미지 다운로드를 실행한다. (forceDownload)
- 캐시에 존재하면
- cancelPotentialDownload()를 호출해서 ImageView에 대해 다른 URL을 다운로드 중이라면 작업을 취소하고,
- 캐시에서 읽어온 이미지를 imageView에 반영한다.
forceDownload() 메서드는
- cancelPotentialDownload() 메서드가 false를 리턴한 경우 바로 리턴한다.
- 이 메서드가 false를 리턴하면, 같은 URL에 대해 이미 다운로드가 진행중이라는 것이다.
- cancelPotentialDownload()가 true를 리턴한 경우
- imageView의 이미지로 noImageDrawble를 설정한 뒤에
- runAsyncImageDownloading()을 실행해서 이미지 다운로드를 시작한다.
cancelPotentialDownload() 메서드는 한 개의 ImageView에 대해 서로 다른 이미지를 로딩하는 것을 방지하기 위한 코드이다. 예를 들어, ListView에서 사용되는 ImageView는 재사용될 가능성이 높은데 이 경우 시간 간격을 두고 ImageView에 서로 다른 URL 이미지 다운로드가 실행될 수 있다. 이 경우 앞서 요청한 이미지는 다운로드 할 필요가 없기 때문에 다운로드를 취소함으로써 불필요한 네트워크 사용을 방지할 수 있을 것이다. cancelPotentialDownload() 메서드가 바로 이 취소작업을 처리한다.
뒤에서 살펴볼 runAsyncImageDownloading() 메서드는 이미지 다운로드를 시작하기 전에 ImageView의 tag 값으로 ImageDownloadAsyncCallback 객체를 보관하는데, cancelPottentialDownlad() 메서드는 이 tag 객체를 이용해서 ImageView와 관련된 이미지 다운로드가 진행중인 것이 있는지 확인한다.
cancelPotentialDownload() 메서드는
- imageView.getTag()를 이용해서 ImageDownloadAsyncCallback 객체를 구한다.
- imageView의 tag가 null이면 ImageView와 관련된 다운로드가 진행중이지 않은 것이므로 true를 리턴한다.
- tag에 보관된 콜백 객체의 url이 새롭게 요청한 url과 같으면 이미 동일 URL에 대한 다운로드가 진행중인 것이므로 false를 리턴한다.
- 콜백 객체의 url이 요청 url과 다르면, 다른 이미지를 다운로드하고 있는 것이므로 작업 취소를 요청하고 false를 리턴한다.
실제로 이미지 다운로드를 시작하는 runAsyncImageDownloading() 메서드 코드 및 관련 메서드를 아래 코드에 표시하였다.
// ImageDownloader의 뒷 부분
private void runAsyncImageDownloading(String url, ImageView imageView) {
File tempFile = createTemporaryFile();
if (tempFile == null)
return;
Callable<File> callable = new FileDownloadCallable(url, tempFile);
ImageDownloadAsyncCallback callback = new ImageDownloadAsyncCallback(
url, imageView, imageCache);
imageView.setTag(callback);
new AsyncExecutor<File>().setCallable(callable).setCallback(callback).execute();
}
private File createTemporaryFile() {
try {
return File.createTempFile("image", ".tmp", context.getCacheDir());
} catch (IOException e) {
Log.e(TAG, "fail to create temp file", e);
return null;
}
}
}
runAsyncImageDownloading() 메서드는
- 다운로드 받은 내용을 저장할 임시 파일을 생성한다. (createTemporaryFile())
- 파일 다운로드 작업을 실행하는 FileDownloadCallable 객체를 생성한다. (이 클래스는 '안드로이드에서 URL로 파일 다운 받아 로컬에 저장하기' 글에서 작성한 것이다.)
- 비동기 파일 다운로드가 완료될 때 사용될 ImageDownloadAsyncCallback 객체를 생성하고, ImageView의 tag 값으로 콜백 객체를 설정한다.
- AsyncExecutor를 이용해서 비동기 파일 다운로드를 시작한다.
ImageDownloader 클래스 사용
ImageDownloader 클래스의 사용 방법은 다음과 같이 간단하다.
-- 이미지 캐시를 사용하기 때문에, onCreate() 같은 곳에서 파일캐시/이미지캐시 초기화 필요
// 2레벨 캐시(이미지 파일 캐시)를 사용하려면 동일 이름의 파일 캐시를 생성해 주어야 한다.
FileCacheFactory.getInstance().create("imagecache", cacheSize);
// 이미지 캐시 초기화
ImageCacheFactory.getInstance().createTwoLevelCache("imagecache", 40);
-- ImageDownloader가 필요한 코드에서 사용, 예를 들면 LiveView의 Adapter
public class FeaturedToonAdapter extends BaseAdapter {
private ImageDownloader imageDownloader;
public FeaturedToonAdapter(Context context) {
...
this.imageDownloader = new ImageDownloader(context, "imagecache");
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
...
BitmapDrawable noImage = new BitmapDrawable(context.getResources(),
BitmapFactory.decodeResource(context.getResources(), R.drawable.noimage))
imageDownloader.download(imageUrl, imageView, noImage);
return row;
}
관련자료
- 안드로이드 이미지 캐시 구현
http://javacan.tistory.com/entry/android-image-cache-implementation - 안드로이드에서 URL로 파일 다운 받아 로컬에 저장하기
http://javacan.tistory.com/entry/save-file-from-url-in-android - 유지보수를 고려한 안드로이드 비동기 처리 기반 코드 만들기
http://javacan.tistory.com/entry/maintainable-async-processing-code-based-on-AsyncTask - Image Downloader: Multithreading For Performance
http://android-developers.blogspot.kr/2010/07/multithreading-for-performance.html