Volley网络图片加载控件NetworkImageView

Quibbler 2021-3-4 593

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、图片加载流程

        调用NetworkImageViewsetImageUrl(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);

        图片加载完毕之后,回调ImageListeneronResponse()方法,根据请求结果设置当前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"/>

        同时需要修改对应的构造方法和加载逻辑,布局中获取对应的值后,加载图片。



不忘初心的阿甘
最新回复 (0)
    • 安卓笔记本
      2
        登录 注册 QQ
返回
仅供学习交流,切勿用于商业用途。如有错误欢迎指出:fluent0418@gmail.com