I have a gridview of about 1500 images. I'm using thumbnails from MediaStore to show them. But when I've populated the grid in UI thread it worked too slow for the first time (thumbnails not created yet?). So I've decided to move this process to background. Using this tutorial and one another I've written this class:
public class ImageLoader {
// -----------------------------------------------------------------------
//
// Fields
//
// -----------------------------------------------------------------------
private static Context mContext;
private static ImageLoader mImageLoader = new ImageLoader();
// -----------------------------------------------------------------------
//
// Static methods
//
// -----------------------------------------------------------------------
public static void init(Application application){
mContext = application;
}
public static ImageLoader getInstance(){
return mImageLoader;
}
// -----------------------------------------------------------------------
//
// Methods
//
// -----------------------------------------------------------------------
public void download(ImageView imageView, final long id) {
Boolean b = cancelPotentialDownload(id, imageView);
Log.v("Loader", "download " + b.toString());
if (b) {
DownloadImageTask task = new DownloadImageTask(imageView, id);
DownloadedDrawable downloadedDrawable = new DownloadedDrawable(task);
imageView.setImageDrawable(downloadedDrawable);
task.execute();
}
}
// -----------------------------------------------------------------------
//
// Inner classes
//
// -----------------------------------------------------------------------
private class DownloadImageTask extends AsyncTask<Void, Void, Bitmap>{
private final WeakReference<ImageView> iv;
private final long id;
public DownloadImageTask(ImageView imageView, long id){
iv = new WeakReference<ImageView>(imageView);
this.id = id;
}
@Override
protected Bitmap doInBackground(Void... params) {
Bitmap bitmap = downloadImageThumbnail(id);
return bitmap;
}
@Override
protected void onPostExecute(Bitmap result) {
Log.v("Task", "completed " + (result == null));
if (iv != null) {
ImageView imageView = iv.get();
DownloadImageTask bitmapDownloaderTask = getDownloadImageTask(imageView);
// Change bitmap only if this process is still associated with it
if (this == bitmapDownloaderTask) {
imageView.setImageBitmap(result);
}
}
}
}
static class DownloadedDrawable extends ColorDrawable {
private final WeakReference<DownloadImageTask> downloadImageTaskReference;
public DownloadedDrawable(DownloadImageTask downloadImageTask) {
super(Color.TRANSPARENT);
downloadImageTaskReference = new WeakReference<DownloadImageTask>(downloadImageTask);
}
public DownloadImageTask getDownloadImageTask() {
return downloadImageTaskReference.get();
}
}
// -----------------------------------------------------------------------
//
// Private static helper methods
//
// -----------------------------------------------------------------------
private static boolean cancelPotentialDownload(long id, ImageView imageView) {
DownloadImageTask downloadImageTask = getDownloadImageTask(imageView);
if (downloadImageTask != null) {
long imageId = downloadImageTask.id;
if ((imageId == 0) || (imageId != id)) {
Boolean b = downloadImageTask.cancel(true);
Log.v("Task", "Stopped " + b.toString());
} else {
// The same URL is already being downloaded.
return false;
}
}
return true;
}
private static DownloadImageTask getDownloadImageTask(ImageView imageView) {
if (imageView != null) {
Drawable drawable = imageView.getDrawable();
if (drawable instanceof DownloadedDrawable) {
DownloadedDrawable downloadedDrawable = (DownloadedDrawable)drawable;
return downloadedDrawable.getDownloadImageTask();
}
}
return null;
}
// -----------------------------------------------------------------------
//
// Private helper methods
//
// -----------------------------------------------------------------------
private Bitmap downloadImageThumbnail(long id) {
Bitmap bitmap = MediaStore.Images.Thumbnails.getThumbnail(mContext.getContentResolver(), id, MediaStore.Images.Thumbnails.MICRO_KIND, null);
return bitmap;
}
}
But there was an issue with such a big number of images - there was a lot of tasks running at the same time so after activity starts there is a blank gridview and then suddenly it becomes filled with images (I've read AsyncTask sources and didn't understand why it works this way - there is only one task running at the same time). And another one - if you fastly scroll down a large listview of images (1500) then you'll need to wait too long to see the result. Anyway I need to make gridview populated smoothly. So I've decided to manage tasks myself and made some modifications:
private static final int POOL_CAPACITY = 10;
// -----------------------------------------------------------------------
//
// Fields
//
// -----------------------------------------------------------------------
private static Context mContext;
private static ImageLoader mImageLoader = new ImageLoader();
private static ArrayList<DownloadImageTask> executingPool = new ArrayList<ImageLoader.DownloadImageTask>();
private static ArrayList<DownloadImageTask> waitingPool = new ArrayList<ImageLoader.DownloadImageTask>();
// -----------------------------------------------------------------------
//
// Static methods
//
// -----------------------------------------------------------------------
public static void init(Application application){
mContext = application;
}
public static ImageLoader getInstance(){
return mImageLoader;
}
// -----------------------------------------------------------------------
//
// Methods
//
// -----------------------------------------------------------------------
public void download(ImageView imageView, final long id) {
Boolean b = cancelPotentialDownload(id, imageView);
Log.v("Loader", "download " + b.toString());
if (b) {
DownloadImageTask task = new DownloadImageTask(imageView, id);
Log.v("Loader","download " + task.hashCode() + " ex size " + executingPool.size() + " wa size " + waitingPool.size());
DownloadedDrawable downloadedDrawable = new DownloadedDrawable(task);
imageView.setImageDrawable(downloadedDrawable);
addTask(task);
}
}
private void addTask(DownloadImageTask task){
Log.v("Loader", "addTask " + task.hashCode() + " ex size " + executingPool.size() + " wa size " + waitingPool.size());
if(executingPool.size() < POOL_CAPACITY) {
executingPool.add(task);
task.execute();
}
else
waitingPool.add(task);
}
private synchronized void executeNext(DownloadImageTask finished){
Log.v("Loader", "executeNext " + finished.hashCode() + " ex size " + executingPool.size() + " wa size " + waitingPool.size());
executingPool.remove(finished);
if(waitingPool.size() > 0){
DownloadImageTask task = waitingPool.remove(0);
addTask(task);
}
}
// -----------------------------------------------------------------------
//
// Inner classes
//
// -----------------------------------------------------------------------
private class DownloadImageTask extends AsyncTask<Void, Void, Bitmap>{
private final WeakReference<ImageView> iv;
private final long id;
public DownloadImageTask(ImageView imageView, long id){
iv = new WeakReference<ImageView>(imageView);
this.id = id;
}
@Override
protected Bitmap doInBackground(Void... params) {
Log.v("Loader", "doInBackground " + this.hashCode() + " ex size " + executingPool.size() + " wa size " + waitingPool.size());
Bitmap bitmap = downloadImageThumbnail(id);
return bitmap;
}
@Override
protected void onPostExecute(Bitmap result) {
Log.v("Task", "completed " + (result == null));
if (iv != null) {
ImageView imageView = iv.get();
DownloadImageTask bitmapDownloaderTask = getDownloadImageTask(imageView);
// Change bitmap only if this process is still associated with it
if (this == bitmapDownloaderTask) {
imageView.setImageBitmap(result);
}
}
executeNext(this);
}
}
static class DownloadedDrawable extends ColorDrawable {
private final WeakReference<DownloadImageTask> downloadImageTaskReference;
public DownloadedDrawable(DownloadImageTask downloadImageTask) {
super(Color.TRANSPARENT);
downloadImageTaskReference = new WeakReference<DownloadImageTask>(downloadImageTask);
}
public DownloadImageTask getDownloadImageTask() {
return downloadImageTaskReference.get();
}
}
// -----------------------------------------------------------------------
//
// Private static helper methods
//
// -----------------------------------------------------------------------
private static boolean cancelPotentialDownload(long id, ImageView imageView) {
DownloadImageTask downloadImageTask = getDownloadImageTask(imageView);
if (downloadImageTask != null) {
long imageId = downloadImageTask.id;
if ((imageId == 0) || (imageId != id)) {
Boolean b = downloadImageTask.cancel(true);
Log.v("Task", "Stopped " + b.toString());
} else {
// The same URL is already being downloaded.
return false;
}
}
return true;
}
private static DownloadImageTask getDownloadImageTask(ImageView imageView) {
if (imageView != null) {
Drawable drawable = imageView.getDrawable();
if (drawable instanceof DownloadedDrawable) {
DownloadedDrawable downloadedDrawable = (DownloadedDrawable)drawable;
return downloadedDrawable.getDownloadImageTask();
}
}
return null;
}
// -----------------------------------------------------------------------
//
// Private helper methods
//
// -----------------------------------------------------------------------
private Bitmap downloadImageThumbnail(long id) {
Bitmap bitmap = MediaStore.Images.Thumbnails.getThumbnail(mContext.getContentResolver(), id, MediaStore.Images.Thumbnails.MICRO_KIND, null);
return bitmap;
}
}
Now gridview populated faster but there is still an issue with fast scrolling down. And another one - sometimes I get an error:
java.lang.IllegalStateException: Cannot execute task: the task has already been executed (a task can be executed only once)
I really don't know what to do. I've tried to make addTask and executeNext synchronized (I'm not good at questions about synchronization) but it works even worth. I don't know any third party library which helps to download image thumbnail by it's id from mediastore. Maybe you can help me?