1 /* 2 * Copyright (C) 2012 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.displayingbitmaps.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.support.v4.app.FragmentActivity; 28 import android.support.v4.app.FragmentManager; 29 import android.widget.ImageView; 30 31 import com.example.android.common.logger.Log; 32 import com.example.android.displayingbitmaps.BuildConfig; 33 34 import java.lang.ref.WeakReference; 35 36 /** 37 * This class wraps up completing some arbitrary long running work when loading a bitmap to an 38 * ImageView. It handles things like using a memory and disk cache, running the work in a background 39 * thread and setting a placeholder image. 40 */ 41 public abstract class ImageWorker { 42 private static final String TAG = "ImageWorker"; 43 private static final int FADE_IN_TIME = 200; 44 45 private ImageCache mImageCache; 46 private ImageCache.ImageCacheParams mImageCacheParams; 47 private Bitmap mLoadingBitmap; 48 private boolean mFadeInBitmap = true; 49 private boolean mExitTasksEarly = false; 50 protected boolean mPauseWork = false; 51 private final Object mPauseWorkLock = new Object(); 52 53 protected Resources mResources; 54 55 private static final int MESSAGE_CLEAR = 0; 56 private static final int MESSAGE_INIT_DISK_CACHE = 1; 57 private static final int MESSAGE_FLUSH = 2; 58 private static final int MESSAGE_CLOSE = 3; 59 ImageWorker(Context context)60 protected ImageWorker(Context context) { 61 mResources = context.getResources(); 62 } 63 64 /** 65 * Load an image specified by the data parameter into an ImageView (override 66 * {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and 67 * disk cache will be used if an {@link ImageCache} has been added using 68 * {@link ImageWorker#addImageCache(android.support.v4.app.FragmentManager, ImageCache.ImageCacheParams)}. If the 69 * image is found in the memory cache, it is set immediately, otherwise an {@link AsyncTask} 70 * will be created to asynchronously load the bitmap. 71 * 72 * @param data The URL of the image to download. 73 * @param imageView The ImageView to bind the downloaded image to. 74 * @param listener A listener that will be called back once the image has been loaded. 75 */ loadImage(Object data, ImageView imageView, OnImageLoadedListener listener)76 public void loadImage(Object data, ImageView imageView, OnImageLoadedListener listener) { 77 if (data == null) { 78 return; 79 } 80 81 BitmapDrawable value = null; 82 83 if (mImageCache != null) { 84 value = mImageCache.getBitmapFromMemCache(String.valueOf(data)); 85 } 86 87 if (value != null) { 88 // Bitmap found in memory cache 89 imageView.setImageDrawable(value); 90 if (listener != null) { 91 listener.onImageLoaded(true); 92 } 93 } else if (cancelPotentialWork(data, imageView)) { 94 //BEGIN_INCLUDE(execute_background_task) 95 final BitmapWorkerTask task = new BitmapWorkerTask(data, imageView, listener); 96 final AsyncDrawable asyncDrawable = 97 new AsyncDrawable(mResources, mLoadingBitmap, task); 98 imageView.setImageDrawable(asyncDrawable); 99 100 // NOTE: This uses a custom version of AsyncTask that has been pulled from the 101 // framework and slightly modified. Refer to the docs at the top of the class 102 // for more info on what was changed. 103 task.executeOnExecutor(AsyncTask.DUAL_THREAD_EXECUTOR); 104 //END_INCLUDE(execute_background_task) 105 } 106 } 107 108 /** 109 * Load an image specified by the data parameter into an ImageView (override 110 * {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and 111 * disk cache will be used if an {@link ImageCache} has been added using 112 * {@link ImageWorker#addImageCache(android.support.v4.app.FragmentManager, ImageCache.ImageCacheParams)}. If the 113 * image is found in the memory cache, it is set immediately, otherwise an {@link AsyncTask} 114 * will be created to asynchronously load the bitmap. 115 * 116 * @param data The URL of the image to download. 117 * @param imageView The ImageView to bind the downloaded image to. 118 */ loadImage(Object data, ImageView imageView)119 public void loadImage(Object data, ImageView imageView) { 120 loadImage(data, imageView, null); 121 } 122 123 /** 124 * Set placeholder bitmap that shows when the the background thread is running. 125 * 126 * @param bitmap 127 */ setLoadingImage(Bitmap bitmap)128 public void setLoadingImage(Bitmap bitmap) { 129 mLoadingBitmap = bitmap; 130 } 131 132 /** 133 * Set placeholder bitmap that shows when the the background thread is running. 134 * 135 * @param resId 136 */ setLoadingImage(int resId)137 public void setLoadingImage(int resId) { 138 mLoadingBitmap = BitmapFactory.decodeResource(mResources, resId); 139 } 140 141 /** 142 * Adds an {@link ImageCache} to this {@link ImageWorker} to handle disk and memory bitmap 143 * caching. 144 * @param fragmentManager 145 * @param cacheParams The cache parameters to use for the image cache. 146 */ addImageCache(FragmentManager fragmentManager, ImageCache.ImageCacheParams cacheParams)147 public void addImageCache(FragmentManager fragmentManager, 148 ImageCache.ImageCacheParams cacheParams) { 149 mImageCacheParams = cacheParams; 150 mImageCache = ImageCache.getInstance(fragmentManager, mImageCacheParams); 151 new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE); 152 } 153 154 /** 155 * Adds an {@link ImageCache} to this {@link ImageWorker} to handle disk and memory bitmap 156 * caching. 157 * @param activity 158 * @param diskCacheDirectoryName See 159 * {@link ImageCache.ImageCacheParams#ImageCacheParams(android.content.Context, String)}. 160 */ addImageCache(FragmentActivity activity, String diskCacheDirectoryName)161 public void addImageCache(FragmentActivity activity, String diskCacheDirectoryName) { 162 mImageCacheParams = new ImageCache.ImageCacheParams(activity, diskCacheDirectoryName); 163 mImageCache = ImageCache.getInstance(activity.getSupportFragmentManager(), mImageCacheParams); 164 new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE); 165 } 166 167 /** 168 * If set to true, the image will fade-in once it has been loaded by the background thread. 169 */ setImageFadeIn(boolean fadeIn)170 public void setImageFadeIn(boolean fadeIn) { 171 mFadeInBitmap = fadeIn; 172 } 173 setExitTasksEarly(boolean exitTasksEarly)174 public void setExitTasksEarly(boolean exitTasksEarly) { 175 mExitTasksEarly = exitTasksEarly; 176 setPauseWork(false); 177 } 178 179 /** 180 * Subclasses should override this to define any processing or work that must happen to produce 181 * the final bitmap. This will be executed in a background thread and be long running. For 182 * example, you could resize a large bitmap here, or pull down an image from the network. 183 * 184 * @param data The data to identify which image to process, as provided by 185 * {@link ImageWorker#loadImage(Object, android.widget.ImageView)} 186 * @return The processed bitmap 187 */ processBitmap(Object data)188 protected abstract Bitmap processBitmap(Object data); 189 190 /** 191 * @return The {@link ImageCache} object currently being used by this ImageWorker. 192 */ getImageCache()193 protected ImageCache getImageCache() { 194 return mImageCache; 195 } 196 197 /** 198 * Cancels any pending work attached to the provided ImageView. 199 * @param imageView 200 */ cancelWork(ImageView imageView)201 public static void cancelWork(ImageView imageView) { 202 final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); 203 if (bitmapWorkerTask != null) { 204 bitmapWorkerTask.cancel(true); 205 if (BuildConfig.DEBUG) { 206 final Object bitmapData = bitmapWorkerTask.mData; 207 Log.d(TAG, "cancelWork - cancelled work for " + bitmapData); 208 } 209 } 210 } 211 212 /** 213 * Returns true if the current work has been canceled or if there was no work in 214 * progress on this image view. 215 * Returns false if the work in progress deals with the same data. The work is not 216 * stopped in that case. 217 */ cancelPotentialWork(Object data, ImageView imageView)218 public static boolean cancelPotentialWork(Object data, ImageView imageView) { 219 //BEGIN_INCLUDE(cancel_potential_work) 220 final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); 221 222 if (bitmapWorkerTask != null) { 223 final Object bitmapData = bitmapWorkerTask.mData; 224 if (bitmapData == null || !bitmapData.equals(data)) { 225 bitmapWorkerTask.cancel(true); 226 if (BuildConfig.DEBUG) { 227 Log.d(TAG, "cancelPotentialWork - cancelled work for " + data); 228 } 229 } else { 230 // The same work is already in progress. 231 return false; 232 } 233 } 234 return true; 235 //END_INCLUDE(cancel_potential_work) 236 } 237 238 /** 239 * @param imageView Any imageView 240 * @return Retrieve the currently active work task (if any) associated with this imageView. 241 * null if there is no such task. 242 */ getBitmapWorkerTask(ImageView imageView)243 private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { 244 if (imageView != null) { 245 final Drawable drawable = imageView.getDrawable(); 246 if (drawable instanceof AsyncDrawable) { 247 final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; 248 return asyncDrawable.getBitmapWorkerTask(); 249 } 250 } 251 return null; 252 } 253 254 /** 255 * The actual AsyncTask that will asynchronously process the image. 256 */ 257 private class BitmapWorkerTask extends AsyncTask<Void, Void, BitmapDrawable> { 258 private Object mData; 259 private final WeakReference<ImageView> imageViewReference; 260 private final OnImageLoadedListener mOnImageLoadedListener; 261 BitmapWorkerTask(Object data, ImageView imageView)262 public BitmapWorkerTask(Object data, ImageView imageView) { 263 mData = data; 264 imageViewReference = new WeakReference<ImageView>(imageView); 265 mOnImageLoadedListener = null; 266 } 267 BitmapWorkerTask(Object data, ImageView imageView, OnImageLoadedListener listener)268 public BitmapWorkerTask(Object data, ImageView imageView, OnImageLoadedListener listener) { 269 mData = data; 270 imageViewReference = new WeakReference<ImageView>(imageView); 271 mOnImageLoadedListener = listener; 272 } 273 274 /** 275 * Background processing. 276 */ 277 @Override doInBackground(Void... params)278 protected BitmapDrawable doInBackground(Void... params) { 279 //BEGIN_INCLUDE(load_bitmap_in_background) 280 if (BuildConfig.DEBUG) { 281 Log.d(TAG, "doInBackground - starting work"); 282 } 283 284 final String dataString = String.valueOf(mData); 285 Bitmap bitmap = null; 286 BitmapDrawable drawable = null; 287 288 // Wait here if work is paused and the task is not cancelled 289 synchronized (mPauseWorkLock) { 290 while (mPauseWork && !isCancelled()) { 291 try { 292 mPauseWorkLock.wait(); 293 } catch (InterruptedException e) {} 294 } 295 } 296 297 // If the image cache is available and this task has not been cancelled by another 298 // thread and the ImageView that was originally bound to this task is still bound back 299 // to this task and our "exit early" flag is not set then try and fetch the bitmap from 300 // the cache 301 if (mImageCache != null && !isCancelled() && getAttachedImageView() != null 302 && !mExitTasksEarly) { 303 bitmap = mImageCache.getBitmapFromDiskCache(dataString); 304 } 305 306 // If the bitmap was not found in the cache and this task has not been cancelled by 307 // another thread and the ImageView that was originally bound to this task is still 308 // bound back to this task and our "exit early" flag is not set, then call the main 309 // process method (as implemented by a subclass) 310 if (bitmap == null && !isCancelled() && getAttachedImageView() != null 311 && !mExitTasksEarly) { 312 bitmap = processBitmap(mData); 313 } 314 315 // If the bitmap was processed and the image cache is available, then add the processed 316 // bitmap to the cache for future use. Note we don't check if the task was cancelled 317 // here, if it was, and the thread is still running, we may as well add the processed 318 // bitmap to our cache as it might be used again in the future 319 if (bitmap != null) { 320 if (Utils.hasHoneycomb()) { 321 // Running on Honeycomb or newer, so wrap in a standard BitmapDrawable 322 drawable = new BitmapDrawable(mResources, bitmap); 323 } else { 324 // Running on Gingerbread or older, so wrap in a RecyclingBitmapDrawable 325 // which will recycle automagically 326 drawable = new RecyclingBitmapDrawable(mResources, bitmap); 327 } 328 329 if (mImageCache != null) { 330 mImageCache.addBitmapToCache(dataString, drawable); 331 } 332 } 333 334 if (BuildConfig.DEBUG) { 335 Log.d(TAG, "doInBackground - finished work"); 336 } 337 338 return drawable; 339 //END_INCLUDE(load_bitmap_in_background) 340 } 341 342 /** 343 * Once the image is processed, associates it to the imageView 344 */ 345 @Override onPostExecute(BitmapDrawable value)346 protected void onPostExecute(BitmapDrawable value) { 347 //BEGIN_INCLUDE(complete_background_work) 348 boolean success = false; 349 // if cancel was called on this task or the "exit early" flag is set then we're done 350 if (isCancelled() || mExitTasksEarly) { 351 value = null; 352 } 353 354 final ImageView imageView = getAttachedImageView(); 355 if (value != null && imageView != null) { 356 if (BuildConfig.DEBUG) { 357 Log.d(TAG, "onPostExecute - setting bitmap"); 358 } 359 success = true; 360 setImageDrawable(imageView, value); 361 } 362 if (mOnImageLoadedListener != null) { 363 mOnImageLoadedListener.onImageLoaded(success); 364 } 365 //END_INCLUDE(complete_background_work) 366 } 367 368 @Override onCancelled(BitmapDrawable value)369 protected void onCancelled(BitmapDrawable value) { 370 super.onCancelled(value); 371 synchronized (mPauseWorkLock) { 372 mPauseWorkLock.notifyAll(); 373 } 374 } 375 376 /** 377 * Returns the ImageView associated with this task as long as the ImageView's task still 378 * points to this task as well. Returns null otherwise. 379 */ getAttachedImageView()380 private ImageView getAttachedImageView() { 381 final ImageView imageView = imageViewReference.get(); 382 final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); 383 384 if (this == bitmapWorkerTask) { 385 return imageView; 386 } 387 388 return null; 389 } 390 } 391 392 /** 393 * Interface definition for callback on image loaded successfully. 394 */ 395 public interface OnImageLoadedListener { 396 397 /** 398 * Called once the image has been loaded. 399 * @param success True if the image was loaded successfully, false if 400 * there was an error. 401 */ onImageLoaded(boolean success)402 void onImageLoaded(boolean success); 403 } 404 405 /** 406 * A custom Drawable that will be attached to the imageView while the work is in progress. 407 * Contains a reference to the actual worker task, so that it can be stopped if a new binding is 408 * required, and makes sure that only the last started worker process can bind its result, 409 * independently of the finish order. 410 */ 411 private static class AsyncDrawable extends BitmapDrawable { 412 private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; 413 AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask)414 public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { 415 super(res, bitmap); 416 bitmapWorkerTaskReference = 417 new WeakReference<BitmapWorkerTask>(bitmapWorkerTask); 418 } 419 getBitmapWorkerTask()420 public BitmapWorkerTask getBitmapWorkerTask() { 421 return bitmapWorkerTaskReference.get(); 422 } 423 } 424 425 /** 426 * Called when the processing is complete and the final drawable should be 427 * set on the ImageView. 428 * 429 * @param imageView 430 * @param drawable 431 */ setImageDrawable(ImageView imageView, Drawable drawable)432 private void setImageDrawable(ImageView imageView, Drawable drawable) { 433 if (mFadeInBitmap) { 434 // Transition drawable with a transparent drawable and the final drawable 435 final TransitionDrawable td = 436 new TransitionDrawable(new Drawable[] { 437 new ColorDrawable(android.R.color.transparent), 438 drawable 439 }); 440 // Set background to loading bitmap 441 imageView.setBackgroundDrawable( 442 new BitmapDrawable(mResources, mLoadingBitmap)); 443 444 imageView.setImageDrawable(td); 445 td.startTransition(FADE_IN_TIME); 446 } else { 447 imageView.setImageDrawable(drawable); 448 } 449 } 450 451 /** 452 * Pause any ongoing background work. This can be used as a temporary 453 * measure to improve performance. For example background work could 454 * be paused when a ListView or GridView is being scrolled using a 455 * {@link android.widget.AbsListView.OnScrollListener} to keep 456 * scrolling smooth. 457 * <p> 458 * If work is paused, be sure setPauseWork(false) is called again 459 * before your fragment or activity is destroyed (for example during 460 * {@link android.app.Activity#onPause()}), or there is a risk the 461 * background thread will never finish. 462 */ setPauseWork(boolean pauseWork)463 public void setPauseWork(boolean pauseWork) { 464 synchronized (mPauseWorkLock) { 465 mPauseWork = pauseWork; 466 if (!mPauseWork) { 467 mPauseWorkLock.notifyAll(); 468 } 469 } 470 } 471 472 protected class CacheAsyncTask extends AsyncTask<Object, Void, Void> { 473 474 @Override doInBackground(Object... params)475 protected Void doInBackground(Object... params) { 476 switch ((Integer)params[0]) { 477 case MESSAGE_CLEAR: 478 clearCacheInternal(); 479 break; 480 case MESSAGE_INIT_DISK_CACHE: 481 initDiskCacheInternal(); 482 break; 483 case MESSAGE_FLUSH: 484 flushCacheInternal(); 485 break; 486 case MESSAGE_CLOSE: 487 closeCacheInternal(); 488 break; 489 } 490 return null; 491 } 492 } 493 initDiskCacheInternal()494 protected void initDiskCacheInternal() { 495 if (mImageCache != null) { 496 mImageCache.initDiskCache(); 497 } 498 } 499 clearCacheInternal()500 protected void clearCacheInternal() { 501 if (mImageCache != null) { 502 mImageCache.clearCache(); 503 } 504 } 505 flushCacheInternal()506 protected void flushCacheInternal() { 507 if (mImageCache != null) { 508 mImageCache.flush(); 509 } 510 } 511 closeCacheInternal()512 protected void closeCacheInternal() { 513 if (mImageCache != null) { 514 mImageCache.close(); 515 mImageCache = null; 516 } 517 } 518 clearCache()519 public void clearCache() { 520 new CacheAsyncTask().execute(MESSAGE_CLEAR); 521 } 522 flushCache()523 public void flushCache() { 524 new CacheAsyncTask().execute(MESSAGE_FLUSH); 525 } 526 closeCache()527 public void closeCache() { 528 new CacheAsyncTask().execute(MESSAGE_CLOSE); 529 } 530 } 531