| /* |
| * 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.contactslist.util; |
| |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.drawable.BitmapDrawable; |
| import android.graphics.drawable.ColorDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.graphics.drawable.TransitionDrawable; |
| import android.os.AsyncTask; |
| import android.support.v4.app.FragmentManager; |
| import android.util.Log; |
| import android.widget.ImageView; |
| |
| import com.example.android.contactslist.BuildConfig; |
| |
| import java.io.FileDescriptor; |
| import java.lang.ref.WeakReference; |
| |
| /** |
| * This class wraps up completing some arbitrary long running work when loading a bitmap to an |
| * ImageView. It handles things like using a memory and disk cache, running the work in a background |
| * thread and setting a placeholder image. |
| */ |
| public abstract class ImageLoader { |
| private static final String TAG = "ImageLoader"; |
| private static final int FADE_IN_TIME = 200; |
| |
| private ImageCache mImageCache; |
| private Bitmap mLoadingBitmap; |
| private boolean mFadeInBitmap = true; |
| private boolean mPauseWork = false; |
| private final Object mPauseWorkLock = new Object(); |
| private int mImageSize; |
| private Resources mResources; |
| |
| protected ImageLoader(Context context, int imageSize) { |
| mResources = context.getResources(); |
| mImageSize = imageSize; |
| } |
| |
| public int getImageSize() { |
| return mImageSize; |
| } |
| |
| /** |
| * Load an image specified by the data parameter into an ImageView (override |
| * {@link ImageLoader#processBitmap(Object)} to define the processing logic). If the image is |
| * found in the memory cache, it is set immediately, otherwise an {@link AsyncTask} will be |
| * created to asynchronously load the bitmap. |
| * |
| * @param data The URL of the image to download. |
| * @param imageView The ImageView to bind the downloaded image to. |
| */ |
| public void loadImage(Object data, ImageView imageView) { |
| if (data == null) { |
| imageView.setImageBitmap(mLoadingBitmap); |
| return; |
| } |
| |
| Bitmap bitmap = null; |
| |
| if (mImageCache != null) { |
| bitmap = mImageCache.getBitmapFromMemCache(String.valueOf(data)); |
| } |
| |
| if (bitmap != null) { |
| // Bitmap found in memory cache |
| imageView.setImageBitmap(bitmap); |
| } else if (cancelPotentialWork(data, imageView)) { |
| final BitmapWorkerTask task = new BitmapWorkerTask(imageView); |
| final AsyncDrawable asyncDrawable = |
| new AsyncDrawable(mResources, mLoadingBitmap, task); |
| imageView.setImageDrawable(asyncDrawable); |
| task.execute(data); |
| } |
| } |
| |
| /** |
| * Set placeholder bitmap that shows when the the background thread is running. |
| * |
| * @param resId Resource ID of loading image. |
| */ |
| public void setLoadingImage(int resId) { |
| mLoadingBitmap = BitmapFactory.decodeResource(mResources, resId); |
| } |
| |
| /** |
| * Adds an {@link ImageCache} to this image loader. |
| * |
| * @param fragmentManager A FragmentManager to use to retain the cache over configuration |
| * changes such as an orientation change. |
| * @param memCacheSizePercent The cache size as a percent of available app memory. |
| */ |
| public void addImageCache(FragmentManager fragmentManager, float memCacheSizePercent) { |
| mImageCache = ImageCache.getInstance(fragmentManager, memCacheSizePercent); |
| } |
| |
| /** |
| * If set to true, the image will fade-in once it has been loaded by the background thread. |
| */ |
| public void setImageFadeIn(boolean fadeIn) { |
| mFadeInBitmap = fadeIn; |
| } |
| |
| /** |
| * Subclasses should override this to define any processing or work that must happen to produce |
| * the final bitmap. This will be executed in a background thread and be long running. For |
| * example, you could resize a large bitmap here, or pull down an image from the network. |
| * |
| * @param data The data to identify which image to process, as provided by |
| * {@link ImageLoader#loadImage(Object, ImageView)} |
| * @return The processed bitmap |
| */ |
| protected abstract Bitmap processBitmap(Object data); |
| |
| /** |
| * Cancels any pending work attached to the provided ImageView. |
| */ |
| public static void cancelWork(ImageView imageView) { |
| final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); |
| if (bitmapWorkerTask != null) { |
| bitmapWorkerTask.cancel(true); |
| if (BuildConfig.DEBUG) { |
| final Object bitmapData = bitmapWorkerTask.data; |
| Log.d(TAG, "cancelWork - cancelled work for " + bitmapData); |
| } |
| } |
| } |
| |
| /** |
| * Returns true if the current work has been canceled or if there was no work in |
| * progress on this image view. |
| * Returns false if the work in progress deals with the same data. The work is not |
| * stopped in that case. |
| */ |
| public static boolean cancelPotentialWork(Object data, ImageView imageView) { |
| final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); |
| |
| if (bitmapWorkerTask != null) { |
| final Object bitmapData = bitmapWorkerTask.data; |
| if (bitmapData == null || !bitmapData.equals(data)) { |
| bitmapWorkerTask.cancel(true); |
| if (BuildConfig.DEBUG) { |
| Log.d(TAG, "cancelPotentialWork - cancelled work for " + data); |
| } |
| } else { |
| // The same work is already in progress. |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * @param imageView Any imageView |
| * @return Retrieve the currently active work task (if any) associated with this imageView. |
| * null if there is no such task. |
| */ |
| private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { |
| if (imageView != null) { |
| final Drawable drawable = imageView.getDrawable(); |
| if (drawable instanceof AsyncDrawable) { |
| final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; |
| return asyncDrawable.getBitmapWorkerTask(); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * The actual AsyncTask that will asynchronously process the image. |
| */ |
| private class BitmapWorkerTask extends AsyncTask<Object, Void, Bitmap> { |
| private Object data; |
| private final WeakReference<ImageView> imageViewReference; |
| |
| public BitmapWorkerTask(ImageView imageView) { |
| imageViewReference = new WeakReference<ImageView>(imageView); |
| } |
| |
| /** |
| * Background processing. |
| */ |
| @Override |
| protected Bitmap doInBackground(Object... params) { |
| if (BuildConfig.DEBUG) { |
| Log.d(TAG, "doInBackground - starting work"); |
| } |
| |
| data = params[0]; |
| final String dataString = String.valueOf(data); |
| Bitmap bitmap = null; |
| |
| // Wait here if work is paused and the task is not cancelled |
| synchronized (mPauseWorkLock) { |
| while (mPauseWork && !isCancelled()) { |
| try { |
| mPauseWorkLock.wait(); |
| } catch (InterruptedException e) {} |
| } |
| } |
| |
| // If the task has not been cancelled by another thread and the ImageView that was |
| // originally bound to this task is still bound back to this task and our "exit early" |
| // flag is not set, then call the main process method (as implemented by a subclass) |
| if (!isCancelled() && getAttachedImageView() != null) { |
| bitmap = processBitmap(params[0]); |
| } |
| |
| // If the bitmap was processed and the image cache is available, then add the processed |
| // 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 (BuildConfig.DEBUG) { |
| Log.d(TAG, "doInBackground - finished work"); |
| } |
| |
| return bitmap; |
| } |
| |
| /** |
| * Once the image is processed, associates it to the imageView |
| */ |
| @Override |
| protected void onPostExecute(Bitmap bitmap) { |
| // if cancel was called on this task or the "exit early" flag is set then we're done |
| if (isCancelled()) { |
| bitmap = null; |
| } |
| |
| final ImageView imageView = getAttachedImageView(); |
| if (bitmap != null && imageView != null) { |
| if (BuildConfig.DEBUG) { |
| Log.d(TAG, "onPostExecute - setting bitmap"); |
| } |
| setImageBitmap(imageView, bitmap); |
| } |
| } |
| |
| @Override |
| protected void onCancelled(Bitmap bitmap) { |
| super.onCancelled(bitmap); |
| synchronized (mPauseWorkLock) { |
| mPauseWorkLock.notifyAll(); |
| } |
| } |
| |
| /** |
| * Returns the ImageView associated with this task as long as the ImageView's task still |
| * points to this task as well. Returns null otherwise. |
| */ |
| private ImageView getAttachedImageView() { |
| final ImageView imageView = imageViewReference.get(); |
| final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); |
| |
| if (this == bitmapWorkerTask) { |
| return imageView; |
| } |
| |
| return null; |
| } |
| } |
| |
| /** |
| * A custom Drawable that will be attached to the imageView while the work is in progress. |
| * Contains a reference to the actual worker task, so that it can be stopped if a new binding is |
| * required, and makes sure that only the last started worker process can bind its result, |
| * independently of the finish order. |
| */ |
| private static class AsyncDrawable extends BitmapDrawable { |
| private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; |
| |
| public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { |
| super(res, bitmap); |
| bitmapWorkerTaskReference = |
| new WeakReference<BitmapWorkerTask>(bitmapWorkerTask); |
| } |
| |
| public BitmapWorkerTask getBitmapWorkerTask() { |
| return bitmapWorkerTaskReference.get(); |
| } |
| } |
| |
| /** |
| * Called when the processing is complete and the final bitmap should be set on the ImageView. |
| * |
| * @param imageView The ImageView to set the bitmap to. |
| * @param bitmap The new bitmap to set. |
| */ |
| private void setImageBitmap(ImageView imageView, Bitmap bitmap) { |
| if (mFadeInBitmap) { |
| // Transition drawable to fade from loading bitmap to final bitmap |
| final TransitionDrawable td = |
| new TransitionDrawable(new Drawable[] { |
| new ColorDrawable(android.R.color.transparent), |
| new BitmapDrawable(mResources, bitmap) |
| }); |
| imageView.setBackgroundDrawable(imageView.getDrawable()); |
| imageView.setImageDrawable(td); |
| td.startTransition(FADE_IN_TIME); |
| } else { |
| imageView.setImageBitmap(bitmap); |
| } |
| } |
| |
| /** |
| * Pause any ongoing background work. This can be used as a temporary |
| * measure to improve performance. For example background work could |
| * be paused when a ListView or GridView is being scrolled using a |
| * {@link android.widget.AbsListView.OnScrollListener} to keep |
| * scrolling smooth. |
| * <p> |
| * If work is paused, be sure setPauseWork(false) is called again |
| * before your fragment or activity is destroyed (for example during |
| * {@link android.app.Activity#onPause()}), or there is a risk the |
| * background thread will never finish. |
| */ |
| public void setPauseWork(boolean pauseWork) { |
| synchronized (mPauseWorkLock) { |
| mPauseWork = pauseWork; |
| if (!mPauseWork) { |
| mPauseWorkLock.notifyAll(); |
| } |
| } |
| } |
| |
| /** |
| * Decode and sample down a bitmap from a file input stream to the requested width and height. |
| * |
| * @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 |
| * @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) { |
| |
| // First decode with inJustDecodeBounds=true to check dimensions |
| final BitmapFactory.Options options = new BitmapFactory.Options(); |
| options.inJustDecodeBounds = true; |
| BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); |
| |
| // Calculate inSampleSize |
| options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); |
| |
| // Decode bitmap with inSampleSize set |
| options.inJustDecodeBounds = false; |
| return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); |
| } |
| |
| /** |
| * Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding |
| * bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates |
| * the closest inSampleSize that will result in the final decoded bitmap having a width and |
| * height equal to or larger than the requested width and height. This implementation does not |
| * ensure a power of 2 is returned for inSampleSize which can be faster when decoding but |
| * results in a larger bitmap which isn't as useful for caching purposes. |
| * |
| * @param options An options object with out* params already populated (run through a decode* |
| * method with inJustDecodeBounds==true |
| * @param reqWidth The requested width of the resulting bitmap |
| * @param reqHeight The requested height of the resulting bitmap |
| * @return The value to be used for inSampleSize |
| */ |
| public static 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) { |
| |
| // Calculate ratios of height and width to requested height and width |
| final int heightRatio = Math.round((float) height / (float) reqHeight); |
| final int widthRatio = Math.round((float) width / (float) reqWidth); |
| |
| // Choose the smallest ratio as inSampleSize value, this will guarantee a final image |
| // with both dimensions larger than or equal to the requested height and width. |
| inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; |
| |
| // This offers some additional logic in case the image has a strange |
| // aspect ratio. For example, a panorama may have a much larger |
| // width than height. In these cases the total pixels might still |
| // end up being too large to fit comfortably in memory, so we should |
| // be more aggressive with sample down the image (=larger inSampleSize). |
| |
| final float totalPixels = width * height; |
| |
| // Anything more than 2x the requested pixels we'll sample down further |
| final float totalReqPixelsCap = reqWidth * reqHeight * 2; |
| |
| while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) { |
| inSampleSize++; |
| } |
| } |
| return inSampleSize; |
| } |
| } |