Volley网络图片加载控件NetworkImageView
在Volley图片加载ImageLoader一文中了解了使用ImageLoader进行图片加载的原理。还有更简单的网络图片加载,在Volley加载网络图片一文第3节提到NetworkImageView,继承自ImageView通过简短的200行代码,结合ImageLoader实现一个简单易用的图片加载控件。
让我们通过源码了解NetworkImageView的实现,开发者也可以自行设计实现一个网络图片加载控件。
1、结构颇析
NetworkImageView代码结构简洁,逻辑清晰明了。是开发者学习自定义View的典范。
1.1、构造方法
在View的四个构造函数一文中就提到过View的四种构造方法,自定义View至少要实现前三种构造方法。NetworkImageView中展示的正是标准的构造方法实现:
public NetworkImageView(Context context) {
this(context, null);
}
public NetworkImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public NetworkImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
1.2、图片相关变量
内部定了三个图片加载变量:网络图片地址mUrl、默认图片资源mDefaultImageId、网络图片加载失败显示mErrorImageId。
/** The URL of the network image to load */
private String mUrl;
/** Resource ID of the image to be used as a placeholder until the network image is loaded. */
private int mDefaultImageId;
/** Resource ID of the image to be used if the network response fails. */
private int mErrorImageId;
每个变量都对应一个setter方法,在业务代码中设置对应的值。
public void setImageUrl(String url, ImageLoader imageLoader) {
Threads.throwIfNotOnMainThread();
mUrl = url;
mImageLoader = imageLoader;
// The URL has potentially changed. See if we need to load it.
loadImageIfNecessary(/* isInLayoutPass= */ false);
}
public void setDefaultImageResId(int defaultImage) {
mDefaultImageId = defaultImage;
}
public void setErrorImageResId(int errorImage) {
mErrorImageId = errorImage;
}
1.3、图片加载ImageLoader + ImageContainer
最后两个内部成员:ImageLoader用来进行图片加载,通过setImageUrl()方法同图片URL地址一起设置。另一个是图片加载请求容器包含当前请求及对应的ImageView。
/** Local copy of the ImageLoader. */
private ImageLoader mImageLoader;
/** Current ImageContainer. (either in-flight or finished) */
private ImageContainer mImageContainer;
2、图片加载流程
调用NetworkImageView的setImageUrl(String url, ImageLoader imageLoader)方法,设置网络图片加载地址之后,内部随后就通过loadImageIfNecessary(boolean isInLayoutPass)开启图片加载流程。
为什么有一个boolean参数呢?需要判断当前ImageView是否已经准备好加载图片。该方法有两处调用:一个是设置图片加载Url方法中,另一个在重写View的onLayout(boolean changed, int left, int top, int right, int bottom)方法中调用。
2.1、设置图片地址后
调用loadImageIfNecessary(boolean isInLayoutPass)方法开启网络图片加载,NetworkImageView图片加载主要代码量都在这个方法中。让我们一段一段分解开看:
显示获取ImageView的宽高信息,如果尺寸为0或者暂时无法获取就直接返回。后面在View的onLayout(...)回调中会再次调用方法加载loadImageIfNecessary(true)。
int width = getWidth();
int height = getHeight();
ScaleType scaleType = getScaleType();
boolean wrapWidth = false, wrapHeight = false;
if (getLayoutParams() != null) {
wrapWidth = getLayoutParams().width == LayoutParams.WRAP_CONTENT;
wrapHeight = getLayoutParams().height == LayoutParams.WRAP_CONTENT;
}
// if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content
// view, hold off on loading the image.
boolean isFullyWrapContent = wrapWidth && wrapHeight;
if (width == 0 && height == 0 && !isFullyWrapContent) {
return;
}
判断图片加载Url地址是否为空,如果为空就取消之前的网络请求(如果有的话),给NetworkImageView设置空Url可以取消网络图片的加载。再给当前ImageView设置默认的图片资源(如果有的话)。
// if the URL to be loaded in this view is empty, cancel any old requests and clear the
// currently loaded image.
if (TextUtils.isEmpty(mUrl)) {
if (mImageContainer != null) {
mImageContainer.cancelRequest();
mImageContainer = null;
}
setDefaultImageOrNull();
return;
}
如果之前已经给NetworkImageView设置了网络图片,判断前后两次加载的图片Url是否相同。不一样,那么取消前一个Url的加载任务;一样就返回,避免重复的网络请求。
// if there was an old request in this view, check if it needs to be canceled.
if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
if (mImageContainer.getRequestUrl().equals(mUrl)) {
// if the request is from the same URL, return.
return;
} else {
// if there is a pre-existing request, cancel it if it's fetching a different URL.
mImageContainer.cancelRequest();
setDefaultImageOrNull();
}
}
最后,通过ImageLoader加载图片,这在Volley图片加载ImageLoader一文中已有详细解析。
mImageContainer =
mImageLoader.get(
mUrl,
new ImageListener() {
@Override
public void onErrorResponse(VolleyError error) {
if (mErrorImageId != 0) {
setImageResource(mErrorImageId);
}
}
@Override
public void onResponse(
final ImageContainer response, boolean isImmediate) {
// If this was an immediate response that was delivered inside of a
// layout
// pass do not set the image immediately as it will trigger a
// requestLayout
// inside of a layout. Instead, defer setting the image by posting
// back to
// the main thread.
if (isImmediate && isInLayoutPass) {
post(
new Runnable() {
@Override
public void run() {
onResponse(response, /* isImmediate= */ false);
}
});
return;
}
if (response.getBitmap() != null) {
setImageBitmap(response.getBitmap());
} else if (mDefaultImageId != 0) {
setImageResource(mDefaultImageId);
}
}
},
maxWidth,
maxHeight,
scaleType);
图片加载完毕之后,回调ImageListener的onResponse()方法,根据请求结果设置当前ImageView图像。
public void onResponse(
final ImageContainer response, boolean isImmediate) {
// If this was an immediate response that was delivered inside of a
// layout
// pass do not set the image immediately as it will trigger a
// requestLayout
// inside of a layout. Instead, defer setting the image by posting
// back to
// the main thread.
if (isImmediate && isInLayoutPass) {
post(
new Runnable() {
@Override
public void run() {
onResponse(response, /* isImmediate= */ false);
}
});
return;
}
if (response.getBitmap() != null) {
setImageBitmap(response.getBitmap());
} else if (mDefaultImageId != 0) {
setImageResource(mDefaultImageId);
}
}
2.2、在onLayout()回调中
当View已经测量完毕,准备执行layout流程。在ImageView的onLayout()回调中再次加载图片。loadImageIfNecessary(true)方法逻辑和2.1节中的差不多就不再重复了。
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
loadImageIfNecessary(/* isInLayoutPass= */ true);
}
3、自定义属性完善
在第1.2节中需要开发者自己调用方法设置Url等属性,立马就想到自定义属性,省的代码中调用设置方法,关于自定义属性详见自定义View属性declare-styleable一文。
举个例子,自定义NetworkImageView的三个属性:
<declare-styleable name="NetwokImageView">
<attr name="url" format="string" />
<attr name="default_image" format="reference" />
<attr name="error_image" format="reference" />
</declare-styleable>
就可以在XML布局中直接设置要加载的图片地址和默认资源:
<com.android.volley.toolbox.NetworkImageView
app:url = "www.baidu.com/***.png"
app:default_image = "@drawable/default_image"
app:error_image = "@drawable/error_image"/>
同时需要修改对应的构造方法和加载逻辑,布局中获取对应的值后,加载图片。