• 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.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