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