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.os.AsyncTask; 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 Bitmap mLoadingBitmap; 46 private boolean mFadeInBitmap = true; 47 private boolean mExitTasksEarly = false; 48 49 protected Context mContext; 50 protected ImageWorkerAdapter mImageWorkerAdapter; 51 ImageWorker(Context context)52 protected ImageWorker(Context context) { 53 mContext = context; 54 } 55 56 /** 57 * Load an image specified by the data parameter into an ImageView (override 58 * {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and disk 59 * cache will be used if an {@link ImageCache} has been set using 60 * {@link ImageWorker#setImageCache(ImageCache)}. If the image is found in the memory cache, it 61 * is set immediately, otherwise an {@link AsyncTask} will be created to asynchronously load the 62 * bitmap. 63 * 64 * @param data The URL of the image to download. 65 * @param imageView The ImageView to bind the downloaded image to. 66 */ loadImage(Object data, ImageView imageView)67 public void loadImage(Object data, ImageView imageView) { 68 Bitmap bitmap = null; 69 70 if (mImageCache != null) { 71 bitmap = mImageCache.getBitmapFromMemCache(String.valueOf(data)); 72 } 73 74 if (bitmap != null) { 75 // Bitmap found in memory cache 76 imageView.setImageBitmap(bitmap); 77 } else if (cancelPotentialWork(data, imageView)) { 78 final BitmapWorkerTask task = new BitmapWorkerTask(imageView); 79 final AsyncDrawable asyncDrawable = 80 new AsyncDrawable(mContext.getResources(), mLoadingBitmap, task); 81 imageView.setImageDrawable(asyncDrawable); 82 task.execute(data); 83 } 84 } 85 86 /** 87 * Load an image specified from a set adapter into an ImageView (override 88 * {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and disk 89 * cache will be used if an {@link ImageCache} has been set using 90 * {@link ImageWorker#setImageCache(ImageCache)}. If the image is found in the memory cache, it 91 * is set immediately, otherwise an {@link AsyncTask} will be created to asynchronously load the 92 * bitmap. {@link ImageWorker#setAdapter(ImageWorkerAdapter)} must be called before using this 93 * method. 94 * 95 * @param data The URL of the image to download. 96 * @param imageView The ImageView to bind the downloaded image to. 97 */ loadImage(int num, ImageView imageView)98 public void loadImage(int num, ImageView imageView) { 99 if (mImageWorkerAdapter != null) { 100 loadImage(mImageWorkerAdapter.getItem(num), imageView); 101 } else { 102 throw new NullPointerException("Data not set, must call setAdapter() first."); 103 } 104 } 105 106 /** 107 * Set placeholder bitmap that shows when the the background thread is running. 108 * 109 * @param bitmap 110 */ setLoadingImage(Bitmap bitmap)111 public void setLoadingImage(Bitmap bitmap) { 112 mLoadingBitmap = bitmap; 113 } 114 115 /** 116 * Set placeholder bitmap that shows when the the background thread is running. 117 * 118 * @param resId 119 */ setLoadingImage(int resId)120 public void setLoadingImage(int resId) { 121 mLoadingBitmap = BitmapFactory.decodeResource(mContext.getResources(), resId); 122 } 123 124 /** 125 * Set the {@link ImageCache} object to use with this ImageWorker. 126 * 127 * @param cacheCallback 128 */ setImageCache(ImageCache cacheCallback)129 public void setImageCache(ImageCache cacheCallback) { 130 mImageCache = cacheCallback; 131 } 132 getImageCache()133 public ImageCache getImageCache() { 134 return mImageCache; 135 } 136 137 /** 138 * If set to true, the image will fade-in once it has been loaded by the background thread. 139 * 140 * @param fadeIn 141 */ setImageFadeIn(boolean fadeIn)142 public void setImageFadeIn(boolean fadeIn) { 143 mFadeInBitmap = fadeIn; 144 } 145 setExitTasksEarly(boolean exitTasksEarly)146 public void setExitTasksEarly(boolean exitTasksEarly) { 147 mExitTasksEarly = exitTasksEarly; 148 } 149 150 /** 151 * Subclasses should override this to define any processing or work that must happen to produce 152 * the final bitmap. This will be executed in a background thread and be long running. For 153 * example, you could resize a large bitmap here, or pull down an image from the network. 154 * 155 * @param data The data to identify which image to process, as provided by 156 * {@link ImageWorker#loadImage(Object, ImageView)} 157 * @return The processed bitmap 158 */ processBitmap(Object data)159 protected abstract Bitmap processBitmap(Object data); 160 cancelWork(ImageView imageView)161 public static void cancelWork(ImageView imageView) { 162 final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); 163 if (bitmapWorkerTask != null) { 164 bitmapWorkerTask.cancel(true); 165 if (BuildConfig.DEBUG) { 166 final Object bitmapData = bitmapWorkerTask.data; 167 Log.d(TAG, "cancelWork - cancelled work for " + bitmapData); 168 } 169 } 170 } 171 172 /** 173 * Returns true if the current work has been canceled or if there was no work in 174 * progress on this image view. 175 * Returns false if the work in progress deals with the same data. The work is not 176 * stopped in that case. 177 */ cancelPotentialWork(Object data, ImageView imageView)178 public static boolean cancelPotentialWork(Object data, ImageView imageView) { 179 final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); 180 181 if (bitmapWorkerTask != null) { 182 final Object bitmapData = bitmapWorkerTask.data; 183 if (bitmapData == null || !bitmapData.equals(data)) { 184 bitmapWorkerTask.cancel(true); 185 if (BuildConfig.DEBUG) { 186 Log.d(TAG, "cancelPotentialWork - cancelled work for " + data); 187 } 188 } else { 189 // The same work is already in progress. 190 return false; 191 } 192 } 193 return true; 194 } 195 196 /** 197 * @param imageView Any imageView 198 * @return Retrieve the currently active work task (if any) associated with this imageView. 199 * null if there is no such task. 200 */ getBitmapWorkerTask(ImageView imageView)201 private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { 202 if (imageView != null) { 203 final Drawable drawable = imageView.getDrawable(); 204 if (drawable instanceof AsyncDrawable) { 205 final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; 206 return asyncDrawable.getBitmapWorkerTask(); 207 } 208 } 209 return null; 210 } 211 212 /** 213 * The actual AsyncTask that will asynchronously process the image. 214 */ 215 private class BitmapWorkerTask extends AsyncTask<Object, Void, Bitmap> { 216 private Object data; 217 private final WeakReference<ImageView> imageViewReference; 218 BitmapWorkerTask(ImageView imageView)219 public BitmapWorkerTask(ImageView imageView) { 220 imageViewReference = new WeakReference<ImageView>(imageView); 221 } 222 223 /** 224 * Background processing. 225 */ 226 @Override doInBackground(Object... params)227 protected Bitmap doInBackground(Object... params) { 228 data = params[0]; 229 final String dataString = String.valueOf(data); 230 Bitmap bitmap = null; 231 232 // If the image cache is available and this task has not been cancelled by another 233 // thread and the ImageView that was originally bound to this task is still bound back 234 // to this task and our "exit early" flag is not set then try and fetch the bitmap from 235 // the cache 236 if (mImageCache != null && !isCancelled() && getAttachedImageView() != null 237 && !mExitTasksEarly) { 238 bitmap = mImageCache.getBitmapFromDiskCache(dataString); 239 } 240 241 // If the bitmap was not found in the cache and this task has not been cancelled by 242 // another thread and the ImageView that was originally bound to this task is still 243 // bound back to this task and our "exit early" flag is not set, then call the main 244 // process method (as implemented by a subclass) 245 if (bitmap == null && !isCancelled() && getAttachedImageView() != null 246 && !mExitTasksEarly) { 247 bitmap = processBitmap(params[0]); 248 } 249 250 // If the bitmap was processed and the image cache is available, then add the processed 251 // bitmap to the cache for future use. Note we don't check if the task was cancelled 252 // here, if it was, and the thread is still running, we may as well add the processed 253 // bitmap to our cache as it might be used again in the future 254 if (bitmap != null && mImageCache != null) { 255 mImageCache.addBitmapToCache(dataString, bitmap); 256 } 257 258 return bitmap; 259 } 260 261 /** 262 * Once the image is processed, associates it to the imageView 263 */ 264 @Override onPostExecute(Bitmap bitmap)265 protected void onPostExecute(Bitmap bitmap) { 266 // if cancel was called on this task or the "exit early" flag is set then we're done 267 if (isCancelled() || mExitTasksEarly) { 268 bitmap = null; 269 } 270 271 final ImageView imageView = getAttachedImageView(); 272 if (bitmap != null && imageView != null) { 273 setImageBitmap(imageView, bitmap); 274 } 275 } 276 277 /** 278 * Returns the ImageView associated with this task as long as the ImageView's task still 279 * points to this task as well. Returns null otherwise. 280 */ getAttachedImageView()281 private ImageView getAttachedImageView() { 282 final ImageView imageView = imageViewReference.get(); 283 final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); 284 285 if (this == bitmapWorkerTask) { 286 return imageView; 287 } 288 289 return null; 290 } 291 } 292 293 /** 294 * A custom Drawable that will be attached to the imageView while the work is in progress. 295 * Contains a reference to the actual worker task, so that it can be stopped if a new binding is 296 * required, and makes sure that only the last started worker process can bind its result, 297 * independently of the finish order. 298 */ 299 private static class AsyncDrawable extends BitmapDrawable { 300 private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; 301 AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask)302 public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { 303 super(res, bitmap); 304 305 bitmapWorkerTaskReference = 306 new WeakReference<BitmapWorkerTask>(bitmapWorkerTask); 307 } 308 getBitmapWorkerTask()309 public BitmapWorkerTask getBitmapWorkerTask() { 310 return bitmapWorkerTaskReference.get(); 311 } 312 } 313 314 /** 315 * Called when the processing is complete and the final bitmap should be set on the ImageView. 316 * 317 * @param imageView 318 * @param bitmap 319 */ setImageBitmap(ImageView imageView, Bitmap bitmap)320 private void setImageBitmap(ImageView imageView, Bitmap bitmap) { 321 if (mFadeInBitmap) { 322 // Transition drawable with a transparent drwabale and the final bitmap 323 final TransitionDrawable td = 324 new TransitionDrawable(new Drawable[] { 325 new ColorDrawable(android.R.color.transparent), 326 new BitmapDrawable(mContext.getResources(), bitmap) 327 }); 328 // Set background to loading bitmap 329 imageView.setBackgroundDrawable( 330 new BitmapDrawable(mContext.getResources(), mLoadingBitmap)); 331 332 imageView.setImageDrawable(td); 333 td.startTransition(FADE_IN_TIME); 334 } else { 335 imageView.setImageBitmap(bitmap); 336 } 337 } 338 339 /** 340 * Set the simple adapter which holds the backing data. 341 * 342 * @param adapter 343 */ setAdapter(ImageWorkerAdapter adapter)344 public void setAdapter(ImageWorkerAdapter adapter) { 345 mImageWorkerAdapter = adapter; 346 } 347 348 /** 349 * Get the current adapter. 350 * 351 * @return 352 */ getAdapter()353 public ImageWorkerAdapter getAdapter() { 354 return mImageWorkerAdapter; 355 } 356 357 /** 358 * A very simple adapter for use with ImageWorker class and subclasses. 359 */ 360 public static abstract class ImageWorkerAdapter { getItem(int num)361 public abstract Object getItem(int num); getSize()362 public abstract int getSize(); 363 } 364 } 365