Skip to content

Most visited

Recently visited

navigation
DisplayingBitmaps / src / com.example.android.displayingbitmaps / util /

ImageWorker.java

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