Loading Imageviews inside Custom Listview with LRU cache
ListView is a kind of widget that will drive crazy if you not carefully handling it. For custom adapter to any listview you will inflate views only through getView() method. This is where many developers fail to handle the situation. In one of my project I was trying to show image views inside listview items. Everything is fine when I wrote the simple code shown below, but only thing I was facing is that scrolling effect of listview is not smooth. I hope this will make angry to any user who uses my app.
public class myListAdapter extends ArrayAdapter<MyListView> {
@SuppressLint("NewApi")
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder vHolder;
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.my_list_view_item, null);
vHolder = new ViewHolder();
vHolder.dName = (TextView) convertView.findViewById(R.id.docuName);
vHolder.dateNTime = (TextView) convertView.findViewById(R.id.docuDateNTime);
vHolder.iView = (ImageView) convertView.findViewById(R.id.docuPic);
convertView.setTag(vHolder);
} else
vHolder = (ViewHolder) convertView.getTag();
// My Listview
MyListView mListView = (MyListView) listOfItem.get(position);
vHolder.dName.setText(mListView.getItemName());
vHolder.dateNTime.setText(mListView.getitemCreatedAt());
if (getBitmapImage(mListView.getItemPicPath()) != null) {
vHolder.iView.setImageBitmap(getBitmapImage(mListView.getItemPicPath()));
} else {
vHolder.iView.setImageDrawable(imageView.getContext().getResources().getDrawable(R.drawable.list_placeholder));
}
return convertView;
}
// getting bitmap image
private Bitmap getBitmapImage(String itemPicPath) {
BitmapFactory.Options options = new BitmapFactory.Options();
// downsizing image as it throws OutOfMemory Exception for larger
// images
options.inSampleSize = 16;
return BitmapFactory.decodeFile(Uri.parse(itemPicPath).getPath(), options);
}
}
After doing some research I found out using AsyncTask will do the job, Hence I modified some parts of my code as below,
public class myListAdapter extends ArrayAdapter<MyListView> {
@SuppressLint("NewApi")
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder vHolder;
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.my_list_view_item, null);
vHolder = new ViewHolder();
vHolder.dName = (TextView) convertView.findViewById(R.id.docuName);
vHolder.dateNTime = (TextView) convertView.findViewById(R.id.docuDateNTime);
vHolder.iView = (ImageView) convertView.findViewById(R.id.docuPic);
convertView.setTag(vHolder);
} else
vHolder = (ViewHolder) convertView.getTag();
// My Listview
MyListView mListView = (MyListView) listOfItem.get(position);
vHolder.dName.setText(mListView.getItemName());
vHolder.dateNTime.setText(mListView.getitemCreatedAt());
if (vHolder.iView != null)
new imageDownloaderTask(vHolder.iView).execute(mListView.getItemPicPath());
return convertView;
}
private class imageDownloaderTask extends AsyncTask<String, Void, Bitmap> {
WeakReference<ImageView> iViewRef;
public imageDownloaderTask(ImageView iView) {
iViewRef = new WeakReference<ImageView>(iView);
}
@Override
protected Bitmap doInBackground(String... params) {
return getBitmapImage(params[0]);
}
@SuppressLint("NewApi")
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
if (isCancelled()) {
bitmap = null;
}
if (iViewRef != null) {
ImageView imageView = iViewRef.get();
if (imageView != null) {
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
imageView.setRotation(90);
} else {
imageView.setImageDrawable(imageView.getContext().getResources().getDrawable(R.drawable.list_placeholder));
imageView.setRotation(90);
}
}
}
}
// getting bitmap image
private Bitmap getBitmapImage(String itemPicPath) {
BitmapFactory.Options options = new BitmapFactory.Options();
// downsizing image as it throws OutOfMemory Exception for larger
// images
options.inSampleSize = 16;
return BitmapFactory.decodeFile(Uri.parse(itemPicPath).getPath(), options);
}
}
// ViewHolder
static class ViewHolder {
TextView dName;
TextView dateNTime;
ImageView iView;
}
}
This made my listview scroll smoothly. But again one thing I noted that every imageview regains it own view again and again while scrolling. This is not desired.
Then again I done some research and found out LRU cache is the best solution Hence I modified my code accordingly
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder vHolder;
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.my_list_view_item, null);
vHolder = new ViewHolder();
vHolder.dName = (TextView) convertView.findViewById(R.id.docuName);
vHolder.dateNTime = (TextView) convertView.findViewById(R.id.docuDateNTime);
vHolder.iView = (ImageView) convertView.findViewById(R.id.docuPic);
convertView.setTag(vHolder);
} else
vHolder = (ViewHolder) convertView.getTag();
// My Listview
MyListView mListView = (MyListView) listOfItem.get(position);
vHolder.dName.setText(mListView.getItemName());
vHolder.dateNTime.setText(mListView.getitemCreatedAt());
if (vHolder.iView != null)
Image.loadToView(mListView.getItemPicPath(), vHolder.iView);
return convertView;
}
private static class Image {
private static LruCache<String, Bitmap> mMemoryCache = null;
private static int cacheSize = 1024 * 1024 * 10;
private static class imageDownloaderTask extends AsyncTask<String, Void, Bitmap> {
private ImageView mTarget;
public imageDownloaderTask(ImageView target) {
this.mTarget = target;
}
@Override
protected void onPreExecute() {
mTarget.setTag(this);
}
@SuppressLint("NewApi")
@Override
protected Bitmap doInBackground(String... urls) {
String url = urls[0];
Bitmap result = null;
if (url != null) {
result = load(url);
if (result != null) {
mMemoryCache.put(url, result);
}
}
return result;
}
@Override
protected void onPostExecute(Bitmap result) {
if (mTarget.getTag() == this) {
mTarget.setTag(null);
if (result != null)
mTarget.setImageBitmap(result);
} else if (mTarget.getTag() != null) {
((imageDownloaderTask) mTarget.getTag()).cancel(true);
mTarget.setTag(null);
}
}
}
public static Bitmap load(String urlString) {
BitmapFactory.Options options = new BitmapFactory.Options();
// downsizing image as it throws OutOfMemory Exception for larger
// images
options.inSampleSize = 16;
Bitmap bitmap = BitmapFactory.decodeFile(Uri.parse(urlString).getPath(), options);
if (bitmap == null)
bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.list_placeholder);
return bitmap;
}
@SuppressLint("NewApi")
public static void loadToView(String url, ImageView view) {
if (url == null || url.length() == 0)
return;
if (mMemoryCache == null) {
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return (bitmap.getRowBytes() * bitmap.getHeight());
}
};
}
Bitmap bitmap = getBitmapFromMemCache(url);
if (bitmap == null) {
final imageDownloaderTask task = (imageDownloaderTask) new imageDownloaderTask(view);
view.setTag(task);
view.setRotation(90);
task.execute(url);
} else {
view.setImageBitmap(bitmap);
view.setRotation(90);
}
}
// getting Bitmap from memory cache
@SuppressLint("NewApi")
public static Bitmap getBitmapFromMemCache(String url) {
return (Bitmap) mMemoryCache.get(url);
}
}
// ViewHolder
class ViewHolder {
TextView dName;
TextView dateNTime;
ImageView iView;
}
Finally I my listview scrolls smoothly and imageview is loaded only once, and reuses the image cache. Hope code is quiet readable …
Enjoy …!