diff --git a/samples/training/bitmapfun/res/layout/image_detail_fragment.xml b/samples/training/bitmapfun/res/layout/image_detail_fragment.xml index ff616dacc..6940357af 100644 --- a/samples/training/bitmapfun/res/layout/image_detail_fragment.xml +++ b/samples/training/bitmapfun/res/layout/image_detail_fragment.xml @@ -25,7 +25,7 @@ android:layout_height="wrap_content" android:layout_gravity="center" /> - mMemoryCache; + private LruCache mMemoryCache; private ImageCacheParams mCacheParams; private final Object mDiskCacheLock = new Object(); private boolean mDiskCacheStarting = true; @@ -126,14 +126,28 @@ public class ImageCache { if (BuildConfig.DEBUG) { Log.d(TAG, "Memory cache created (size = " + mCacheParams.memCacheSize + ")"); } - mMemoryCache = new LruCache(mCacheParams.memCacheSize) { + mMemoryCache = new LruCache(mCacheParams.memCacheSize) { + + /** + * Notify the removed entry that is no longer being cached + */ + @Override + protected void entryRemoved(boolean evicted, String key, + BitmapDrawable oldValue, BitmapDrawable newValue) { + if (RecyclingBitmapDrawable.class.isInstance(oldValue)) { + // The removed entry is a recycling drawable, so notify it + // that it has been removed from the memory cache + ((RecyclingBitmapDrawable) oldValue).setIsCached(false); + } + } + /** * Measure item size in kilobytes rather than units which is more practical * for a bitmap cache */ @Override - protected int sizeOf(String key, Bitmap bitmap) { - final int bitmapSize = getBitmapSize(bitmap) / 1024; + protected int sizeOf(String key, BitmapDrawable value) { + final int bitmapSize = getBitmapSize(value) / 1024; return bitmapSize == 0 ? 1 : bitmapSize; } }; @@ -184,16 +198,21 @@ public class ImageCache { /** * Adds a bitmap to both memory and disk cache. * @param data Unique identifier for the bitmap to store - * @param bitmap The bitmap to store + * @param value The bitmap drawable to store */ - public void addBitmapToCache(String data, Bitmap bitmap) { - if (data == null || bitmap == null) { + public void addBitmapToCache(String data, BitmapDrawable value) { + if (data == null || value == null) { return; } // Add to memory cache - if (mMemoryCache != null && mMemoryCache.get(data) == null) { - mMemoryCache.put(data, bitmap); + if (mMemoryCache != null) { + if (RecyclingBitmapDrawable.class.isInstance(value)) { + // The removed entry is a recycling drawable, so notify it + // that it has been added into the memory cache + ((RecyclingBitmapDrawable) value).setIsCached(true); + } + mMemoryCache.put(data, value); } synchronized (mDiskCacheLock) { @@ -207,7 +226,7 @@ public class ImageCache { final DiskLruCache.Editor editor = mDiskLruCache.edit(key); if (editor != null) { out = editor.newOutputStream(DISK_CACHE_INDEX); - bitmap.compress( + value.getBitmap().compress( mCacheParams.compressFormat, mCacheParams.compressQuality, out); editor.commit(); out.close(); @@ -234,19 +253,20 @@ public class ImageCache { * Get from memory cache. * * @param data Unique identifier for which item to get - * @return The bitmap if found in cache, null otherwise + * @return The bitmap drawable if found in cache, null otherwise */ - public Bitmap getBitmapFromMemCache(String data) { + public BitmapDrawable getBitmapFromMemCache(String data) { + BitmapDrawable memValue = null; + if (mMemoryCache != null) { - final Bitmap memBitmap = mMemoryCache.get(data); - if (memBitmap != null) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "Memory cache hit"); - } - return memBitmap; - } + memValue = mMemoryCache.get(data); } - return null; + + if (BuildConfig.DEBUG && memValue != null) { + Log.d(TAG, "Memory cache hit"); + } + + return memValue; } /** @@ -453,12 +473,14 @@ public class ImageCache { } /** - * Get the size in bytes of a bitmap. - * @param bitmap + * Get the size in bytes of a bitmap in a BitmapDrawable. + * @param value * @return size in bytes */ @TargetApi(12) - public static int getBitmapSize(Bitmap bitmap) { + public static int getBitmapSize(BitmapDrawable value) { + Bitmap bitmap = value.getBitmap(); + if (Utils.hasHoneycombMR1()) { return bitmap.getByteCount(); } 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 0f890e0d5..84a0f5981 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 @@ -76,15 +76,15 @@ public abstract class ImageWorker { return; } - Bitmap bitmap = null; + BitmapDrawable value = null; if (mImageCache != null) { - bitmap = mImageCache.getBitmapFromMemCache(String.valueOf(data)); + value = mImageCache.getBitmapFromMemCache(String.valueOf(data)); } - if (bitmap != null) { + if (value != null) { // Bitmap found in memory cache - imageView.setImageBitmap(bitmap); + imageView.setImageDrawable(value); } else if (cancelPotentialWork(data, imageView)) { final BitmapWorkerTask task = new BitmapWorkerTask(imageView); final AsyncDrawable asyncDrawable = @@ -222,7 +222,7 @@ public abstract class ImageWorker { /** * The actual AsyncTask that will asynchronously process the image. */ - private class BitmapWorkerTask extends AsyncTask { + private class BitmapWorkerTask extends AsyncTask { private Object data; private final WeakReference imageViewReference; @@ -234,7 +234,7 @@ public abstract class ImageWorker { * Background processing. */ @Override - protected Bitmap doInBackground(Object... params) { + protected BitmapDrawable doInBackground(Object... params) { if (BuildConfig.DEBUG) { Log.d(TAG, "doInBackground - starting work"); } @@ -242,6 +242,7 @@ public abstract class ImageWorker { data = params[0]; final String dataString = String.valueOf(data); Bitmap bitmap = null; + BitmapDrawable drawable = null; // Wait here if work is paused and the task is not cancelled synchronized (mPauseWorkLock) { @@ -274,39 +275,50 @@ public abstract class ImageWorker { // bitmap to the cache for future use. Note we don't check if the task was cancelled // here, if it was, and the thread is still running, we may as well add the processed // bitmap to our cache as it might be used again in the future - if (bitmap != null && mImageCache != null) { - mImageCache.addBitmapToCache(dataString, bitmap); + if (bitmap != null) { + if (Utils.hasHoneycomb()) { + // Running on Honeycomb or newer, so wrap in a standard BitmapDrawable + drawable = new BitmapDrawable(mResources, bitmap); + } else { + // Running on Gingerbread or older, so wrap in a RecyclingBitmapDrawable + // which will recycle automagically + drawable = new RecyclingBitmapDrawable(mResources, bitmap); + } + + if (mImageCache != null) { + mImageCache.addBitmapToCache(dataString, drawable); + } } if (BuildConfig.DEBUG) { Log.d(TAG, "doInBackground - finished work"); } - return bitmap; + return drawable; } /** * Once the image is processed, associates it to the imageView */ @Override - protected void onPostExecute(Bitmap bitmap) { + protected void onPostExecute(BitmapDrawable value) { // if cancel was called on this task or the "exit early" flag is set then we're done if (isCancelled() || mExitTasksEarly) { - bitmap = null; + value = null; } final ImageView imageView = getAttachedImageView(); - if (bitmap != null && imageView != null) { + if (value != null && imageView != null) { if (BuildConfig.DEBUG) { Log.d(TAG, "onPostExecute - setting bitmap"); } - setImageBitmap(imageView, bitmap); + setImageDrawable(imageView, value); } } @Override - protected void onCancelled(Bitmap bitmap) { - super.onCancelled(bitmap); + protected void onCancelled(BitmapDrawable value) { + super.onCancelled(value); synchronized (mPauseWorkLock) { mPauseWorkLock.notifyAll(); } @@ -349,18 +361,19 @@ public abstract class ImageWorker { } /** - * Called when the processing is complete and the final bitmap should be set on the ImageView. + * Called when the processing is complete and the final drawable should be + * set on the ImageView. * * @param imageView - * @param bitmap + * @param drawable */ - private void setImageBitmap(ImageView imageView, Bitmap bitmap) { + private void setImageDrawable(ImageView imageView, Drawable drawable) { if (mFadeInBitmap) { - // Transition drawable with a transparent drwabale and the final bitmap + // Transition drawable with a transparent drawable and the final drawable final TransitionDrawable td = new TransitionDrawable(new Drawable[] { new ColorDrawable(android.R.color.transparent), - new BitmapDrawable(mResources, bitmap) + drawable }); // Set background to loading bitmap imageView.setBackgroundDrawable( @@ -369,7 +382,7 @@ public abstract class ImageWorker { imageView.setImageDrawable(td); td.startTransition(FADE_IN_TIME); } else { - imageView.setImageBitmap(bitmap); + imageView.setImageDrawable(drawable); } } diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/RecyclingBitmapDrawable.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/RecyclingBitmapDrawable.java new file mode 100644 index 000000000..2aae97f3c --- /dev/null +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/RecyclingBitmapDrawable.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.bitmapfun.util; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.util.Log; + +import com.example.android.bitmapfun.BuildConfig; + +/** + * A BitmapDrawable that keeps track of whether it is being displayed or cached. + * When the drawable is no longer being displayed or cached, + * {@link Bitmap#recycle() recycle()} will be called on this drawable's bitmap. + */ +public class RecyclingBitmapDrawable extends BitmapDrawable { + + static final String LOG_TAG = "CountingBitmapDrawable"; + + private int mCacheRefCount = 0; + private int mDisplayRefCount = 0; + + private boolean mHasBeenDisplayed; + + public RecyclingBitmapDrawable(Resources res, Bitmap bitmap) { + super(res, bitmap); + } + + /** + * Notify the drawable that the displayed state has changed. Internally a + * count is kept so that the drawable knows when it is no longer being + * displayed. + * + * @param isDisplayed - Whether the drawable is being displayed or not + */ + public void setIsDisplayed(boolean isDisplayed) { + synchronized (this) { + if (isDisplayed) { + mDisplayRefCount++; + mHasBeenDisplayed = true; + } else { + mDisplayRefCount--; + } + } + + // Check to see if recycle() can be called + checkState(); + } + + /** + * Notify the drawable that the cache state has changed. Internally a count + * is kept so that the drawable knows when it is no longer being cached. + * + * @param isCached - Whether the drawable is being cached or not + */ + public void setIsCached(boolean isCached) { + synchronized (this) { + if (isCached) { + mCacheRefCount++; + } else { + mCacheRefCount--; + } + } + + // Check to see if recycle() can be called + checkState(); + } + + private synchronized void checkState() { + // If the drawable cache and display ref counts = 0, and this drawable + // has been displayed, then recycle + if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed + && hasValidBitmap()) { + if (BuildConfig.DEBUG) { + Log.d(LOG_TAG, "No longer being used or cached so recycling. " + + toString()); + } + + getBitmap().recycle(); + } + } + + private synchronized boolean hasValidBitmap() { + Bitmap bitmap = getBitmap(); + return bitmap != null && !bitmap.isRecycled(); + } + +}