2 * Copyright (C) 2013 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com
.android
.volley
.toolbox
;
18 import android
.graphics
.Bitmap
;
19 import android
.graphics
.Bitmap
.Config
;
20 import android
.os
.Handler
;
21 import android
.os
.Looper
;
22 import android
.widget
.ImageView
;
23 import android
.widget
.ImageView
.ScaleType
;
24 import com
.android
.volley
.Request
;
25 import com
.android
.volley
.RequestQueue
;
26 import com
.android
.volley
.Response
.ErrorListener
;
27 import com
.android
.volley
.Response
.Listener
;
28 import com
.android
.volley
.VolleyError
;
30 import java
.util
.HashMap
;
31 import java
.util
.LinkedList
;
34 * Helper that handles loading and caching images from remote URLs.
36 * The simple way to use this class is to call {@link ImageLoader#get(String, ImageListener)}
37 * and to pass in the default image listener provided by
38 * {@link ImageLoader#getImageListener(ImageView, int, int)}. Note that all function calls to
39 * this class must be made from the main thead, and all responses will be delivered to the main
42 public class ImageLoader
{
43 /** RequestQueue for dispatching ImageRequests onto. */
44 private final RequestQueue mRequestQueue
;
46 /** Amount of time to wait after first response arrives before delivering all responses. */
47 private int mBatchResponseDelayMs
= 100;
49 /** The cache implementation to be used as an L1 cache before calling into volley. */
50 private final ImageCache mCache
;
53 * HashMap of Cache keys -> BatchedImageRequest used to track in-flight requests so
54 * that we can coalesce multiple requests to the same URL into a single network request.
56 private final HashMap
<String
, BatchedImageRequest
> mInFlightRequests
=
57 new HashMap
<String
, BatchedImageRequest
>();
59 /** HashMap of the currently pending responses (waiting to be delivered). */
60 private final HashMap
<String
, BatchedImageRequest
> mBatchedResponses
=
61 new HashMap
<String
, BatchedImageRequest
>();
63 /** Handler to the main thread. */
64 private final Handler mHandler
= new Handler(Looper
.getMainLooper());
66 /** Runnable for in-flight response delivery. */
67 private Runnable mRunnable
;
70 * Simple cache adapter interface. If provided to the ImageLoader, it
71 * will be used as an L1 cache before dispatch to Volley. Implementations
72 * must not block. Implementation with an LruCache is recommended.
74 public interface ImageCache
{
75 public Bitmap
getBitmap(String url
);
76 public void putBitmap(String url
, Bitmap bitmap
);
80 * Constructs a new ImageLoader.
81 * @param queue The RequestQueue to use for making image requests.
82 * @param imageCache The cache to use as an L1 cache.
84 public ImageLoader(RequestQueue queue
, ImageCache imageCache
) {
85 mRequestQueue
= queue
;
90 * The default implementation of ImageListener which handles basic functionality
91 * of showing a default image until the network response is received, at which point
92 * it will switch to either the actual image or the error image.
93 * @param view The imageView that the listener is associated with.
94 * @param defaultImageResId Default image resource ID to use, or 0 if it doesn't exist.
95 * @param errorImageResId Error image resource ID to use, or 0 if it doesn't exist.
97 public static ImageListener
getImageListener(final ImageView view
,
98 final int defaultImageResId
, final int errorImageResId
) {
99 return new ImageListener() {
101 public void onErrorResponse(VolleyError error
) {
102 if (errorImageResId
!= 0) {
103 view
.setImageResource(errorImageResId
);
108 public void onResponse(ImageContainer response
, boolean isImmediate
) {
109 if (response
.getBitmap() != null) {
110 view
.setImageBitmap(response
.getBitmap());
111 } else if (defaultImageResId
!= 0) {
112 view
.setImageResource(defaultImageResId
);
119 * Interface for the response handlers on image requests.
121 * The call flow is this:
122 * 1. Upon being attached to a request, onResponse(response, true) will
123 * be invoked to reflect any cached data that was already available. If the
124 * data was available, response.getBitmap() will be non-null.
126 * 2. After a network response returns, only one of the following cases will happen:
127 * - onResponse(response, false) will be called if the image was loaded.
129 * - onErrorResponse will be called if there was an error loading the image.
131 public interface ImageListener
extends ErrorListener
{
133 * Listens for non-error changes to the loading of the image request.
135 * @param response Holds all information pertaining to the request, as well
136 * as the bitmap (if it is loaded).
137 * @param isImmediate True if this was called during ImageLoader.get() variants.
138 * This can be used to differentiate between a cached image loading and a network
139 * image loading in order to, for example, run an animation to fade in network loaded
142 public void onResponse(ImageContainer response
, boolean isImmediate
);
146 * Checks if the item is available in the cache.
147 * @param requestUrl The url of the remote image
148 * @param maxWidth The maximum width of the returned image.
149 * @param maxHeight The maximum height of the returned image.
150 * @return True if the item exists in cache, false otherwise.
152 public boolean isCached(String requestUrl
, int maxWidth
, int maxHeight
) {
153 return isCached(requestUrl
, maxWidth
, maxHeight
, ScaleType
.CENTER_INSIDE
);
157 * Checks if the item is available in the cache.
159 * @param requestUrl The url of the remote image
160 * @param maxWidth The maximum width of the returned image.
161 * @param maxHeight The maximum height of the returned image.
162 * @param scaleType The scaleType of the imageView.
163 * @return True if the item exists in cache, false otherwise.
165 public boolean isCached(String requestUrl
, int maxWidth
, int maxHeight
, ScaleType scaleType
) {
166 throwIfNotOnMainThread();
168 String cacheKey
= getCacheKey(requestUrl
, maxWidth
, maxHeight
, scaleType
);
169 return mCache
.getBitmap(cacheKey
) != null;
173 * Returns an ImageContainer for the requested URL.
175 * The ImageContainer will contain either the specified default bitmap or the loaded bitmap.
176 * If the default was returned, the {@link ImageLoader} will be invoked when the
177 * request is fulfilled.
179 * @param requestUrl The URL of the image to be loaded.
181 public ImageContainer
get(String requestUrl
, final ImageListener listener
) {
182 return get(requestUrl
, listener
, 0, 0);
186 * Equivalent to calling {@link #get(String, ImageListener, int, int, ScaleType)} with
187 * {@code Scaletype == ScaleType.CENTER_INSIDE}.
189 public ImageContainer
get(String requestUrl
, ImageListener imageListener
,
190 int maxWidth
, int maxHeight
) {
191 return get(requestUrl
, imageListener
, maxWidth
, maxHeight
, ScaleType
.CENTER_INSIDE
);
195 * Issues a bitmap request with the given URL if that image is not available
196 * in the cache, and returns a bitmap container that contains all of the data
197 * relating to the request (as well as the default image if the requested
198 * image is not available).
199 * @param requestUrl The url of the remote image
200 * @param imageListener The listener to call when the remote image is loaded
201 * @param maxWidth The maximum width of the returned image.
202 * @param maxHeight The maximum height of the returned image.
203 * @param scaleType The ImageViews ScaleType used to calculate the needed image size.
204 * @return A container object that contains all of the properties of the request, as well as
205 * the currently available image (default if remote is not loaded).
207 public ImageContainer
get(String requestUrl
, ImageListener imageListener
,
208 int maxWidth
, int maxHeight
, ScaleType scaleType
) {
210 // only fulfill requests that were initiated from the main thread.
211 throwIfNotOnMainThread();
213 final String cacheKey
= getCacheKey(requestUrl
, maxWidth
, maxHeight
, scaleType
);
215 // Try to look up the request in the cache of remote images.
216 Bitmap cachedBitmap
= mCache
.getBitmap(cacheKey
);
217 if (cachedBitmap
!= null) {
218 // Return the cached bitmap.
219 ImageContainer container
= new ImageContainer(cachedBitmap
, requestUrl
, null, null);
220 imageListener
.onResponse(container
, true);
224 // The bitmap did not exist in the cache, fetch it!
225 ImageContainer imageContainer
=
226 new ImageContainer(null, requestUrl
, cacheKey
, imageListener
);
228 // Update the caller to let them know that they should use the default bitmap.
229 imageListener
.onResponse(imageContainer
, true);
231 // Check to see if a request is already in-flight.
232 BatchedImageRequest request
= mInFlightRequests
.get(cacheKey
);
233 if (request
!= null) {
234 // If it is, add this request to the list of listeners.
235 request
.addContainer(imageContainer
);
236 return imageContainer
;
239 // The request is not already in flight. Send the new request to the network and
241 Request
<Bitmap
> newRequest
= makeImageRequest(requestUrl
, maxWidth
, maxHeight
, scaleType
,
244 mRequestQueue
.add(newRequest
);
245 mInFlightRequests
.put(cacheKey
,
246 new BatchedImageRequest(newRequest
, imageContainer
));
247 return imageContainer
;
250 protected Request
<Bitmap
> makeImageRequest(String requestUrl
, int maxWidth
, int maxHeight
,
251 ScaleType scaleType
, final String cacheKey
) {
252 return new ImageRequest(requestUrl
, new Listener
<Bitmap
>() {
254 public void onResponse(Bitmap response
) {
255 onGetImageSuccess(cacheKey
, response
);
257 }, maxWidth
, maxHeight
, scaleType
, Config
.RGB_565
, new ErrorListener() {
259 public void onErrorResponse(VolleyError error
) {
260 onGetImageError(cacheKey
, error
);
266 * Sets the amount of time to wait after the first response arrives before delivering all
267 * responses. Batching can be disabled entirely by passing in 0.
268 * @param newBatchedResponseDelayMs The time in milliseconds to wait.
270 public void setBatchedResponseDelay(int newBatchedResponseDelayMs
) {
271 mBatchResponseDelayMs
= newBatchedResponseDelayMs
;
275 * Handler for when an image was successfully loaded.
276 * @param cacheKey The cache key that is associated with the image request.
277 * @param response The bitmap that was returned from the network.
279 protected void onGetImageSuccess(String cacheKey
, Bitmap response
) {
280 // cache the image that was fetched.
281 mCache
.putBitmap(cacheKey
, response
);
283 // remove the request from the list of in-flight requests.
284 BatchedImageRequest request
= mInFlightRequests
.remove(cacheKey
);
286 if (request
!= null) {
287 // Update the response bitmap.
288 request
.mResponseBitmap
= response
;
290 // Send the batched response
291 batchResponse(cacheKey
, request
);
296 * Handler for when an image failed to load.
297 * @param cacheKey The cache key that is associated with the image request.
299 protected void onGetImageError(String cacheKey
, VolleyError error
) {
300 // Notify the requesters that something failed via a null result.
301 // Remove this request from the list of in-flight requests.
302 BatchedImageRequest request
= mInFlightRequests
.remove(cacheKey
);
304 if (request
!= null) {
305 // Set the error for this request
306 request
.setError(error
);
308 // Send the batched response
309 batchResponse(cacheKey
, request
);
314 * Container object for all of the data surrounding an image request.
316 public class ImageContainer
{
318 * The most relevant bitmap for the container. If the image was in cache, the
319 * Holder to use for the final bitmap (the one that pairs to the requested URL).
321 private Bitmap mBitmap
;
323 private final ImageListener mListener
;
325 /** The cache key that was associated with the request */
326 private final String mCacheKey
;
328 /** The request URL that was specified */
329 private final String mRequestUrl
;
332 * Constructs a BitmapContainer object.
333 * @param bitmap The final bitmap (if it exists).
334 * @param requestUrl The requested URL for this container.
335 * @param cacheKey The cache key that identifies the requested URL for this container.
337 public ImageContainer(Bitmap bitmap
, String requestUrl
,
338 String cacheKey
, ImageListener listener
) {
340 mRequestUrl
= requestUrl
;
341 mCacheKey
= cacheKey
;
342 mListener
= listener
;
346 * Releases interest in the in-flight request (and cancels it if no one else is listening).
348 public void cancelRequest() {
349 if (mListener
== null) {
353 BatchedImageRequest request
= mInFlightRequests
.get(mCacheKey
);
354 if (request
!= null) {
355 boolean canceled
= request
.removeContainerAndCancelIfNecessary(this);
357 mInFlightRequests
.remove(mCacheKey
);
360 // check to see if it is already batched for delivery.
361 request
= mBatchedResponses
.get(mCacheKey
);
362 if (request
!= null) {
363 request
.removeContainerAndCancelIfNecessary(this);
364 if (request
.mContainers
.size() == 0) {
365 mBatchedResponses
.remove(mCacheKey
);
372 * Returns the bitmap associated with the request URL if it has been loaded, null otherwise.
374 public Bitmap
getBitmap() {
379 * Returns the requested URL for this container.
381 public String
getRequestUrl() {
387 * Wrapper class used to map a Request to the set of active ImageContainer objects that are
388 * interested in its results.
390 private class BatchedImageRequest
{
391 /** The request being tracked */
392 private final Request
<?
> mRequest
;
394 /** The result of the request being tracked by this item */
395 private Bitmap mResponseBitmap
;
397 /** Error if one occurred for this response */
398 private VolleyError mError
;
400 /** List of all of the active ImageContainers that are interested in the request */
401 private final LinkedList
<ImageContainer
> mContainers
= new LinkedList
<ImageContainer
>();
404 * Constructs a new BatchedImageRequest object
405 * @param request The request being tracked
406 * @param container The ImageContainer of the person who initiated the request.
408 public BatchedImageRequest(Request
<?
> request
, ImageContainer container
) {
410 mContainers
.add(container
);
414 * Set the error for this response
416 public void setError(VolleyError error
) {
421 * Get the error for this response
423 public VolleyError
getError() {
428 * Adds another ImageContainer to the list of those interested in the results of
431 public void addContainer(ImageContainer container
) {
432 mContainers
.add(container
);
436 * Detatches the bitmap container from the request and cancels the request if no one is
438 * @param container The container to remove from the list
439 * @return True if the request was canceled, false otherwise.
441 public boolean removeContainerAndCancelIfNecessary(ImageContainer container
) {
442 mContainers
.remove(container
);
443 if (mContainers
.size() == 0) {
452 * Starts the runnable for batched delivery of responses if it is not already started.
453 * @param cacheKey The cacheKey of the response being delivered.
454 * @param request The BatchedImageRequest to be delivered.
456 private void batchResponse(String cacheKey
, BatchedImageRequest request
) {
457 mBatchedResponses
.put(cacheKey
, request
);
458 // If we don't already have a batch delivery runnable in flight, make a new one.
459 // Note that this will be used to deliver responses to all callers in mBatchedResponses.
460 if (mRunnable
== null) {
461 mRunnable
= new Runnable() {
464 for (BatchedImageRequest bir
: mBatchedResponses
.values()) {
465 for (ImageContainer container
: bir
.mContainers
) {
466 // If one of the callers in the batched request canceled the request
467 // after the response was received but before it was delivered,
469 if (container
.mListener
== null) {
472 if (bir
.getError() == null) {
473 container
.mBitmap
= bir
.mResponseBitmap
;
474 container
.mListener
.onResponse(container
, false);
476 container
.mListener
.onErrorResponse(bir
.getError());
480 mBatchedResponses
.clear();
485 // Post the runnable.
486 mHandler
.postDelayed(mRunnable
, mBatchResponseDelayMs
);
490 private void throwIfNotOnMainThread() {
491 if (Looper
.myLooper() != Looper
.getMainLooper()) {
492 throw new IllegalStateException("ImageLoader must be invoked from the main thread.");
496 * Creates a cache key for use with the L1 cache.
497 * @param url The URL of the request.
498 * @param maxWidth The max-width of the output.
499 * @param maxHeight The max-height of the output.
500 * @param scaleType The scaleType of the imageView.
502 private static String
getCacheKey(String url
, int maxWidth
, int maxHeight
, ScaleType scaleType
) {
503 return new StringBuilder(url
.length() + 12).append("#W").append(maxWidth
)
504 .append("#H").append(maxHeight
).append("#S").append(scaleType
.ordinal()).append(url
)