diff options
Diffstat (limited to 'core/src/main/java/com/android/volley/toolbox/ImageRequest.java')
-rw-r--r-- | core/src/main/java/com/android/volley/toolbox/ImageRequest.java | 283 |
1 files changed, 283 insertions, 0 deletions
diff --git a/core/src/main/java/com/android/volley/toolbox/ImageRequest.java b/core/src/main/java/com/android/volley/toolbox/ImageRequest.java new file mode 100644 index 0000000..32b5aa3 --- /dev/null +++ b/core/src/main/java/com/android/volley/toolbox/ImageRequest.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2011 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.graphics.BitmapFactory; +import android.widget.ImageView.ScaleType; +import androidx.annotation.GuardedBy; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import com.android.volley.DefaultRetryPolicy; +import com.android.volley.NetworkResponse; +import com.android.volley.ParseError; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.VolleyLog; + +/** A canned request for getting an image at a given URL and calling back with a decoded Bitmap. */ +public class ImageRequest extends Request<Bitmap> { + /** Socket timeout in milliseconds for image requests */ + public static final int DEFAULT_IMAGE_TIMEOUT_MS = 1000; + + /** Default number of retries for image requests */ + public static final int DEFAULT_IMAGE_MAX_RETRIES = 2; + + /** Default backoff multiplier for image requests */ + public static final float DEFAULT_IMAGE_BACKOFF_MULT = 2f; + + /** Lock to guard mListener as it is cleared on cancel() and read on delivery. */ + private final Object mLock = new Object(); + + @GuardedBy("mLock") + @Nullable + private Response.Listener<Bitmap> mListener; + + private final Config mDecodeConfig; + private final int mMaxWidth; + private final int mMaxHeight; + private final ScaleType mScaleType; + + /** Decoding lock so that we don't decode more than one image at a time (to avoid OOM's) */ + private static final Object sDecodeLock = new Object(); + + /** + * Creates a new image request, decoding to a maximum specified width and height. If both width + * and height are zero, the image will be decoded to its natural size. If one of the two is + * nonzero, that dimension will be clamped and the other one will be set to preserve the image's + * aspect ratio. If both width and height are nonzero, the image will be decoded to be fit in + * the rectangle of dimensions width x height while keeping its aspect ratio. + * + * @param url URL of the image + * @param listener Listener to receive the decoded bitmap + * @param maxWidth Maximum width to decode this bitmap to, or zero for none + * @param maxHeight Maximum height to decode this bitmap to, or zero for none + * @param scaleType The ImageViews ScaleType used to calculate the needed image size. + * @param decodeConfig Format to decode the bitmap to + * @param errorListener Error listener, or null to ignore errors + */ + public ImageRequest( + String url, + Response.Listener<Bitmap> listener, + int maxWidth, + int maxHeight, + ScaleType scaleType, + Config decodeConfig, + @Nullable Response.ErrorListener errorListener) { + super(Method.GET, url, errorListener); + setRetryPolicy( + new DefaultRetryPolicy( + DEFAULT_IMAGE_TIMEOUT_MS, + DEFAULT_IMAGE_MAX_RETRIES, + DEFAULT_IMAGE_BACKOFF_MULT)); + mListener = listener; + mDecodeConfig = decodeConfig; + mMaxWidth = maxWidth; + mMaxHeight = maxHeight; + mScaleType = scaleType; + } + + /** + * For API compatibility with the pre-ScaleType variant of the constructor. Equivalent to the + * normal constructor with {@code ScaleType.CENTER_INSIDE}. + */ + @Deprecated + public ImageRequest( + String url, + Response.Listener<Bitmap> listener, + int maxWidth, + int maxHeight, + Config decodeConfig, + Response.ErrorListener errorListener) { + this( + url, + listener, + maxWidth, + maxHeight, + ScaleType.CENTER_INSIDE, + decodeConfig, + errorListener); + } + + @Override + public Priority getPriority() { + return Priority.LOW; + } + + /** + * Scales one side of a rectangle to fit aspect ratio. + * + * @param maxPrimary Maximum size of the primary dimension (i.e. width for max width), or zero + * to maintain aspect ratio with secondary dimension + * @param maxSecondary Maximum size of the secondary dimension, or zero to maintain aspect ratio + * with primary dimension + * @param actualPrimary Actual size of the primary dimension + * @param actualSecondary Actual size of the secondary dimension + * @param scaleType The ScaleType used to calculate the needed image size. + */ + private static int getResizedDimension( + int maxPrimary, + int maxSecondary, + int actualPrimary, + int actualSecondary, + ScaleType scaleType) { + + // If no dominant value at all, just return the actual. + if ((maxPrimary == 0) && (maxSecondary == 0)) { + return actualPrimary; + } + + // If ScaleType.FIT_XY fill the whole rectangle, ignore ratio. + if (scaleType == ScaleType.FIT_XY) { + if (maxPrimary == 0) { + return actualPrimary; + } + return maxPrimary; + } + + // If primary is unspecified, scale primary to match secondary's scaling ratio. + if (maxPrimary == 0) { + double ratio = (double) maxSecondary / (double) actualSecondary; + return (int) (actualPrimary * ratio); + } + + if (maxSecondary == 0) { + return maxPrimary; + } + + double ratio = (double) actualSecondary / (double) actualPrimary; + int resized = maxPrimary; + + // If ScaleType.CENTER_CROP fill the whole rectangle, preserve aspect ratio. + if (scaleType == ScaleType.CENTER_CROP) { + if ((resized * ratio) < maxSecondary) { + resized = (int) (maxSecondary / ratio); + } + return resized; + } + + if ((resized * ratio) > maxSecondary) { + resized = (int) (maxSecondary / ratio); + } + return resized; + } + + @Override + protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) { + // Serialize all decode on a global lock to reduce concurrent heap usage. + synchronized (sDecodeLock) { + try { + return doParse(response); + } catch (OutOfMemoryError e) { + VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl()); + return Response.error(new ParseError(e)); + } + } + } + + /** The real guts of parseNetworkResponse. Broken out for readability. */ + private Response<Bitmap> doParse(NetworkResponse response) { + byte[] data = response.data; + BitmapFactory.Options decodeOptions = new BitmapFactory.Options(); + Bitmap bitmap = null; + if (mMaxWidth == 0 && mMaxHeight == 0) { + decodeOptions.inPreferredConfig = mDecodeConfig; + bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); + } else { + // If we have to resize this image, first get the natural bounds. + decodeOptions.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); + int actualWidth = decodeOptions.outWidth; + int actualHeight = decodeOptions.outHeight; + + // Then compute the dimensions we would ideally like to decode to. + int desiredWidth = + getResizedDimension( + mMaxWidth, mMaxHeight, actualWidth, actualHeight, mScaleType); + int desiredHeight = + getResizedDimension( + mMaxHeight, mMaxWidth, actualHeight, actualWidth, mScaleType); + + // Decode to the nearest power of two scaling factor. + decodeOptions.inJustDecodeBounds = false; + // TODO(ficus): Do we need this or is it okay since API 8 doesn't support it? + // decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED; + decodeOptions.inSampleSize = + findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight); + Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); + + // If necessary, scale down to the maximal acceptable size. + if (tempBitmap != null + && (tempBitmap.getWidth() > desiredWidth + || tempBitmap.getHeight() > desiredHeight)) { + bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desiredHeight, true); + tempBitmap.recycle(); + } else { + bitmap = tempBitmap; + } + } + + if (bitmap == null) { + return Response.error(new ParseError(response)); + } else { + return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response)); + } + } + + @Override + public void cancel() { + super.cancel(); + synchronized (mLock) { + mListener = null; + } + } + + @Override + protected void deliverResponse(Bitmap response) { + Response.Listener<Bitmap> listener; + synchronized (mLock) { + listener = mListener; + } + if (listener != null) { + listener.onResponse(response); + } + } + + /** + * Returns the largest power-of-two divisor for use in downscaling a bitmap that will not result + * in the scaling past the desired dimensions. + * + * @param actualWidth Actual width of the bitmap + * @param actualHeight Actual height of the bitmap + * @param desiredWidth Desired width of the bitmap + * @param desiredHeight Desired height of the bitmap + */ + @VisibleForTesting + static int findBestSampleSize( + int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) { + double wr = (double) actualWidth / desiredWidth; + double hr = (double) actualHeight / desiredHeight; + double ratio = Math.min(wr, hr); + float n = 1.0f; + while ((n * 2) <= ratio) { + n *= 2; + } + + return (int) n; + } +} |