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