com.android.volley.toolbox.ImageLoader Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of library Show documentation
Show all versions of library Show documentation
Volley is a network library from Android source code.
/**
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.volley.toolbox;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.os.Handler;
import android.os.Looper;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response.ErrorListener;
import com.android.volley.Response.Listener;
import com.android.volley.VolleyError;
import java.util.HashMap;
import java.util.LinkedList;
/**
* Helper that handles loading and caching images from remote URLs.
*
* The simple way to use this class is to call {@link ImageLoader#get(String, ImageListener)}
* and to pass in the default image listener provided by
* {@link ImageLoader#getImageListener(ImageView, int, int)}. Note that all function calls to
* this class must be made from the main thead, and all responses will be delivered to the main
* thread as well.
*/
public class ImageLoader {
/** RequestQueue for dispatching ImageRequests onto. */
private final RequestQueue mRequestQueue;
/** Amount of time to wait after first response arrives before delivering all responses. */
private int mBatchResponseDelayMs = 100;
/** The cache implementation to be used as an L1 cache before calling into volley. */
private final ImageCache mCache;
/**
* HashMap of Cache keys -> BatchedImageRequest used to track in-flight requests so
* that we can coalesce multiple requests to the same URL into a single network request.
*/
private final HashMap mInFlightRequests =
new HashMap();
/** HashMap of the currently pending responses (waiting to be delivered). */
private final HashMap mBatchedResponses =
new HashMap();
/** Handler to the main thread. */
private final Handler mHandler = new Handler(Looper.getMainLooper());
/** Runnable for in-flight response delivery. */
private Runnable mRunnable;
/**
* Simple cache adapter interface. If provided to the ImageLoader, it
* will be used as an L1 cache before dispatch to Volley. Implementations
* must not block. Implementation with an LruCache is recommended.
*/
public interface ImageCache {
public Bitmap getBitmap(String url);
public void putBitmap(String url, Bitmap bitmap);
}
/**
* Constructs a new ImageLoader.
* @param queue The RequestQueue to use for making image requests.
* @param imageCache The cache to use as an L1 cache.
*/
public ImageLoader(RequestQueue queue, ImageCache imageCache) {
mRequestQueue = queue;
mCache = imageCache;
}
/**
* The default implementation of ImageListener which handles basic functionality
* of showing a default image until the network response is received, at which point
* it will switch to either the actual image or the error image.
* @param view The imageView that the listener is associated with.
* @param defaultImageResId Default image resource ID to use, or 0 if it doesn't exist.
* @param errorImageResId Error image resource ID to use, or 0 if it doesn't exist.
*/
public static ImageListener getImageListener(final ImageView view,
final int defaultImageResId, final int errorImageResId) {
return new ImageListener() {
@Override
public void onErrorResponse(VolleyError error) {
if (errorImageResId != 0) {
view.setImageResource(errorImageResId);
}
}
@Override
public void onResponse(ImageContainer response, boolean isImmediate) {
if (response.getBitmap() != null) {
view.setImageBitmap(response.getBitmap());
} else if (defaultImageResId != 0) {
view.setImageResource(defaultImageResId);
}
}
};
}
/**
* Interface for the response handlers on image requests.
*
* The call flow is this:
* 1. Upon being attached to a request, onResponse(response, true) will
* be invoked to reflect any cached data that was already available. If the
* data was available, response.getBitmap() will be non-null.
*
* 2. After a network response returns, only one of the following cases will happen:
* - onResponse(response, false) will be called if the image was loaded.
* or
* - onErrorResponse will be called if there was an error loading the image.
*/
public interface ImageListener extends ErrorListener {
/**
* Listens for non-error changes to the loading of the image request.
*
* @param response Holds all information pertaining to the request, as well
* as the bitmap (if it is loaded).
* @param isImmediate True if this was called during ImageLoader.get() variants.
* This can be used to differentiate between a cached image loading and a network
* image loading in order to, for example, run an animation to fade in network loaded
* images.
*/
public void onResponse(ImageContainer response, boolean isImmediate);
}
/**
* Checks if the item is available in the cache.
* @param requestUrl The url of the remote image
* @param maxWidth The maximum width of the returned image.
* @param maxHeight The maximum height of the returned image.
* @return True if the item exists in cache, false otherwise.
*/
public boolean isCached(String requestUrl, int maxWidth, int maxHeight) {
return isCached(requestUrl, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
}
/**
* Checks if the item is available in the cache.
*
* @param requestUrl The url of the remote image
* @param maxWidth The maximum width of the returned image.
* @param maxHeight The maximum height of the returned image.
* @param scaleType The scaleType of the imageView.
* @return True if the item exists in cache, false otherwise.
*/
public boolean isCached(String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType) {
throwIfNotOnMainThread();
String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
return mCache.getBitmap(cacheKey) != null;
}
/**
* Returns an ImageContainer for the requested URL.
*
* The ImageContainer will contain either the specified default bitmap or the loaded bitmap.
* If the default was returned, the {@link ImageLoader} will be invoked when the
* request is fulfilled.
*
* @param requestUrl The URL of the image to be loaded.
*/
public ImageContainer get(String requestUrl, final ImageListener listener) {
return get(requestUrl, listener, 0, 0);
}
/**
* Equivalent to calling {@link #get(String, ImageListener, int, int, ScaleType)} with
* {@code Scaletype == ScaleType.CENTER_INSIDE}.
*/
public ImageContainer get(String requestUrl, ImageListener imageListener,
int maxWidth, int maxHeight) {
return get(requestUrl, imageListener, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
}
/**
* Issues a bitmap request with the given URL if that image is not available
* in the cache, and returns a bitmap container that contains all of the data
* relating to the request (as well as the default image if the requested
* image is not available).
* @param requestUrl The url of the remote image
* @param imageListener The listener to call when the remote image is loaded
* @param maxWidth The maximum width of the returned image.
* @param maxHeight The maximum height of the returned image.
* @param scaleType The ImageViews ScaleType used to calculate the needed image size.
* @return A container object that contains all of the properties of the request, as well as
* the currently available image (default if remote is not loaded).
*/
public ImageContainer get(String requestUrl, ImageListener imageListener,
int maxWidth, int maxHeight, ScaleType scaleType) {
// only fulfill requests that were initiated from the main thread.
throwIfNotOnMainThread();
final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
// Try to look up the request in the cache of remote images.
Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
if (cachedBitmap != null) {
// Return the cached bitmap.
ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
imageListener.onResponse(container, true);
return container;
}
// The bitmap did not exist in the cache, fetch it!
ImageContainer imageContainer =
new ImageContainer(null, requestUrl, cacheKey, imageListener);
// Update the caller to let them know that they should use the default bitmap.
imageListener.onResponse(imageContainer, true);
// Check to see if a request is already in-flight.
BatchedImageRequest request = mInFlightRequests.get(cacheKey);
if (request != null) {
// If it is, add this request to the list of listeners.
request.addContainer(imageContainer);
return imageContainer;
}
// The request is not already in flight. Send the new request to the network and
// track it.
Request newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
cacheKey);
mRequestQueue.add(newRequest);
mInFlightRequests.put(cacheKey,
new BatchedImageRequest(newRequest, imageContainer));
return imageContainer;
}
protected Request makeImageRequest(String requestUrl, int maxWidth, int maxHeight,
ScaleType scaleType, final String cacheKey) {
return new ImageRequest(requestUrl, new Listener() {
@Override
public void onResponse(Bitmap response) {
onGetImageSuccess(cacheKey, response);
}
}, maxWidth, maxHeight, scaleType, Config.RGB_565, new ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onGetImageError(cacheKey, error);
}
});
}
/**
* Sets the amount of time to wait after the first response arrives before delivering all
* responses. Batching can be disabled entirely by passing in 0.
* @param newBatchedResponseDelayMs The time in milliseconds to wait.
*/
public void setBatchedResponseDelay(int newBatchedResponseDelayMs) {
mBatchResponseDelayMs = newBatchedResponseDelayMs;
}
/**
* Handler for when an image was successfully loaded.
* @param cacheKey The cache key that is associated with the image request.
* @param response The bitmap that was returned from the network.
*/
protected void onGetImageSuccess(String cacheKey, Bitmap response) {
// cache the image that was fetched.
mCache.putBitmap(cacheKey, response);
// remove the request from the list of in-flight requests.
BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
if (request != null) {
// Update the response bitmap.
request.mResponseBitmap = response;
// Send the batched response
batchResponse(cacheKey, request);
}
}
/**
* Handler for when an image failed to load.
* @param cacheKey The cache key that is associated with the image request.
*/
protected void onGetImageError(String cacheKey, VolleyError error) {
// Notify the requesters that something failed via a null result.
// Remove this request from the list of in-flight requests.
BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
if (request != null) {
// Set the error for this request
request.setError(error);
// Send the batched response
batchResponse(cacheKey, request);
}
}
/**
* Container object for all of the data surrounding an image request.
*/
public class ImageContainer {
/**
* The most relevant bitmap for the container. If the image was in cache, the
* Holder to use for the final bitmap (the one that pairs to the requested URL).
*/
private Bitmap mBitmap;
private final ImageListener mListener;
/** The cache key that was associated with the request */
private final String mCacheKey;
/** The request URL that was specified */
private final String mRequestUrl;
/**
* Constructs a BitmapContainer object.
* @param bitmap The final bitmap (if it exists).
* @param requestUrl The requested URL for this container.
* @param cacheKey The cache key that identifies the requested URL for this container.
*/
public ImageContainer(Bitmap bitmap, String requestUrl,
String cacheKey, ImageListener listener) {
mBitmap = bitmap;
mRequestUrl = requestUrl;
mCacheKey = cacheKey;
mListener = listener;
}
/**
* Releases interest in the in-flight request (and cancels it if no one else is listening).
*/
public void cancelRequest() {
if (mListener == null) {
return;
}
BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
if (request != null) {
boolean canceled = request.removeContainerAndCancelIfNecessary(this);
if (canceled) {
mInFlightRequests.remove(mCacheKey);
}
} else {
// check to see if it is already batched for delivery.
request = mBatchedResponses.get(mCacheKey);
if (request != null) {
request.removeContainerAndCancelIfNecessary(this);
if (request.mContainers.size() == 0) {
mBatchedResponses.remove(mCacheKey);
}
}
}
}
/**
* Returns the bitmap associated with the request URL if it has been loaded, null otherwise.
*/
public Bitmap getBitmap() {
return mBitmap;
}
/**
* Returns the requested URL for this container.
*/
public String getRequestUrl() {
return mRequestUrl;
}
}
/**
* Wrapper class used to map a Request to the set of active ImageContainer objects that are
* interested in its results.
*/
private class BatchedImageRequest {
/** The request being tracked */
private final Request> mRequest;
/** The result of the request being tracked by this item */
private Bitmap mResponseBitmap;
/** Error if one occurred for this response */
private VolleyError mError;
/** List of all of the active ImageContainers that are interested in the request */
private final LinkedList mContainers = new LinkedList();
/**
* Constructs a new BatchedImageRequest object
* @param request The request being tracked
* @param container The ImageContainer of the person who initiated the request.
*/
public BatchedImageRequest(Request> request, ImageContainer container) {
mRequest = request;
mContainers.add(container);
}
/**
* Set the error for this response
*/
public void setError(VolleyError error) {
mError = error;
}
/**
* Get the error for this response
*/
public VolleyError getError() {
return mError;
}
/**
* Adds another ImageContainer to the list of those interested in the results of
* the request.
*/
public void addContainer(ImageContainer container) {
mContainers.add(container);
}
/**
* Detatches the bitmap container from the request and cancels the request if no one is
* left listening.
* @param container The container to remove from the list
* @return True if the request was canceled, false otherwise.
*/
public boolean removeContainerAndCancelIfNecessary(ImageContainer container) {
mContainers.remove(container);
if (mContainers.size() == 0) {
mRequest.cancel();
return true;
}
return false;
}
}
/**
* Starts the runnable for batched delivery of responses if it is not already started.
* @param cacheKey The cacheKey of the response being delivered.
* @param request The BatchedImageRequest to be delivered.
*/
private void batchResponse(String cacheKey, BatchedImageRequest request) {
mBatchedResponses.put(cacheKey, request);
// If we don't already have a batch delivery runnable in flight, make a new one.
// Note that this will be used to deliver responses to all callers in mBatchedResponses.
if (mRunnable == null) {
mRunnable = new Runnable() {
@Override
public void run() {
for (BatchedImageRequest bir : mBatchedResponses.values()) {
for (ImageContainer container : bir.mContainers) {
// If one of the callers in the batched request canceled the request
// after the response was received but before it was delivered,
// skip them.
if (container.mListener == null) {
continue;
}
if (bir.getError() == null) {
container.mBitmap = bir.mResponseBitmap;
container.mListener.onResponse(container, false);
} else {
container.mListener.onErrorResponse(bir.getError());
}
}
}
mBatchedResponses.clear();
mRunnable = null;
}
};
// Post the runnable.
mHandler.postDelayed(mRunnable, mBatchResponseDelayMs);
}
}
private void throwIfNotOnMainThread() {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new IllegalStateException("ImageLoader must be invoked from the main thread.");
}
}
/**
* Creates a cache key for use with the L1 cache.
* @param url The URL of the request.
* @param maxWidth The max-width of the output.
* @param maxHeight The max-height of the output.
* @param scaleType The scaleType of the imageView.
*/
private static String getCacheKey(String url, int maxWidth, int maxHeight, ScaleType scaleType) {
return new StringBuilder(url.length() + 12).append("#W").append(maxWidth)
.append("#H").append(maxHeight).append("#S").append(scaleType.ordinal()).append(url)
.toString();
}
}