• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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