使用Glide实现在非WiFi环境手动点击下载图片(判断Glide是否缓存了图片)

1、概述

Glide作为Google推荐的一套快速高效的图片加载框架,有很多人都在使用,我也不例外。不过在项目的需求中,难免会遇到一个这样的需求:在非WiFi环境下,需要手动点击才能下载图片
这初步实现起来是很简单的,但一些细节却不好解决。比如,在使用移动数据的情况下,我不能去自动加载图片,但已经缓存过的图片我们得让他自动显示出来。这个时候我们会发现,Glide没有直接的、明确的接口去立马判断某图片(Url等)是否已经缓存了。
为了实现这个功能,我们就只能自己去Glide的缓存目录寻找那图片是否已经缓存下来了,但Glide缓存文件名字是不会是Url或图片文件名,因此我们得采取一些其他手段。

若我的思路中有如何错误,欢迎指正。

另外,在此感谢郭神的Glide解析,为我解决这个问题提供帮助(博客指路:Android图片加载框架最全解析(三),深入探究Glide的缓存机制

2、实现

(PS. 以下代码是对在非WiFi环境下,需要手动点击才能下载图片这一需求的完全实现)

(1)、初步的判断

    /**
     * 加载图片
     *
     * @param context      加载这个行为所处的Activity或Fragment
     * @param url          图片的网址
     * @param imageView    加载到哪个ImageView上
     * @param widthPixels  图片的宽
     * @param heightPixels 图片的高
     */
    public static void loadNetworkImage(final Context context, final String url, final ImageView imageView, final int widthPixels, final int heightPixels) {
        if (isLoadFailed(imageView)) { // 判断是否是正在加载中的ImageView
            return;
        }
        imageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (isLoadFailed(imageView)) { // 判断是否加载失败
                    loadImage(context, url, imageView, widthPixels, heightPixels);
                } else if (!isLoading(imageView) && sOnImageViewClickListener != null) { // 判断是否加载中 与 是否有设置监听
                    sOnImageViewClickListener.onClick(v);
                }
            }
        });
        if (AppUtil.loadIsManualLoadPhotoInNotWiFi(context) && !NetworkUtil.isWifi()) { // 判断用户是否开启了 在非WiFi环境下,需要手动点击才能下载图片,后在判断是否处于WiFi中
            loadCacheImage(context, url, imageView, widthPixels, heightPixels);
            return;
        }
        loadImage(context, url, imageView, widthPixels, heightPixels);
    }

这里的第一个if判断是为了不让ImageView在RecyclerView等控件中,应滑动关系而不断刷新布局而设定的,避免多次对同一控件调用加载图片。

中间的OnClickListener自然是为了让图片加载失败后可以重新加载,或者是给用户手动点击加载而用,其中里面第二个if判断是为了解决在加载成功后,还需要对图片控件进行点击,从而进行其他逻辑(如QQ里对加载完成的图片进行查看大图)。

这里需要注意,在每个Activity或Fragment被销毁的时候,需要清空正在加载中的图片集合与置空图片点击监听,避免出现问题。置空代码与判断代码如下:

    /**
     * 正在加载中的控件集合
     */
    private static Set<ImageView> sImageViews = new HashSet<>();

    /**
     * 加载失败所显示的图片
     */
    private static Drawable sErrorImg = ViewUtil.getDrawable(R.mipmap.img_default);

    /**
     * 判断图片是否加载失败
     */
    private static boolean isLoadFailed(ImageView imageView) {
        return sErrorImg.equals(imageView.getDrawable());
    }

    /**
     * 判断图片是否在加载中
     */
    private static boolean isLoading(ImageView imageView) {
        return sImageViews.contains(imageView);
    }

    /**
     * 取消所有图片加载,并置空监听
     */
    public static void clearLoadingImg() {
        for (ImageView imageView : sImageViews) {
            Glide.clear(imageView);
        }
        sImageViews.clear();
        sOnImageViewClickListener = null;
    }

(2)、判断图片是否已经缓存了

    private static void loadCacheImage(Context context, String url, ImageView imageView, int widthPixels, int heightPixels) {
        // 寻找缓存图片
        File file = DiskLruCacheWrapper.get(Glide.getPhotoCacheDir(context), 250 * 1024 * 1024).get(new OriginalKey(url, EmptySignature.obtain()));
        if (file != null) {
            loadImage(context, url, imageView, widthPixels, heightPixels);
        } else {
            imageView.setImageDrawable(sErrorImg);
            sImageViews.remove(imageView);
        }
    }

这里就是这个需求中最大的难点了,如何去判断Glide已经缓存了该图片。

通过阅读、分析Glide的源码(也可以选择去看郭神的Glide解析,网址也在上面发了),我们可以知道,Glide对其缓存Key的构建是比较复杂的,有着十多个参数。但我们也会发现,Glide所缓存的原图的Key实际用到的参数只有两个url和signature,而大多数情况下,signature只会是个空值。因此,我们只需要想办法把url转化成Glide的缓存Key就大功告成了。
不过通过源码我们也知道,原图的缓存Key类——OriginalKey是缺省的,也就是所,我们这些外部应用是无法使用。于是我就采用了一种取巧的方法,直接将Glide的这个类复制到自己的项目里,该类代码如下:

    /**
     * Glide原图缓存Key
     */
    private static class OriginalKey implements Key {

        private final String id;
        private final Key signature;

        private OriginalKey(String id, Key signature) {
            this.id = id;
            this.signature = signature;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            OriginalKey that = (OriginalKey) o;

            return id.equals(that.id) && signature.equals(that.signature);
        }

        @Override
        public int hashCode() {
            int result = id.hashCode();
            result = 31 * result + signature.hashCode();
            return result;
        }

        @Override
        public void updateDiskCacheKey(MessageDigest messageDigest) throws UnsupportedEncodingException {
            messageDigest.update(id.getBytes(STRING_CHARSET_NAME));
            signature.updateDiskCacheKey(messageDigest);
        }
    }

这类里有两个参数,第一个就是我们图片的Url,第二个则是签名,用于方面标识图片是否需要更新的,而这里,我是直接选择调用EmptySignature.obtain(),传入一个空签名。
在得到原图的缓存Key之后,我们就只需要得到Glide的磁盘缓存了。

通过DiskLruCacheWrapper.get(File directory, int maxSize)方法,我们就可以得到Glide的磁盘缓存对象DiskCache了(在这里建议传入Glide的缓存目录与250M。虽说Glide有进行判断,如果缓存对象以存在就直接把已存在的返回过来,但为了避免出现什么莫名的异常,就按着Glide源码里的调用方式使用)。
接着通过DiskCache.get(Key key)方法去获得缓存图片文件。

在找到缓存文件后为什么不直接使用缓存文件加载呢,则是为了避免Glide的二次缓存。我具体的Glide加载图片方法都是用的同一个,里面都是设定进行缓存,如果这里传入的是缓存文件,就会导致二次缓存,浪费!

(3)、具体的Glide加载图片方法

    private static void loadImage(final Context context, Object url, final ImageView imageView, int widthPixels, int heightPixels) {
        DrawableRequestBuilder<Object> builder = Glide.with(context)
                .load(url)
                .placeholder(sLoadingImg)
                .error(sErrorImg)
                .diskCacheStrategy(DiskCacheStrategy.SOURCE) // 设置磁盘缓存只缓存原图
                .skipMemoryCache(false) //进行内存缓存
                .centerCrop();
        if (widthPixels != 0 || heightPixels != 0) {
            builder.override(widthPixels, heightPixels);
        }
        builder.into(new GlideDrawableImageViewTarget(imageView) {
            @Override
            public void onLoadStarted(Drawable placeholder) {
                super.onLoadStarted(placeholder);
                Animation rotationAnimation = AnimationUtils.loadAnimation(context, R.anim.rotate_loading);
                imageView.startAnimation(rotationAnimation);
                sImageViews.add(imageView); // 将其添加到 正在加载中的控件集合 中
            }

            @Override
            public void onLoadFailed(Exception e, Drawable errorDrawable) {
                super.onLoadFailed(e, errorDrawable);
                removeViewInLoadingSet();
            }

            @Override
            public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> animation) {
                super.onResourceReady(resource, animation);
                removeViewInLoadingSet();
            }

            /**
             * 将 已加载完成/加载失败的控件 从 正在加载中的控件集合 中移除
             */
            private void removeViewInLoadingSet() {
                imageView.clearAnimation();
                sImageViews.remove(imageView);
            }
        });
    }

最后这里就是Glide加载的具体实现了,里面我通过GlideDrawableImageViewTarget对加载状态进行监听,设置加载中的动画。

到此,整个需求就完成了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章