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