diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageCache.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageCache.java index f567e76a3..d65d65568 100644 --- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageCache.java +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageCache.java @@ -20,8 +20,8 @@ import android.annotation.TargetApi; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; -import android.graphics.drawable.BitmapDrawable; import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; import android.os.Bundle; import android.os.Environment; import android.os.StatFs; @@ -33,11 +33,16 @@ import android.util.Log; import com.example.android.bitmapfun.BuildConfig; import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.lang.ref.SoftReference; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.HashSet; +import java.util.Iterator; /** * This class holds our bitmap caches (memory and disk). @@ -68,6 +73,8 @@ public class ImageCache { private final Object mDiskCacheLock = new Object(); private boolean mDiskCacheStarting = true; + private HashSet> mReusableBitmaps; + /** * Creating a new ImageCache object using the specified parameters. * @@ -126,6 +133,12 @@ public class ImageCache { if (BuildConfig.DEBUG) { Log.d(TAG, "Memory cache created (size = " + mCacheParams.memCacheSize + ")"); } + + // If we're running on Honeycomb or newer, then + if (Utils.hasHoneycomb()) { + mReusableBitmaps = new HashSet>(); + } + mMemoryCache = new LruCache(mCacheParams.memCacheSize) { /** @@ -138,6 +151,14 @@ public class ImageCache { // The removed entry is a recycling drawable, so notify it // that it has been removed from the memory cache ((RecyclingBitmapDrawable) oldValue).setIsCached(false); + } else { + // The removed entry is a standard BitmapDrawable + + if (Utils.hasHoneycomb()) { + // We're running on Honeycomb or later, so add the bitmap + // to a SoftRefrence set for possible use with inBitmap later + mReusableBitmaps.add(new SoftReference(oldValue.getBitmap())); + } } } @@ -277,6 +298,8 @@ public class ImageCache { */ public Bitmap getBitmapFromDiskCache(String data) { final String key = hashKeyForDisk(data); + Bitmap bitmap = null; + synchronized (mDiskCacheLock) { while (mDiskCacheStarting) { try { @@ -293,8 +316,12 @@ public class ImageCache { } inputStream = snapshot.getInputStream(DISK_CACHE_INDEX); if (inputStream != null) { - final Bitmap bitmap = BitmapFactory.decodeStream(inputStream); - return bitmap; + FileDescriptor fd = ((FileInputStream) inputStream).getFD(); + + // Decode bitmap, but we don't want to sample so give + // MAX_VALUE as the target dimensions + bitmap = ImageResizer.decodeSampledBitmapFromDescriptor( + fd, Integer.MAX_VALUE, Integer.MAX_VALUE, this); } } } catch (final IOException e) { @@ -307,10 +334,43 @@ public class ImageCache { } catch (IOException e) {} } } - return null; + return bitmap; } } + /** + * @param options - BitmapFactory.Options with out* options populated + * @return Bitmap that case be used for inBitmap + */ + protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) { + Bitmap bitmap = null; + + if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) { + final Iterator> iterator = mReusableBitmaps.iterator(); + Bitmap item; + + while (iterator.hasNext()) { + item = iterator.next().get(); + + if (null != item && item.isMutable()) { + // Check to see it the item can be used for inBitmap + if (canUseForInBitmap(item, options)) { + bitmap = item; + + // Remove from reusable set so it can't be used again + iterator.remove(); + break; + } + } else { + // Remove from the set if the reference has been cleared. + iterator.remove(); + } + } + } + + return bitmap; + } + /** * Clears both the memory and disk cache associated with this ImageCache object. Note that * this includes disk access so this should not be executed on the main/UI thread. @@ -425,6 +485,20 @@ public class ImageCache { } } + /** + * @param candidate - Bitmap to check + * @param targetOptions - Options that have the out* value populated + * @return true if candidate can be used for inBitmap re-use with + * targetOptions + */ + private static boolean canUseForInBitmap( + Bitmap candidate, BitmapFactory.Options targetOptions) { + int width = targetOptions.outWidth / targetOptions.inSampleSize; + int height = targetOptions.outHeight / targetOptions.inSampleSize; + + return candidate.getWidth() == width && candidate.getHeight() == height; + } + /** * Get a usable cache directory (external if available, internal otherwise). * diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageFetcher.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageFetcher.java index 708484530..4c92d7417 100644 --- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageFetcher.java +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageFetcher.java @@ -241,7 +241,8 @@ public class ImageFetcher extends ImageResizer { Bitmap bitmap = null; if (fileDescriptor != null) { - bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, mImageWidth, mImageHeight); + bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, mImageWidth, + mImageHeight, getImageCache()); } if (fileInputStream != null) { try { diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageResizer.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageResizer.java index 6a129c318..2a9d152d1 100644 --- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageResizer.java +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageResizer.java @@ -16,10 +16,12 @@ package com.example.android.bitmapfun.util; +import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.os.Build; import android.util.Log; import com.example.android.bitmapfun.BuildConfig; @@ -90,7 +92,8 @@ public class ImageResizer extends ImageWorker { if (BuildConfig.DEBUG) { Log.d(TAG, "processBitmap - " + resId); } - return decodeSampledBitmapFromResource(mResources, resId, mImageWidth, mImageHeight); + return decodeSampledBitmapFromResource(mResources, resId, mImageWidth, + mImageHeight, getImageCache()); } @Override @@ -105,11 +108,12 @@ public class ImageResizer extends ImageWorker { * @param resId The resource id of the image data * @param reqWidth The requested width of the resulting bitmap * @param reqHeight The requested height of the resulting bitmap + * @param cache The ImageCache used to find candidate bitmaps for use with inBitmap * @return A bitmap sampled down from the original with the same aspect ratio and dimensions * that are equal to or greater than the requested width and height */ public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, - int reqWidth, int reqHeight) { + int reqWidth, int reqHeight, ImageCache cache) { // First decode with inJustDecodeBounds=true to check dimensions final BitmapFactory.Options options = new BitmapFactory.Options(); @@ -119,6 +123,11 @@ public class ImageResizer extends ImageWorker { // Calculate inSampleSize options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + // If we're running on Honeycomb or newer, try to use inBitmap + if (Utils.hasHoneycomb()) { + addInBitmapOptions(options, cache); + } + // Decode bitmap with inSampleSize set options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); @@ -130,11 +139,12 @@ public class ImageResizer extends ImageWorker { * @param filename The full path of the file to decode * @param reqWidth The requested width of the resulting bitmap * @param reqHeight The requested height of the resulting bitmap + * @param cache The ImageCache used to find candidate bitmaps for use with inBitmap * @return A bitmap sampled down from the original with the same aspect ratio and dimensions * that are equal to or greater than the requested width and height */ public static Bitmap decodeSampledBitmapFromFile(String filename, - int reqWidth, int reqHeight) { + int reqWidth, int reqHeight, ImageCache cache) { // First decode with inJustDecodeBounds=true to check dimensions final BitmapFactory.Options options = new BitmapFactory.Options(); @@ -144,6 +154,11 @@ public class ImageResizer extends ImageWorker { // Calculate inSampleSize options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + // If we're running on Honeycomb or newer, try to use inBitmap + if (Utils.hasHoneycomb()) { + addInBitmapOptions(options, cache); + } + // Decode bitmap with inSampleSize set options.inJustDecodeBounds = false; return BitmapFactory.decodeFile(filename, options); @@ -155,11 +170,12 @@ public class ImageResizer extends ImageWorker { * @param fileDescriptor The file descriptor to read from * @param reqWidth The requested width of the resulting bitmap * @param reqHeight The requested height of the resulting bitmap + * @param cache The ImageCache used to find candidate bitmaps for use with inBitmap * @return A bitmap sampled down from the original with the same aspect ratio and dimensions * that are equal to or greater than the requested width and height */ public static Bitmap decodeSampledBitmapFromDescriptor( - FileDescriptor fileDescriptor, int reqWidth, int reqHeight) { + FileDescriptor fileDescriptor, int reqWidth, int reqHeight, ImageCache cache) { // First decode with inJustDecodeBounds=true to check dimensions final BitmapFactory.Options options = new BitmapFactory.Options(); @@ -171,9 +187,34 @@ public class ImageResizer extends ImageWorker { // Decode bitmap with inSampleSize set options.inJustDecodeBounds = false; + + // If we're running on Honeycomb or newer, try to use inBitmap + if (Utils.hasHoneycomb()) { + addInBitmapOptions(options, cache); + } + return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); } + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) { + // inBitmap only works with mutable bitmaps so force the decoder to + // return mutable bitmaps. + options.inMutable = true; + + if (cache != null) { + // Try and find a bitmap to use for inBitmap + Bitmap inBitmap = cache.getBitmapFromReusableSet(options); + + if (inBitmap != null) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "Found bitmap to use for inBitmap"); + } + options.inBitmap = inBitmap; + } + } + } + /** * Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding * bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageWorker.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageWorker.java index 84a0f5981..87b2cb232 100644 --- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageWorker.java +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageWorker.java @@ -164,6 +164,13 @@ public abstract class ImageWorker { */ protected abstract Bitmap processBitmap(Object data); + /** + * @return The {@link ImageCache} object currently being used by this ImageWorker. + */ + protected ImageCache getImageCache() { + return mImageCache; + } + /** * Cancels any pending work attached to the provided ImageView. * @param imageView