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对加载状态进行监听,设置加载中的动画。
到此,整个需求就完成了。