Skip to content

Instantly share code, notes, and snippets.

@amondnet
Created November 22, 2013 18:24
Show Gist options
  • Save amondnet/7604555 to your computer and use it in GitHub Desktop.
Save amondnet/7604555 to your computer and use it in GitHub Desktop.
/**
*
* 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.
*/
//reference : https://github.com/njzk2/ExtVolley
package io.farmers.common.volley.image;
import java.util.HashMap;
import java.util.LinkedList;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.os.Handler;
import android.os.Looper;
import android.widget.ImageView;
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 com.android.volley.toolbox.ImageLoader;
import com.android.volley.toolbox.ImageRequest;
/**
* 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 ExtendedImageLoader extends ImageLoader {
/** Creator of Image Requests */
private ImageRequestFactory mImageRequestFactory;
/**
* Constructs a new ImageLoader with the default ImageRequest factory.
*
* @param queue The RequestQueue to use for making image requests.
* @param imageCache The cache to use as an L1 cache.
*/
public ExtendedImageLoader(RequestQueue queue, ImageCache imageCache) {
this(queue, imageCache, new ImageRequestFactory() {
@Override
public Request<?> getImageRequest(String url, Listener<Bitmap> listener, int maxWidth,
int maxHeight, Config config, ErrorListener errorListener) {
return new ImageRequest(url, listener, maxWidth, maxHeight, config, errorListener);
}
});
}
/**
* 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.
* @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).
*/
@Override
public ImageContainer get(String requestUrl, ImageListener imageListener, int maxWidth,
int maxHeight) {
// only fulfill requests that were initiated from the main thread.
throwIfNotOnMainThread();
final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight);
// 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 = mImageRequestFactory.getImageRequest(requestUrl,
new Listener<Bitmap>() {
@Override
public void onResponse(Bitmap response) {
onGetImageSuccess(cacheKey, response);
}
}, maxWidth, maxHeight, Config.RGB_565, new ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onGetImageError(cacheKey, error);
}
});
mRequestQueue.add(newRequest);
mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer));
return imageContainer;
}
/**
* Constructs a new ImageLoader.
*
* @param queue The RequestQueue to use for making image requests.
* @param imageCache The cache to use as an L1 cache.
* @param factory The factory for creating ImageRequests
*/
public ExtendedImageLoader(RequestQueue queue, ImageCache imageCache,
ImageRequestFactory factory) {
super(queue, imageCache);
mImageRequestFactory = factory;
}
}
/**
* 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 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 com.android.volley.toolbox.ImageRequest;
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. */
protected final RequestQueue mRequestQueue;
/**
* Amount of time to wait after first response arrives before delivering all
* responses.
*/
protected int mBatchResponseDelayMs = 100;
/**
* The cache implementation to be used as an L1 cache before calling into
* volley.
*/
protected 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.
*/
protected final HashMap<String, BatchedImageRequest> mInFlightRequests = new HashMap<String, BatchedImageRequest>();
/** HashMap of the currently pending responses (waiting to be delivered). */
protected final HashMap<String, BatchedImageRequest> mBatchedResponses = new HashMap<String, BatchedImageRequest>();
/** Handler to the main thread. */
protected final Handler mHandler = new Handler(Looper.getMainLooper());
/** Runnable for in-flight response delivery. */
protected 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 imageView 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) {
throwIfNotOnMainThread();
String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight);
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.
* @param defaultImage Optional default image to return until the actual
* image is loaded.
*/
public ImageContainer get(String requestUrl, final ImageListener listener) {
return get(requestUrl, listener, 0, 0);
}
/**
* 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.
* @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) {
// only fulfill requests that were initiated from the main thread.
throwIfNotOnMainThread();
final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight);
// 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 = new ImageRequest(requestUrl, new Listener<Bitmap>() {
@Override
public void onResponse(Bitmap response) {
onGetImageSuccess(cacheKey, response);
}
}, maxWidth, maxHeight, Config.RGB_565, new ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onGetImageError(cacheKey, error);
}
});
mRequestQueue.add(newRequest);
mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer));
return imageContainer;
}
/**
* 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);
// Set the error for this request
request.setError(error);
if (request != null) {
// 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.
*/
protected 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<ImageContainer> mContainers = new LinkedList<ImageContainer>();
/**
* 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.
* @param error The volley error associated with the request (if
* applicable).
*/
protected 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);
}
}
protected 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.
*/
protected static String getCacheKey(String url, int maxWidth, int maxHeight) {
return new StringBuilder(url.length() + 12).append("#W").append(maxWidth).append("#H")
.append(maxHeight).append(url).toString();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment