Saturday 14 July 2012

Caching Bitmaps with LruCache

In the last exercise of "Implement Android Gallery widget with scale-down bitmap", the bitmap will be loaded and scaled every-time getView() of GalleryBaseAdapter called. The side-effect is the gallery widget stop a moment whenever a new a new item come-out.

The LruCache class (also available in the Support Library for use back to API Level 4) is particularly well suited to the task of caching bitmaps, keeping recently referenced objects in a strong referenced LinkedHashMap and evicting the least recently used member before the cache exceeds its designated size. ~ reference: http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html#memory-cache.

In this exercise, two Gallery widget were implemented. In the video shown below, the upper one is original gallery, the lower one is gallery with cached bitmaps. It can be noted that the cached gallery need more time to be loaded in first loading, but swipe much more smooth.



Main activity.
package com.example.androidgallery;

import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;

import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.LruCache;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Gallery;
import android.widget.ImageView;
import android.widget.LinearLayout;

public class MainActivity extends Activity {

public class GalleryBaseAdapter extends BaseAdapter {

ArrayList<String> GalleryFileList;
Context context;

GalleryBaseAdapter(Context cont){
context = cont;
GalleryFileList = new ArrayList<String>();
}

@Override
public int getCount() {
return GalleryFileList.size();
}

@Override
public Object getItem(int position) {
return GalleryFileList.get(position);
}

@Override
public long getItemId(int position) {
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {

Bitmap bm = decodeSampledBitmapFromUri(GalleryFileList.get(position), 200, 200);
LinearLayout layout = new LinearLayout(context);
layout.setLayoutParams(new Gallery.LayoutParams(250, 250));
layout.setGravity(Gravity.CENTER);

ImageView imageView = new ImageView(context);
imageView.setLayoutParams(new Gallery.LayoutParams(200, 200));
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
imageView.setImageBitmap(bm);

layout.addView(imageView);
return layout;
}

public void add(String newitem){
GalleryFileList.add(newitem);
}

}

public class CacheGalleryBaseAdapter extends GalleryBaseAdapter{

CacheGalleryBaseAdapter(Context cont) {
super(cont);
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {

ImageView imageView = new ImageView(context);

//---
// Use the path as the key to LruCache
final String imageKey = GalleryFileList.get(position);

final Bitmap bm = getBitmapFromMemCache(imageKey);
if (bm == null){
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(imageKey);
}

LinearLayout layout = new LinearLayout(context);
layout.setLayoutParams(new Gallery.LayoutParams(250, 250));
layout.setGravity(Gravity.CENTER);

imageView.setLayoutParams(new Gallery.LayoutParams(200, 200));
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
imageView.setImageBitmap(bm);

layout.addView(imageView);
return layout;

}

}

GalleryBaseAdapter myGalleryBaseAdapter;
CacheGalleryBaseAdapter myCacheGalleryBaseAdapter;
Gallery myPhotoGallery, myFastGallery;

private LruCache<String, Bitmap> mMemoryCache;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myPhotoGallery = (Gallery)findViewById(R.id.photogallery);
myFastGallery = (Gallery)findViewById(R.id.fastgallery);

myGalleryBaseAdapter = new GalleryBaseAdapter(this);
myCacheGalleryBaseAdapter = new CacheGalleryBaseAdapter(this);

String ExternalStorageDirectoryPath = Environment
.getExternalStorageDirectory()
.getAbsolutePath();

String targetPath = ExternalStorageDirectoryPath + "/test/";

File targetDirector = new File(targetPath);

File[] files = targetDirector.listFiles();
for (File file : files){
myGalleryBaseAdapter.add(file.getPath());
myCacheGalleryBaseAdapter.add(file.getPath());
}

myPhotoGallery.setAdapter(myGalleryBaseAdapter);
myFastGallery.setAdapter(myCacheGalleryBaseAdapter);

// Get memory class of this device, exceeding this amount will throw an
// OutOfMemory exception.
final int memClass = ((ActivityManager)getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();

// Use 1/8th of the available memory for this memory cache.
final int cacheSize = 1024 * 1024 * memClass / 8;

mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {

protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in bytes rather than number of items.
return bitmap.getByteCount();
}

};
}

class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap>{

private final WeakReference<ImageView> imageViewReference;

public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<ImageView>(imageView);
}

@Override
protected Bitmap doInBackground(String... params) {
final Bitmap bitmap = decodeSampledBitmapFromUri(params[0], 200, 200);
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}

@Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = (ImageView)imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}

}

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}

public Bitmap getBitmapFromMemCache(String key) {
return (Bitmap) mMemoryCache.get(key);
}

public Bitmap decodeSampledBitmapFromUri(String path, int reqWidth, int reqHeight) {
Bitmap bm = null;

// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);

// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
bm = BitmapFactory.decodeFile(path, options);

return bm;
}

public int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;

if (height > reqHeight || width > reqWidth) {
if (width > height) {
inSampleSize = Math.round((float)height / (float)reqHeight);
} else {
inSampleSize = Math.round((float)width / (float)reqWidth);
}
}

return inSampleSize;
}

}


Modify the layout to have two gallery.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Gallery without Cache" />
<Gallery
android:id="@+id/photogallery"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />

<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Gallery with Cache" />
<Gallery
android:id="@+id/fastgallery"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />

</LinearLayout>


android.util.LruCache is needed in the code, modify AndroidManifest.xml to have minSdkVersion and targetSdkVersion of "13".

Download the files.


Please note that this approach have it's own trade-off: if you request cacheSize too much, it will make your app causing java.lang.OutOfMemory exceptions, if too small, it will cause additional overhead. In my own openion, if you have predictable images (or resources) needed for caching, it's a good choice.

Read more: LruCache with different size

Related:
- Apply LruCache on GridView


No comments:

Post a Comment