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