Commit 2a84299d authored by Chris Banes's avatar Chris Banes Committed by Android Git Automerger
Browse files

am f4535aee: Merge "Update bitmapfun sample to call recycle()" into jb-mr1-dev

# Via Android (Google) Code Review (1) and Chris Banes (1)
* commit 'f4535aee':
  Update bitmapfun sample to call recycle()
parents 64f03f37 f4535aee
......@@ -25,7 +25,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center" />
<ImageView
<com.example.android.bitmapfun.ui.RecyclingImageView
android:id="@+id/imageView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
......
......@@ -18,7 +18,6 @@ package com.example.android.bitmapfun.ui;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
......
......@@ -270,7 +270,7 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
// Now handle the main ImageView thumbnails
ImageView imageView;
if (convertView == null) { // if it's not recycled, instantiate and initialize
imageView = new ImageView(mContext);
imageView = new RecyclingImageView(mContext);
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
imageView.setLayoutParams(mImageViewLayoutParams);
} else { // Otherwise re-use the converted view
......
/*
* 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.ui;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.util.AttributeSet;
import android.widget.ImageView;
import com.example.android.bitmapfun.util.RecyclingBitmapDrawable;
/**
* Sub-class of ImageView which automatically notifies the drawable when it is
* being displayed.
*/
public class RecyclingImageView extends ImageView {
public RecyclingImageView(Context context) {
super(context);
}
public RecyclingImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* @see android.widget.ImageView#onDetachedFromWindow()
*/
@Override
protected void onDetachedFromWindow() {
// This has been detached from Window, so clear the drawable
setImageDrawable(null);
super.onDetachedFromWindow();
}
/**
* @see android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)
*/
@Override
public void setImageDrawable(Drawable drawable) {
// Keep hold of previous Drawable
final Drawable previousDrawable = getDrawable();
// Call super to set new Drawable
super.setImageDrawable(drawable);
// Notify new Drawable that it is being displayed
notifyDrawable(drawable, true);
// Notify old Drawable so it is no longer being displayed
notifyDrawable(previousDrawable, false);
}
/**
* Notifies the drawable that it's displayed state has changed.
*
* @param drawable
* @param isDisplayed
*/
private static void notifyDrawable(Drawable drawable, final boolean isDisplayed) {
if (drawable instanceof RecyclingBitmapDrawable) {
// The drawable is a CountingBitmapDrawable, so notify it
((RecyclingBitmapDrawable) drawable).setIsDisplayed(isDisplayed);
} else if (drawable instanceof LayerDrawable) {
// The drawable is a LayerDrawable, so recurse on each layer
LayerDrawable layerDrawable = (LayerDrawable) drawable;
for (int i = 0, z = layerDrawable.getNumberOfLayers(); i < z; i++) {
notifyDrawable(layerDrawable.getDrawable(i), isDisplayed);
}
}
}
}
......@@ -17,10 +17,10 @@
package com.example.android.bitmapfun.util;
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Environment;
......@@ -63,7 +63,7 @@ public class ImageCache {
private static final boolean DEFAULT_INIT_DISK_CACHE_ON_CREATE = false;
private DiskLruCache mDiskLruCache;
private LruCache<String, Bitmap> mMemoryCache;
private LruCache<String, BitmapDrawable> 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<String, Bitmap>(mCacheParams.memCacheSize) {
mMemoryCache = new LruCache<String, BitmapDrawable>(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();
}
......
......@@ -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<Object, Void, Bitmap> {
private class BitmapWorkerTask extends AsyncTask<Object, Void, BitmapDrawable> {
private Object data;
private final WeakReference<ImageView> 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);
}
}
......
/*
* 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();
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment