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 ImageLoader(Context context, int imageSize)54 protected ImageLoader(Context context, int imageSize) { 55 mResources = context.getResources(); 56 mImageSize = imageSize; 57 } 58 getImageSize()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 */ loadImage(Object data, ImageView imageView)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 */ setLoadingImage(int resId)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 */ addImageCache(FragmentManager fragmentManager, float memCacheSizePercent)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 */ setImageFadeIn(boolean fadeIn)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 */ processBitmap(Object data)132 protected abstract Bitmap processBitmap(Object data); 133 134 /** 135 * Cancels any pending work attached to the provided ImageView. 136 */ cancelWork(ImageView imageView)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 */ cancelPotentialWork(Object data, ImageView imageView)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 */ getBitmapWorkerTask(ImageView imageView)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 BitmapWorkerTask(ImageView imageView)195 public BitmapWorkerTask(ImageView imageView) { 196 imageViewReference = new WeakReference<ImageView>(imageView); 197 } 198 199 /** 200 * Background processing. 201 */ 202 @Override doInBackground(Object... params)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 onPostExecute(Bitmap bitmap)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 onCancelled(Bitmap bitmap)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 */ getAttachedImageView()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 AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask)295 public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { 296 super(res, bitmap); 297 bitmapWorkerTaskReference = 298 new WeakReference<BitmapWorkerTask>(bitmapWorkerTask); 299 } 300 getBitmapWorkerTask()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 */ setImageBitmap(ImageView imageView, Bitmap bitmap)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 */ setPauseWork(boolean pauseWork)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 */ decodeSampledBitmapFromDescriptor( FileDescriptor fileDescriptor, int reqWidth, int reqHeight)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 */ calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight)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