Home | History | Annotate | Download | only in util
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.example.android.contactslist.util;
     18 
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.graphics.Bitmap;
     22 import android.graphics.BitmapFactory;
     23 import android.graphics.drawable.BitmapDrawable;
     24 import android.graphics.drawable.ColorDrawable;
     25 import android.graphics.drawable.Drawable;
     26 import android.graphics.drawable.TransitionDrawable;
     27 import android.os.AsyncTask;
     28 import android.support.v4.app.FragmentManager;
     29 import android.util.Log;
     30 import android.widget.ImageView;
     31 
     32 import com.example.android.contactslist.BuildConfig;
     33 
     34 import java.io.FileDescriptor;
     35 import java.lang.ref.WeakReference;
     36 
     37 /**
     38  * This class wraps up completing some arbitrary long running work when loading a bitmap to an
     39  * ImageView. It handles things like using a memory and disk cache, running the work in a background
     40  * thread and setting a placeholder image.
     41  */
     42 public abstract class ImageLoader {
     43     private static final String TAG = "ImageLoader";
     44     private static final int FADE_IN_TIME = 200;
     45 
     46     private ImageCache mImageCache;
     47     private Bitmap mLoadingBitmap;
     48     private boolean mFadeInBitmap = true;
     49     private boolean mPauseWork = false;
     50     private final Object mPauseWorkLock = new Object();
     51     private int mImageSize;
     52     private Resources mResources;
     53 
     54     protected ImageLoader(Context context, int imageSize) {
     55         mResources = context.getResources();
     56         mImageSize = imageSize;
     57     }
     58 
     59     public int getImageSize() {
     60         return mImageSize;
     61     }
     62 
     63     /**
     64      * Load an image specified by the data parameter into an ImageView (override
     65      * {@link ImageLoader#processBitmap(Object)} to define the processing logic). If the image is
     66      * found in the memory cache, it is set immediately, otherwise an {@link AsyncTask} will be
     67      * created to asynchronously load the bitmap.
     68      *
     69      * @param data The URL of the image to download.
     70      * @param imageView The ImageView to bind the downloaded image to.
     71      */
     72     public void loadImage(Object data, ImageView imageView) {
     73         if (data == null) {
     74             imageView.setImageBitmap(mLoadingBitmap);
     75             return;
     76         }
     77 
     78         Bitmap bitmap = null;
     79 
     80         if (mImageCache != null) {
     81             bitmap = mImageCache.getBitmapFromMemCache(String.valueOf(data));
     82         }
     83 
     84         if (bitmap != null) {
     85             // Bitmap found in memory cache
     86             imageView.setImageBitmap(bitmap);
     87         } else if (cancelPotentialWork(data, imageView)) {
     88             final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
     89             final AsyncDrawable asyncDrawable =
     90                     new AsyncDrawable(mResources, mLoadingBitmap, task);
     91             imageView.setImageDrawable(asyncDrawable);
     92             task.execute(data);
     93         }
     94     }
     95 
     96     /**
     97      * Set placeholder bitmap that shows when the the background thread is running.
     98      *
     99      * @param resId Resource ID of loading image.
    100      */
    101     public void setLoadingImage(int resId) {
    102         mLoadingBitmap = BitmapFactory.decodeResource(mResources, resId);
    103     }
    104 
    105     /**
    106      * Adds an {@link ImageCache} to this image loader.
    107      *
    108      * @param fragmentManager A FragmentManager to use to retain the cache over configuration
    109      *                        changes such as an orientation change.
    110      * @param memCacheSizePercent The cache size as a percent of available app memory.
    111      */
    112     public void addImageCache(FragmentManager fragmentManager, float memCacheSizePercent) {
    113         mImageCache = ImageCache.getInstance(fragmentManager, memCacheSizePercent);
    114     }
    115 
    116     /**
    117      * If set to true, the image will fade-in once it has been loaded by the background thread.
    118      */
    119     public void setImageFadeIn(boolean fadeIn) {
    120         mFadeInBitmap = fadeIn;
    121     }
    122 
    123     /**
    124      * Subclasses should override this to define any processing or work that must happen to produce
    125      * the final bitmap. This will be executed in a background thread and be long running. For
    126      * example, you could resize a large bitmap here, or pull down an image from the network.
    127      *
    128      * @param data The data to identify which image to process, as provided by
    129      *            {@link ImageLoader#loadImage(Object, ImageView)}
    130      * @return The processed bitmap
    131      */
    132     protected abstract Bitmap processBitmap(Object data);
    133 
    134     /**
    135      * Cancels any pending work attached to the provided ImageView.
    136      */
    137     public static void cancelWork(ImageView imageView) {
    138         final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
    139         if (bitmapWorkerTask != null) {
    140             bitmapWorkerTask.cancel(true);
    141             if (BuildConfig.DEBUG) {
    142                 final Object bitmapData = bitmapWorkerTask.data;
    143                 Log.d(TAG, "cancelWork - cancelled work for " + bitmapData);
    144             }
    145         }
    146     }
    147 
    148     /**
    149      * Returns true if the current work has been canceled or if there was no work in
    150      * progress on this image view.
    151      * Returns false if the work in progress deals with the same data. The work is not
    152      * stopped in that case.
    153      */
    154     public static boolean cancelPotentialWork(Object data, ImageView imageView) {
    155         final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
    156 
    157         if (bitmapWorkerTask != null) {
    158             final Object bitmapData = bitmapWorkerTask.data;
    159             if (bitmapData == null || !bitmapData.equals(data)) {
    160                 bitmapWorkerTask.cancel(true);
    161                 if (BuildConfig.DEBUG) {
    162                     Log.d(TAG, "cancelPotentialWork - cancelled work for " + data);
    163                 }
    164             } else {
    165                 // The same work is already in progress.
    166                 return false;
    167             }
    168         }
    169         return true;
    170     }
    171 
    172     /**
    173      * @param imageView Any imageView
    174      * @return Retrieve the currently active work task (if any) associated with this imageView.
    175      * null if there is no such task.
    176      */
    177     private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
    178         if (imageView != null) {
    179             final Drawable drawable = imageView.getDrawable();
    180             if (drawable instanceof AsyncDrawable) {
    181                 final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
    182                 return asyncDrawable.getBitmapWorkerTask();
    183             }
    184         }
    185         return null;
    186     }
    187 
    188     /**
    189      * The actual AsyncTask that will asynchronously process the image.
    190      */
    191     private class BitmapWorkerTask extends AsyncTask<Object, Void, Bitmap> {
    192         private Object data;
    193         private final WeakReference<ImageView> imageViewReference;
    194 
    195         public BitmapWorkerTask(ImageView imageView) {
    196             imageViewReference = new WeakReference<ImageView>(imageView);
    197         }
    198 
    199         /**
    200          * Background processing.
    201          */
    202         @Override
    203         protected Bitmap doInBackground(Object... params) {
    204             if (BuildConfig.DEBUG) {
    205                 Log.d(TAG, "doInBackground - starting work");
    206             }
    207 
    208             data = params[0];
    209             final String dataString = String.valueOf(data);
    210             Bitmap bitmap = null;
    211 
    212             // Wait here if work is paused and the task is not cancelled
    213             synchronized (mPauseWorkLock) {
    214                 while (mPauseWork && !isCancelled()) {
    215                     try {
    216                         mPauseWorkLock.wait();
    217                     } catch (InterruptedException e) {}
    218                 }
    219             }
    220 
    221             // If the task has not been cancelled by another thread and the ImageView that was
    222             // originally bound to this task is still bound back to this task and our "exit early"
    223             // flag is not set, then call the main process method (as implemented by a subclass)
    224             if (!isCancelled() && getAttachedImageView() != null) {
    225                 bitmap = processBitmap(params[0]);
    226             }
    227 
    228             // If the bitmap was processed and the image cache is available, then add the processed
    229             // bitmap to the cache for future use. Note we don't check if the task was cancelled
    230             // here, if it was, and the thread is still running, we may as well add the processed
    231             // bitmap to our cache as it might be used again in the future
    232             if (bitmap != null && mImageCache != null) {
    233                 mImageCache.addBitmapToCache(dataString, bitmap);
    234             }
    235 
    236             if (BuildConfig.DEBUG) {
    237                 Log.d(TAG, "doInBackground - finished work");
    238             }
    239 
    240             return bitmap;
    241         }
    242 
    243         /**
    244          * Once the image is processed, associates it to the imageView
    245          */
    246         @Override
    247         protected void onPostExecute(Bitmap bitmap) {
    248             // if cancel was called on this task or the "exit early" flag is set then we're done
    249             if (isCancelled()) {
    250                 bitmap = null;
    251             }
    252 
    253             final ImageView imageView = getAttachedImageView();
    254             if (bitmap != null && imageView != null) {
    255                 if (BuildConfig.DEBUG) {
    256                     Log.d(TAG, "onPostExecute - setting bitmap");
    257                 }
    258                 setImageBitmap(imageView, bitmap);
    259             }
    260         }
    261 
    262         @Override
    263         protected void onCancelled(Bitmap bitmap) {
    264             super.onCancelled(bitmap);
    265             synchronized (mPauseWorkLock) {
    266                 mPauseWorkLock.notifyAll();
    267             }
    268         }
    269 
    270         /**
    271          * Returns the ImageView associated with this task as long as the ImageView's task still
    272          * points to this task as well. Returns null otherwise.
    273          */
    274         private ImageView getAttachedImageView() {
    275             final ImageView imageView = imageViewReference.get();
    276             final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
    277 
    278             if (this == bitmapWorkerTask) {
    279                 return imageView;
    280             }
    281 
    282             return null;
    283         }
    284     }
    285 
    286     /**
    287      * A custom Drawable that will be attached to the imageView while the work is in progress.
    288      * Contains a reference to the actual worker task, so that it can be stopped if a new binding is
    289      * required, and makes sure that only the last started worker process can bind its result,
    290      * independently of the finish order.
    291      */
    292     private static class AsyncDrawable extends BitmapDrawable {
    293         private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
    294 
    295         public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
    296             super(res, bitmap);
    297             bitmapWorkerTaskReference =
    298                 new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
    299         }
    300 
    301         public BitmapWorkerTask getBitmapWorkerTask() {
    302             return bitmapWorkerTaskReference.get();
    303         }
    304     }
    305 
    306     /**
    307      * Called when the processing is complete and the final bitmap should be set on the ImageView.
    308      *
    309      * @param imageView The ImageView to set the bitmap to.
    310      * @param bitmap The new bitmap to set.
    311      */
    312     private void setImageBitmap(ImageView imageView, Bitmap bitmap) {
    313         if (mFadeInBitmap) {
    314             // Transition drawable to fade from loading bitmap to final bitmap
    315             final TransitionDrawable td =
    316                     new TransitionDrawable(new Drawable[] {
    317                             new ColorDrawable(android.R.color.transparent),
    318                             new BitmapDrawable(mResources, bitmap)
    319                     });
    320             imageView.setBackgroundDrawable(imageView.getDrawable());
    321             imageView.setImageDrawable(td);
    322             td.startTransition(FADE_IN_TIME);
    323         } else {
    324             imageView.setImageBitmap(bitmap);
    325         }
    326     }
    327 
    328     /**
    329      * Pause any ongoing background work. This can be used as a temporary
    330      * measure to improve performance. For example background work could
    331      * be paused when a ListView or GridView is being scrolled using a
    332      * {@link android.widget.AbsListView.OnScrollListener} to keep
    333      * scrolling smooth.
    334      * <p>
    335      * If work is paused, be sure setPauseWork(false) is called again
    336      * before your fragment or activity is destroyed (for example during
    337      * {@link android.app.Activity#onPause()}), or there is a risk the
    338      * background thread will never finish.
    339      */
    340     public void setPauseWork(boolean pauseWork) {
    341         synchronized (mPauseWorkLock) {
    342             mPauseWork = pauseWork;
    343             if (!mPauseWork) {
    344                 mPauseWorkLock.notifyAll();
    345             }
    346         }
    347     }
    348 
    349     /**
    350      * Decode and sample down a bitmap from a file input stream to the requested width and height.
    351      *
    352      * @param fileDescriptor The file descriptor to read from
    353      * @param reqWidth The requested width of the resulting bitmap
    354      * @param reqHeight The requested height of the resulting bitmap
    355      * @return A bitmap sampled down from the original with the same aspect ratio and dimensions
    356      *         that are equal to or greater than the requested width and height
    357      */
    358     public static Bitmap decodeSampledBitmapFromDescriptor(
    359             FileDescriptor fileDescriptor, int reqWidth, int reqHeight) {
    360 
    361         // First decode with inJustDecodeBounds=true to check dimensions
    362         final BitmapFactory.Options options = new BitmapFactory.Options();
    363         options.inJustDecodeBounds = true;
    364         BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
    365 
    366         // Calculate inSampleSize
    367         options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
    368 
    369         // Decode bitmap with inSampleSize set
    370         options.inJustDecodeBounds = false;
    371         return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
    372     }
    373 
    374     /**
    375      * Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding
    376      * bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates
    377      * the closest inSampleSize that will result in the final decoded bitmap having a width and
    378      * height equal to or larger than the requested width and height. This implementation does not
    379      * ensure a power of 2 is returned for inSampleSize which can be faster when decoding but
    380      * results in a larger bitmap which isn't as useful for caching purposes.
    381      *
    382      * @param options An options object with out* params already populated (run through a decode*
    383      *            method with inJustDecodeBounds==true
    384      * @param reqWidth The requested width of the resulting bitmap
    385      * @param reqHeight The requested height of the resulting bitmap
    386      * @return The value to be used for inSampleSize
    387      */
    388     public static int calculateInSampleSize(BitmapFactory.Options options,
    389             int reqWidth, int reqHeight) {
    390         // Raw height and width of image
    391         final int height = options.outHeight;
    392         final int width = options.outWidth;
    393         int inSampleSize = 1;
    394 
    395         if (height > reqHeight || width > reqWidth) {
    396 
    397             // Calculate ratios of height and width to requested height and width
    398             final int heightRatio = Math.round((float) height / (float) reqHeight);
    399             final int widthRatio = Math.round((float) width / (float) reqWidth);
    400 
    401             // Choose the smallest ratio as inSampleSize value, this will guarantee a final image
    402             // with both dimensions larger than or equal to the requested height and width.
    403             inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
    404 
    405             // This offers some additional logic in case the image has a strange
    406             // aspect ratio. For example, a panorama may have a much larger
    407             // width than height. In these cases the total pixels might still
    408             // end up being too large to fit comfortably in memory, so we should
    409             // be more aggressive with sample down the image (=larger inSampleSize).
    410 
    411             final float totalPixels = width * height;
    412 
    413             // Anything more than 2x the requested pixels we'll sample down further
    414             final float totalReqPixelsCap = reqWidth * reqHeight * 2;
    415 
    416             while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
    417                 inSampleSize++;
    418             }
    419         }
    420         return inSampleSize;
    421     }
    422 }
    423