aboutsummaryrefslogtreecommitdiff
path: root/core/src/main/java/com/android/volley/toolbox/ImageLoader.java
diff options
context:
space:
mode:
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.java541
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();
+ }
+}