1 /** 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * <p>http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * <p>Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 * express or implied. See the License for the specific language governing permissions and 12 * limitations under the License. 13 */ 14 package com.android.volley.toolbox; 15 16 import android.content.Context; 17 import android.graphics.Bitmap; 18 import android.graphics.drawable.Drawable; 19 import android.text.TextUtils; 20 import android.util.AttributeSet; 21 import android.view.ViewGroup.LayoutParams; 22 import android.widget.ImageView; 23 import androidx.annotation.MainThread; 24 import androidx.annotation.Nullable; 25 import com.android.volley.VolleyError; 26 import com.android.volley.toolbox.ImageLoader.ImageContainer; 27 import com.android.volley.toolbox.ImageLoader.ImageListener; 28 29 /** Handles fetching an image from a URL as well as the life-cycle of the associated request. */ 30 public class NetworkImageView extends ImageView { 31 /** The URL of the network image to load */ 32 private String mUrl; 33 34 /** 35 * Resource ID of the image to be used as a placeholder until the network image is loaded. Won't 36 * be set at the same time as mDefaultImageDrawable or mDefaultImageBitmap. 37 */ 38 private int mDefaultImageId; 39 40 /** 41 * Drawable of the image to be used as a placeholder until the network image is loaded. Won't be 42 * set at the same time as mDefaultImageId or mDefaultImageBitmap. 43 */ 44 @Nullable private Drawable mDefaultImageDrawable; 45 46 /** 47 * Bitmap of the image to be used as a placeholder until the network image is loaded. Won't be 48 * set at the same time as mDefaultImageId or mDefaultImageDrawable. 49 */ 50 @Nullable private Bitmap mDefaultImageBitmap; 51 52 /** 53 * Resource ID of the image to be used if the network response fails. Won't be set at the same 54 * time as mErrorImageDrawable or mErrorImageBitmap. 55 */ 56 private int mErrorImageId; 57 58 /** 59 * Bitmap of the image to be used if the network response fails. Won't be set at the same time 60 * as mErrorImageId or mErrorImageBitmap. 61 */ 62 @Nullable private Drawable mErrorImageDrawable; 63 64 /** 65 * Bitmap of the image to be used if the network response fails. Won't be set at the same time 66 * as mErrorImageId or mErrorImageDrawable. 67 */ 68 @Nullable private Bitmap mErrorImageBitmap; 69 70 /** Local copy of the ImageLoader. */ 71 private ImageLoader mImageLoader; 72 73 /** Current ImageContainer. (either in-flight or finished) */ 74 private ImageContainer mImageContainer; 75 NetworkImageView(Context context)76 public NetworkImageView(Context context) { 77 this(context, null); 78 } 79 NetworkImageView(Context context, AttributeSet attrs)80 public NetworkImageView(Context context, AttributeSet attrs) { 81 this(context, attrs, 0); 82 } 83 NetworkImageView(Context context, AttributeSet attrs, int defStyle)84 public NetworkImageView(Context context, AttributeSet attrs, int defStyle) { 85 super(context, attrs, defStyle); 86 } 87 88 /** 89 * Sets URL of the image that should be loaded into this view. Note that calling this will 90 * immediately either set the cached image (if available) or the default image specified by 91 * {@link NetworkImageView#setDefaultImageResId(int)} on the view. 92 * 93 * <p>NOTE: If applicable, {@link NetworkImageView#setDefaultImageResId(int)} or {@link 94 * NetworkImageView#setDefaultImageBitmap} and {@link NetworkImageView#setErrorImageResId(int)} 95 * or {@link NetworkImageView#setErrorImageBitmap(Bitmap)} should be called prior to calling 96 * this function. 97 * 98 * <p>Must be called from the main thread. 99 * 100 * @param url The URL that should be loaded into this ImageView. 101 * @param imageLoader ImageLoader that will be used to make the request. 102 */ 103 @MainThread setImageUrl(String url, ImageLoader imageLoader)104 public void setImageUrl(String url, ImageLoader imageLoader) { 105 Threads.throwIfNotOnMainThread(); 106 mUrl = url; 107 mImageLoader = imageLoader; 108 // The URL has potentially changed. See if we need to load it. 109 loadImageIfNecessary(/* isInLayoutPass= */ false); 110 } 111 112 /** 113 * Sets the default image resource ID to be used for this view until the attempt to load it 114 * completes. 115 * 116 * <p>This will clear anything set by {@link NetworkImageView#setDefaultImageBitmap} or {@link 117 * NetworkImageView#setDefaultImageDrawable}. 118 */ setDefaultImageResId(int defaultImage)119 public void setDefaultImageResId(int defaultImage) { 120 mDefaultImageBitmap = null; 121 mDefaultImageDrawable = null; 122 mDefaultImageId = defaultImage; 123 } 124 125 /** 126 * Sets the default image drawable to be used for this view until the attempt to load it 127 * completes. 128 * 129 * <p>This will clear anything set by {@link NetworkImageView#setDefaultImageResId} or {@link 130 * NetworkImageView#setDefaultImageBitmap}. 131 */ setDefaultImageDrawable(@ullable Drawable defaultImageDrawable)132 public void setDefaultImageDrawable(@Nullable Drawable defaultImageDrawable) { 133 mDefaultImageId = 0; 134 mDefaultImageBitmap = null; 135 mDefaultImageDrawable = defaultImageDrawable; 136 } 137 138 /** 139 * Sets the default image bitmap to be used for this view until the attempt to load it 140 * completes. 141 * 142 * <p>This will clear anything set by {@link NetworkImageView#setDefaultImageResId} or {@link 143 * NetworkImageView#setDefaultImageDrawable}. 144 */ setDefaultImageBitmap(Bitmap defaultImage)145 public void setDefaultImageBitmap(Bitmap defaultImage) { 146 mDefaultImageId = 0; 147 mDefaultImageDrawable = null; 148 mDefaultImageBitmap = defaultImage; 149 } 150 151 /** 152 * Sets the error image resource ID to be used for this view in the event that the image 153 * requested fails to load. 154 * 155 * <p>This will clear anything set by {@link NetworkImageView#setErrorImageBitmap} or {@link 156 * NetworkImageView#setErrorImageDrawable}. 157 */ setErrorImageResId(int errorImage)158 public void setErrorImageResId(int errorImage) { 159 mErrorImageBitmap = null; 160 mErrorImageDrawable = null; 161 mErrorImageId = errorImage; 162 } 163 164 /** 165 * Sets the error image drawable to be used for this view in the event that the image requested 166 * fails to load. 167 * 168 * <p>This will clear anything set by {@link NetworkImageView#setErrorImageResId} or {@link 169 * NetworkImageView#setDefaultImageBitmap}. 170 */ setErrorImageDrawable(@ullable Drawable errorImageDrawable)171 public void setErrorImageDrawable(@Nullable Drawable errorImageDrawable) { 172 mErrorImageId = 0; 173 mErrorImageBitmap = null; 174 mErrorImageDrawable = errorImageDrawable; 175 } 176 177 /** 178 * Sets the error image bitmap to be used for this view in the event that the image requested 179 * fails to load. 180 * 181 * <p>This will clear anything set by {@link NetworkImageView#setErrorImageResId} or {@link 182 * NetworkImageView#setDefaultImageDrawable}. 183 */ setErrorImageBitmap(Bitmap errorImage)184 public void setErrorImageBitmap(Bitmap errorImage) { 185 mErrorImageId = 0; 186 mErrorImageDrawable = null; 187 mErrorImageBitmap = errorImage; 188 } 189 190 /** 191 * Loads the image for the view if it isn't already loaded. 192 * 193 * @param isInLayoutPass True if this was invoked from a layout pass, false otherwise. 194 */ loadImageIfNecessary(final boolean isInLayoutPass)195 void loadImageIfNecessary(final boolean isInLayoutPass) { 196 int width = getWidth(); 197 int height = getHeight(); 198 ScaleType scaleType = getScaleType(); 199 200 boolean wrapWidth = false, wrapHeight = false; 201 if (getLayoutParams() != null) { 202 wrapWidth = getLayoutParams().width == LayoutParams.WRAP_CONTENT; 203 wrapHeight = getLayoutParams().height == LayoutParams.WRAP_CONTENT; 204 } 205 206 // if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content 207 // view, hold off on loading the image. 208 boolean isFullyWrapContent = wrapWidth && wrapHeight; 209 if (width == 0 && height == 0 && !isFullyWrapContent) { 210 return; 211 } 212 213 // if the URL to be loaded in this view is empty, cancel any old requests and clear the 214 // currently loaded image. 215 if (TextUtils.isEmpty(mUrl)) { 216 if (mImageContainer != null) { 217 mImageContainer.cancelRequest(); 218 mImageContainer = null; 219 } 220 setDefaultImageOrNull(); 221 return; 222 } 223 224 // if there was an old request in this view, check if it needs to be canceled. 225 if (mImageContainer != null && mImageContainer.getRequestUrl() != null) { 226 if (mImageContainer.getRequestUrl().equals(mUrl)) { 227 // if the request is from the same URL, return. 228 return; 229 } else { 230 // if there is a pre-existing request, cancel it if it's fetching a different URL. 231 mImageContainer.cancelRequest(); 232 setDefaultImageOrNull(); 233 } 234 } 235 236 // Calculate the max image width / height to use while ignoring WRAP_CONTENT dimens. 237 int maxWidth = wrapWidth ? 0 : width; 238 int maxHeight = wrapHeight ? 0 : height; 239 240 // The pre-existing content of this view didn't match the current URL. Load the new image 241 // from the network. 242 243 // update the ImageContainer to be the new bitmap container. 244 mImageContainer = 245 mImageLoader.get( 246 mUrl, 247 new ImageListener() { 248 @Override 249 public void onErrorResponse(VolleyError error) { 250 if (mErrorImageId != 0) { 251 setImageResource(mErrorImageId); 252 } else if (mErrorImageDrawable != null) { 253 setImageDrawable(mErrorImageDrawable); 254 } else if (mErrorImageBitmap != null) { 255 setImageBitmap(mErrorImageBitmap); 256 } 257 } 258 259 @Override 260 public void onResponse( 261 final ImageContainer response, boolean isImmediate) { 262 // If this was an immediate response that was delivered inside of a 263 // layout 264 // pass do not set the image immediately as it will trigger a 265 // requestLayout 266 // inside of a layout. Instead, defer setting the image by posting 267 // back to 268 // the main thread. 269 if (isImmediate && isInLayoutPass) { 270 post( 271 new Runnable() { 272 @Override 273 public void run() { 274 onResponse(response, /* isImmediate= */ false); 275 } 276 }); 277 return; 278 } 279 280 if (response.getBitmap() != null) { 281 setImageBitmap(response.getBitmap()); 282 } else if (mDefaultImageId != 0) { 283 setImageResource(mDefaultImageId); 284 } else if (mDefaultImageDrawable != null) { 285 setImageDrawable(mDefaultImageDrawable); 286 } else if (mDefaultImageBitmap != null) { 287 setImageBitmap(mDefaultImageBitmap); 288 } 289 } 290 }, 291 maxWidth, 292 maxHeight, 293 scaleType); 294 } 295 setDefaultImageOrNull()296 private void setDefaultImageOrNull() { 297 if (mDefaultImageId != 0) { 298 setImageResource(mDefaultImageId); 299 } else if (mDefaultImageDrawable != null) { 300 setImageDrawable(mDefaultImageDrawable); 301 } else if (mDefaultImageBitmap != null) { 302 setImageBitmap(mDefaultImageBitmap); 303 } else { 304 setImageBitmap(null); 305 } 306 } 307 308 @Override onLayout(boolean changed, int left, int top, int right, int bottom)309 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 310 super.onLayout(changed, left, top, right, bottom); 311 loadImageIfNecessary(/* isInLayoutPass= */ true); 312 } 313 314 @Override onDetachedFromWindow()315 protected void onDetachedFromWindow() { 316 if (mImageContainer != null) { 317 // If the view was bound to an image request, cancel it and clear 318 // out the image from the view. 319 mImageContainer.cancelRequest(); 320 setImageBitmap(null); 321 // also clear out the container so we can reload the image if necessary. 322 mImageContainer = null; 323 } 324 super.onDetachedFromWindow(); 325 } 326 327 @Override drawableStateChanged()328 protected void drawableStateChanged() { 329 super.drawableStateChanged(); 330 invalidate(); 331 } 332 } 333