diff options
Diffstat (limited to 'core/src/main/java/com/android/volley/toolbox/ImageLoader.java')
-rw-r--r-- | core/src/main/java/com/android/volley/toolbox/ImageLoader.java | 541 |
1 files changed, 541 insertions, 0 deletions
diff --git a/core/src/main/java/com/android/volley/toolbox/ImageLoader.java b/core/src/main/java/com/android/volley/toolbox/ImageLoader.java new file mode 100644 index 0000000..eece2cf --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/ImageLoader.java @@ -0,0 +1,541 @@ +/* + * 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 androidx.annotation.MainThread; +import androidx.annotation.Nullable; +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.ResponseDelivery; +import com.android.volley.VolleyError; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * Helper that handles loading and caching images from remote URLs. + * + * <p>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 thread, and all + * responses will be delivered to the main thread as well. Custom {@link ResponseDelivery}s which + * don't use the main thread are not supported. + */ +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<String, BatchedImageRequest> mInFlightRequests = new HashMap<>(); + + /** HashMap of the currently pending responses (waiting to be delivered). */ + private final HashMap<String, BatchedImageRequest> 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 { + @Nullable + Bitmap getBitmap(String url); + + 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. + * + * <p>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. + * + * <p>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. + */ + 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. + * + * <p>Must be called from the main thread. + * + * @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. + */ + @MainThread + public boolean isCached(String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType) { + Threads.throwIfNotOnMainThread(); + + String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType); + return mCache.getBitmap(cacheKey) != null; + } + + /** + * Returns an ImageContainer for the requested URL. + * + * <p>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, /* maxWidth= */ 0, /* maxHeight= */ 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). + * + * <p>Must be called from the main thread. + * + * @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). + */ + @MainThread + public ImageContainer get( + String requestUrl, + ImageListener imageListener, + int maxWidth, + int maxHeight, + ScaleType scaleType) { + + // only fulfill requests that were initiated from the main thread. + Threads.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, /* cacheKey= */ null, /* listener= */ 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 or completed but pending batch delivery. + BatchedImageRequest request = mInFlightRequests.get(cacheKey); + if (request == null) { + request = mBatchedResponses.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<Bitmap> newRequest = + makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey); + + mRequestQueue.add(newRequest); + mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer)); + return imageContainer; + } + + protected Request<Bitmap> makeImageRequest( + String requestUrl, + int maxWidth, + int maxHeight, + ScaleType scaleType, + final String cacheKey) { + return new ImageRequest( + requestUrl, + new Listener<Bitmap>() { + @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). + * + * <p>Must be called from the main thread. + */ + @MainThread + public void cancelRequest() { + Threads.throwIfNotOnMainThread(); + + 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 static 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 List<ImageContainer> mContainers = new ArrayList<>(); + + /** + * 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); + } + + /** + * Detaches 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); + } + } + + /** + * 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(); + } +} |