1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.volley.toolbox; 18 19import android.graphics.Bitmap; 20import android.graphics.Bitmap.Config; 21import android.graphics.BitmapFactory; 22import android.widget.ImageView.ScaleType; 23 24import com.android.volley.DefaultRetryPolicy; 25import com.android.volley.NetworkResponse; 26import com.android.volley.ParseError; 27import com.android.volley.Request; 28import com.android.volley.Response; 29import com.android.volley.VolleyLog; 30 31/** 32 * A canned request for getting an image at a given URL and calling 33 * back with a decoded Bitmap. 34 */ 35public class ImageRequest extends Request<Bitmap> { 36 /** Socket timeout in milliseconds for image requests */ 37 public static final int DEFAULT_IMAGE_TIMEOUT_MS = 1000; 38 39 /** Default number of retries for image requests */ 40 public static final int DEFAULT_IMAGE_MAX_RETRIES = 2; 41 42 /** Default backoff multiplier for image requests */ 43 public static final float DEFAULT_IMAGE_BACKOFF_MULT = 2f; 44 45 private final Response.Listener<Bitmap> mListener; 46 private final Config mDecodeConfig; 47 private final int mMaxWidth; 48 private final int mMaxHeight; 49 private ScaleType mScaleType; 50 51 /** Decoding lock so that we don't decode more than one image at a time (to avoid OOM's) */ 52 private static final Object sDecodeLock = new Object(); 53 54 /** 55 * Creates a new image request, decoding to a maximum specified width and 56 * height. If both width and height are zero, the image will be decoded to 57 * its natural size. If one of the two is nonzero, that dimension will be 58 * clamped and the other one will be set to preserve the image's aspect 59 * ratio. If both width and height are nonzero, the image will be decoded to 60 * be fit in the rectangle of dimensions width x height while keeping its 61 * aspect ratio. 62 * 63 * @param url URL of the image 64 * @param listener Listener to receive the decoded bitmap 65 * @param maxWidth Maximum width to decode this bitmap to, or zero for none 66 * @param maxHeight Maximum height to decode this bitmap to, or zero for 67 * none 68 * @param scaleType The ImageViews ScaleType used to calculate the needed image size. 69 * @param decodeConfig Format to decode the bitmap to 70 * @param errorListener Error listener, or null to ignore errors 71 */ 72 public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight, 73 ScaleType scaleType, Config decodeConfig, Response.ErrorListener errorListener) { 74 super(Method.GET, url, errorListener); 75 setRetryPolicy(new DefaultRetryPolicy(DEFAULT_IMAGE_TIMEOUT_MS, DEFAULT_IMAGE_MAX_RETRIES, 76 DEFAULT_IMAGE_BACKOFF_MULT)); 77 mListener = listener; 78 mDecodeConfig = decodeConfig; 79 mMaxWidth = maxWidth; 80 mMaxHeight = maxHeight; 81 mScaleType = scaleType; 82 } 83 84 /** 85 * For API compatibility with the pre-ScaleType variant of the constructor. Equivalent to 86 * the normal constructor with {@code ScaleType.CENTER_INSIDE}. 87 */ 88 @Deprecated 89 public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight, 90 Config decodeConfig, Response.ErrorListener errorListener) { 91 this(url, listener, maxWidth, maxHeight, 92 ScaleType.CENTER_INSIDE, decodeConfig, errorListener); 93 } 94 @Override 95 public Priority getPriority() { 96 return Priority.LOW; 97 } 98 99 /** 100 * Scales one side of a rectangle to fit aspect ratio. 101 * 102 * @param maxPrimary Maximum size of the primary dimension (i.e. width for 103 * max width), or zero to maintain aspect ratio with secondary 104 * dimension 105 * @param maxSecondary Maximum size of the secondary dimension, or zero to 106 * maintain aspect ratio with primary dimension 107 * @param actualPrimary Actual size of the primary dimension 108 * @param actualSecondary Actual size of the secondary dimension 109 * @param scaleType The ScaleType used to calculate the needed image size. 110 */ 111 private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary, 112 int actualSecondary, ScaleType scaleType) { 113 114 // If no dominant value at all, just return the actual. 115 if ((maxPrimary == 0) && (maxSecondary == 0)) { 116 return actualPrimary; 117 } 118 119 // If ScaleType.FIT_XY fill the whole rectangle, ignore ratio. 120 if (scaleType == ScaleType.FIT_XY) { 121 if (maxPrimary == 0) { 122 return actualPrimary; 123 } 124 return maxPrimary; 125 } 126 127 // If primary is unspecified, scale primary to match secondary's scaling ratio. 128 if (maxPrimary == 0) { 129 double ratio = (double) maxSecondary / (double) actualSecondary; 130 return (int) (actualPrimary * ratio); 131 } 132 133 if (maxSecondary == 0) { 134 return maxPrimary; 135 } 136 137 double ratio = (double) actualSecondary / (double) actualPrimary; 138 int resized = maxPrimary; 139 140 // If ScaleType.CENTER_CROP fill the whole rectangle, preserve aspect ratio. 141 if (scaleType == ScaleType.CENTER_CROP) { 142 if ((resized * ratio) < maxSecondary) { 143 resized = (int) (maxSecondary / ratio); 144 } 145 return resized; 146 } 147 148 if ((resized * ratio) > maxSecondary) { 149 resized = (int) (maxSecondary / ratio); 150 } 151 return resized; 152 } 153 154 @Override 155 protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) { 156 // Serialize all decode on a global lock to reduce concurrent heap usage. 157 synchronized (sDecodeLock) { 158 try { 159 return doParse(response); 160 } catch (OutOfMemoryError e) { 161 VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl()); 162 return Response.error(new ParseError(e)); 163 } 164 } 165 } 166 167 /** 168 * The real guts of parseNetworkResponse. Broken out for readability. 169 */ 170 private Response<Bitmap> doParse(NetworkResponse response) { 171 byte[] data = response.data; 172 BitmapFactory.Options decodeOptions = new BitmapFactory.Options(); 173 Bitmap bitmap = null; 174 if (mMaxWidth == 0 && mMaxHeight == 0) { 175 decodeOptions.inPreferredConfig = mDecodeConfig; 176 bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); 177 } else { 178 // If we have to resize this image, first get the natural bounds. 179 decodeOptions.inJustDecodeBounds = true; 180 BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); 181 int actualWidth = decodeOptions.outWidth; 182 int actualHeight = decodeOptions.outHeight; 183 184 // Then compute the dimensions we would ideally like to decode to. 185 int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight, 186 actualWidth, actualHeight, mScaleType); 187 int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth, 188 actualHeight, actualWidth, mScaleType); 189 190 // Decode to the nearest power of two scaling factor. 191 decodeOptions.inJustDecodeBounds = false; 192 // TODO(ficus): Do we need this or is it okay since API 8 doesn't support it? 193 // decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED; 194 decodeOptions.inSampleSize = 195 findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight); 196 Bitmap tempBitmap = 197 BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); 198 199 // If necessary, scale down to the maximal acceptable size. 200 if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth || 201 tempBitmap.getHeight() > desiredHeight)) { 202 bitmap = Bitmap.createScaledBitmap(tempBitmap, 203 desiredWidth, desiredHeight, true); 204 tempBitmap.recycle(); 205 } else { 206 bitmap = tempBitmap; 207 } 208 } 209 210 if (bitmap == null) { 211 return Response.error(new ParseError(response)); 212 } else { 213 return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response)); 214 } 215 } 216 217 @Override 218 protected void deliverResponse(Bitmap response) { 219 mListener.onResponse(response); 220 } 221 222 /** 223 * Returns the largest power-of-two divisor for use in downscaling a bitmap 224 * that will not result in the scaling past the desired dimensions. 225 * 226 * @param actualWidth Actual width of the bitmap 227 * @param actualHeight Actual height of the bitmap 228 * @param desiredWidth Desired width of the bitmap 229 * @param desiredHeight Desired height of the bitmap 230 */ 231 // Visible for testing. 232 static int findBestSampleSize( 233 int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) { 234 double wr = (double) actualWidth / desiredWidth; 235 double hr = (double) actualHeight / desiredHeight; 236 double ratio = Math.min(wr, hr); 237 float n = 1.0f; 238 while ((n * 2) <= ratio) { 239 n *= 2; 240 } 241 242 return (int) n; 243 } 244} 245