前言
android中图片加载框架有很多,所有框架最终达到的目都是在Android平台上以极度简单的方式加载和展示图片,如果我们每个都拿来学习和使用的话,这样会极大的浪费我们的时间和经历,所以经过我的一番比较之后,我决定用Glide作为我研究和今后使用的对象。经过一个礼拜的学习和研究之后,我发现Glide是真的强大,通过简单的一行代码就可以展示图片,但是其背后所做的是事却是我们没法想象的,想深入去学习源码的可以去看下郭神的Glide最全解析专栏。本人由于精力和实力的不足,源码就不解析了,郭神的那个系列也写的很详细了,自己看了差不多一个礼拜,可能还是有很多不懂的地方,这里说下看的方法,源码看起来肯定很枯燥,一定要有耐心,不能急,不然可能就看不下去了,一遍看不懂可以多看几遍,来回看慢慢看就会有感觉的。郭神的那个系列第二篇是Glide源码的整个流程,特别长,我的建议可以先跳过去,先看后面几篇关于源码的细节部分,看完再回来看整个流程,这样会好一点。不然第二篇不仅量大而且乱很容易走神,这样一来很可能就看不下去直接放弃了,后面几篇先过下留个大致印象再回过来看整体流程这样会容易一点。还有一个就是可能有人会觉得我会用不就好了,我一个开发人员我没事看懂框架源码干嘛,我又不需要写源码,我要会写我还用这个框架干嘛。这个首先你看了可以增长你技术方面的见识,虽然可能能力增长的也不多,但是最起码开开眼界。其次看懂了源码如果在使用框架过程中有些方面不符合需求,你可以快速在源码的基础上做修改来满足我们的需求。最后还有一点就是看了源码之后可以更好的掌握这个框架的用法,了解的更深,对其内部的实现最起码有个大概的了解,不仅知其然,还知其所以然。好了,前面的话就扯这么多了,在本篇博客中只对Glide(基于Glide v4)用法做个总结,尽量把所有用法总结全,若有没涉及到的欢迎评论补充,不胜感激。
进入正题
1. 添加依赖
注意:这里我们要添加两个依赖库,其中compiler库是用于生成Generated API的,后面我们会讲到它。
dependencies {
implementation 'com.github.bumptech.glide:glide:4.8.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.8.0'
}
2. 下载图片需要网络权限,所以在AndroidManifest.xml中声明一下网络权限
<uses-permission android:name="android.permission.INTERNET" />
3. 加载图片
3.1 首先来尝试加载一张网络图片吧,图片地址:http://cn.bing.com/az/hprichbg/rb/Dongdaemun_ZH-CN10736487148_1920x1080.jpg
3.2 布局文件:很简单就一个Button和一个用于展示的ImageView,点击Button之后就去加载该张图片,给按钮添加了个点击的方法doClick。布局用的是ConstraintLayout,不懂的自己可以去学习下,之前我的博客android布局和屏幕适配相关里面也链接了两个大神关于这个布局的博客,我都是直接拖拽出来的。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:onClick="doClick"
android:text="加载图片"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/imageView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button" />
</android.support.constraint.ConstraintLayout>
3.3 为了点击Button的时候能够将刚才的图片显示在ImageView上,需要修改MainActivity中的代码,如下所示:
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import com.bumptech.glide.Glide;
public class MainActivity extends AppCompatActivity {
private ImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView = findViewById(R.id.imageView);
}
public void doClick(View view) {
String url = "http://cn.bing.com/az/hprichbg/rb/Dongdaemun_ZH-CN10736487148_1920x1080.jpg";
Glide.with(this).load(url).into(imageView);
}
}
3.4 就一行代码就搞定了,就是这么简单,我们来运行下程序,运行效果如下图所示: 加载图片.gif
可以看到,一张网络上的图片已经被成功下载,并且展示到ImageView上了。而我们到底做了什么?实际上核心的代码就只有这一行而已:
Glide.with(this).load(url).into(imageView);
千万不要小看这一行代码,实际上仅仅就这一行代码,你已经可以做非常非常多的事情了,包括加载网络上的图片、加载手机本地的图片、加载应用资源中的图片等等。
3.5 下面我们就来详细解析一下这行代码。
- 首先,调用Glide.with()方法用于创建一个加载图片的实例。with()方法可以接收Context、Activity或者Fragment类型的参数。也就是说我们选择的范围非常广,不管是在Activity还是Fragment中调用with()方法,都可以直接传this。那如果调用的地方既不在Activity中也不在Fragment中呢?也没关系,我们可以获取当前应用程序的ApplicationContext,传入到with()方法当中。注意with()方法中传入的实例会决定Glide加载图片的生命周期,如果传入的是Activity或者Fragment的实例,那么当这个Activity或Fragment被销毁的时候,图片加载也会停止。如果传入的是ApplicationContext,那么只有当应用程序被杀掉的时候,图片加载才会停止。
- 接下来看一下load()方法,这个方法用于指定待加载的图片资源。Glide支持加载各种各样的图片资源,包括网络图片、本地图片、应用资源、二进制流、Uri对象等等。因此load()方法也有很多个方法重载,除了我们刚才使用的加载一个字符串网址之外,你还可以这样使用load()方法:
// 加载本地图片
File file = new File(getExternalCacheDir() + "/image.jpg");
Glide.with(this).load(file).into(imageView);
// 加载应用资源
int resource = R.drawable.image;
Glide.with(this).load(resource).into(imageView);
// 加载二进制流
byte[] image = getImageBytes();
Glide.with(this).load(image).into(imageView);
// 加载Uri对象
Uri imageUri = getImageUri();
Glide.with(this).load(imageUri).into(imageView);
- 最后看一下into()方法,这个方法就很简单了,我们希望让图片显示在哪个ImageView上,把这个ImageView的实例传进去就可以了。当然,into()方法不仅仅是只能接收ImageView类型的参数,还支持很多更丰富的用法,不过那个属于高级技巧,我们后面再说。
- 那么回顾一下Glide最基本的使用方式,其实就是关键的三步走:先with(),再load(),最后into()。熟记这三步,你就已经入门Glide了。
3. 6 设置加载动画
- 通常的用法如下,使用api提供的几个常用动画:
Glide.with(this)
.load(url)
//transition(TransitionOptions transitionOptions)
.transition(DrawableTransitionOptions.withCrossFade())
.into(imageView);
- TransitionOptions决定图片加载完成如何从占位符图片(或者之前的图片)过渡。
- 有三种TransitionOptions:
- GenericTransitionOptions 通用型
- DrawableTransitionOptions
- BitmapTransitionOptions
- TransitionOptions是和你要加载的资源的类型绑定的,也就是说,如果你请求一张位图Bitmap,你就需要使用BitmapTransitionOptions,而不是DrawableTransitionOptions。如果既不是Bitmap也不是Drawable就使用GenericTransitionOptions。Glide加载网络图片默认的类型是Drawable,所以加载网络图片我们使用DrawableTransitionOptions即可。
- 如果要使用自定义的动画,可以使用GenericTransitionOptions.with(int viewAnimationId)或者BitmapTransitionOptions.withCrossFade(int animationId, int duration)或者DrawableTransitionOptions.withCrossFade(int animationId, int duration)。
- 出于性能考虑,最好不要在ListView,GridView,RecycleView中使用过渡动画,使用TransitionOptions.dontTransition()可以不加载动画,也可以使用dontAnimate不加载动画。
RequestOptions options = new RequestOptions()
.dontTransform();
3. 7 设置下载优先级
RequestOptions options = new RequestOptions()
.priority(Priority.NORMAL);
4. 占位图
4.1 加载前占位图
- 观察刚才加载网络图片的效果,你会发现,点击了加载图片按钮之后,要稍微等一会图片才会显示出来。这其实很容易理解,因为从网络上下载图片本来就是需要时间的。那么我们有没有办法再优化一下用户体验呢?当然可以,Glide提供了各种各样非常丰富的API支持,其中就包括了占位图功能。
- 顾名思义,占位图就是指在图片的加载过程中,我们先显示一张临时的图片,等图片加载出来了再替换成要加载的图片。
- 下面我们就来学习一下Glide占位图功能的使用方法,我用项目下的ic_launcher.png小机器人作为占位图显示。然后修改Glide加载部分的代码,如下所示:
RequestOptions options = new RequestOptions()
.placeholder(R.mipmap.ic_launcher);
Glide.with(this)
.load(url)
.apply(options)
.into(imageView);
- 没错,就是这么简单。这里我们先创建了一个RequestOptions对象,然后调用它的placeholder()方法来指定占位图,再将占位图片的资源id传入到这个方法中。最后,在Glide的三步走之间加入一个apply()方法,来应用我们刚才创建的RequestOptions对象。
- 不过如果你现在重新运行一下代码并点击Load Image,很可能是根本看不到占位图效果的。因为Glide有非常强大的缓存机制,我们刚才加载图片的时候Glide自动就已经将它缓存下来了,下次加载的时候将会直接从缓存中读取,不会再去网络下载了,因而加载的速度非常快,所以占位图可能根本来不及显示。
- 因此这里我们还需要稍微做一点修改,来让占位图能有机会显示出来,修改代码如下所示:
RequestOptions options = new RequestOptions()
.placeholder(R.mipmap.ic_launcher)
.diskCacheStrategy(DiskCacheStrategy.NONE);
Glide.with(this)
.load(url)
.apply(options)
.into(imageView);
- 可以看到,这里在RequestOptions对象中又串接了一个diskCacheStrategy()方法,并传入DiskCacheStrategy.NONE参数,这样就可以禁用掉Glide的缓存功能。
-关于Glide缓存方面的内容我们待会儿会进行更详细的讲解,这里只是为了测试占位图功能而加的一个额外配置,暂时你只需要知道禁用缓存必须这么写就可以了。 - 现在重新运行一下代码,效果如下图所示: 加载图片1.gif
- 可以看到,当点击加载图片按钮之后会立即显示一张占位图,然后等真正的图片加载完成之后会将占位图替换掉。
4.2 异常占位图
- 当然,这只是占位图的一种,除了这种加载占位图之外,还有一种异常占位图。异常占位图就是指,如果因为某些异常情况导致图片加载失败,比如说手机网络信号不好,这个时候就显示这张异常占位图。
- 异常占位图的用法相信你已经可以猜到了,首先准备一张error.jpg图片,然后修改Glide加载部分的代码,如下所示:
RequestOptions options = new RequestOptions()
.placeholder(R.mipmap.ic_launcher)
.error(R.drawable.error)
.diskCacheStrategy(DiskCacheStrategy.NONE);
Glide.with(this)
.load(url)
.apply(options)
.into(imageView);
- 很简单,这里又串接了一个error()方法就可以指定异常占位图了。
-
现在你可以将图片的url地址修改成一个不存在的图片地址,或者干脆直接将手机的网络给关了,然后重新运行程序,效果如下图所示:
加载图片2.gif
这样我们就把Glide提供的占位图功能都掌握了。
5. 指定图片加载格式
- 我们都知道,Glide其中一个非常亮眼的功能就是可以加载GIF图片,而同样作为非常出色的图片加载框架的Picasso是不支持这个功能的。
- 而且使用Glide加载GIF图并不需要编写什么额外的代码,Glide内部会自动判断图片格式。我们将刚才那段加载图片代码中的URL地址替换成下面的GIF图片的URL地址就可以了。
现在重新运行一下代码,效果如下图所示: 加载图片3.gif
- 也就是说,不管我们传入的是一张普通图片,还是一张GIF图片,Glide都会自动进行判断,并且可以正确地把它解析并展示出来。
- 但是如果我想指定图片的格式该怎么办呢?就比如说,我希望加载的这张图必须是一张静态图片,我不需要Glide自动帮我判断它到底是静图还是GIF图。
- 想实现这个功能仍然非常简单,我们只需要再串接一个新的方法就可以了,代码如下所示:
Glide.with(this)
.asBitmap()
.load(url)
.apply(options)
.into(imageView);
- 可以看到,这里在with()方法的后面加入了一个asBitmap()方法,这个方法的意思就是说这里只允许加载静态图片,不需要Glide去帮我们自动进行图片格式的判断了。如果你传入的还是一张GIF图的话,Glide会展示这张GIF图的第一帧,而不会去播放它。
- 这里注意asBitmap()方法必须跟在with()方法的后面,load()方法的前面,如果你写错了顺序就肯定会报错了。
- 那么类似地,既然我们能强制指定加载静态图片,就也能强制指定加载动态图片,对应的方法是asGif(),代码中只需用asGif()方法去替代asBitmap()方法就好了。如果指定了只能加载动态图片,而传入的图片却是一张静图的话,那么结果自然就只有加载失败,如果指定了异常占位图的话异常占位图会展示出来。
- Glide 4中新增了asFile()方法和asDrawable()方法,分别用于强制指定文件格式的加载和Drawable格式的加载,用法都比较简单,就不再进行演示了。
6. 指定图片大小
- 在这之前,你可能还需要先了解一个概念,就是我们平时在加载图片的时候很容易会造成内存浪费。什么叫内存浪费呢?比如说一张图片的尺寸是1000 * 1000像素,但是我们界面上的ImageView可能只有200 * 200像素,这个时候如果你不对图片进行任何压缩就直接读取到内存中,这就属于内存浪费了,因为程序中根本就用不到这么高像素的图片。
- 关于图片压缩这方面,我之前也有篇文章是这方面的,感兴趣的朋友可以去阅读一下Bitmap高效加载。
- 使用Glide在绝大多数情况下我们都是不需要指定图片大小的,完全不用担心图片内存浪费,甚至是内存溢出的问题。因为Glide从来都不会直接将图片的完整尺寸全部加载到内存中,而是用多少加载多少。Glide会自动判断ImageView的大小,对图片进行压缩,然后只将这么大的图片像素加载到内存当中,帮助我们节省内存开支。
- 不过,如果你真的有这样的需求,必须给图片指定一个固定的大小,Glide仍然是支持这个功能的。修改Glide加载部分的代码,如下所示:
RequestOptions options = new RequestOptions()
.override(100, 100);
Glide.with(this)
.load(url)
.apply(options)
.into(imageView);
- 仍然非常简单,这里使用override()方法指定了一个图片的尺寸。也就是说,Glide现在只会将图片加载成100*100像素的尺寸,而不会管你的ImageView的大小是多少了。
- 如果你想加载一张图片的原始尺寸的话,可以使用Target.SIZE_ORIGINAL关键字,如下所示:
RequestOptions options = new RequestOptions()
override(Target.SIZE_ORIGINAL);
Glide.with(this)
.load(url)
.apply(options)
.into(imageView);
- 这样的话,Glide就不会再去自动压缩图片,而是会去加载图片的原始尺寸。当然,这种写法也会面临着更高的OOM风险。
7. 缓存机制
- Glide的缓存设计可以说是非常先进的,考虑的场景也很周全。在缓存这一功能上,Glide又将它分成了两个模块,一个是内存缓存,一个是硬盘缓存。
- 这两个缓存模块的作用各不相同,内存缓存的主要作用是防止应用重复将图片数据读取到内存当中,而硬盘缓存的主要作用是防止应用重复从网络或其他地方重复下载和读取数据。
- 内存缓存和硬盘缓存的相互结合才构成了Glide极佳的图片缓存效果,那么接下来我们就来分别学习一下这两种缓存的使用方法。
7.1 首先来看内存缓存
- 你要知道,默认情况下,Glide自动就是开启内存缓存的。也就是说,当我们使用Glide加载了一张图片之后,这张图片就会被缓存到内存当中,只要在它还没从内存中被清除之前,下次使用Glide再加载这张图片都会直接从内存当中读取,而不用重新从网络或硬盘上读取了,这样无疑就可以大幅度提升图片的加载效率。比方说你在一个RecyclerView当中反复上下滑动,RecyclerView中只要是Glide加载过的图片都可以直接从内存当中迅速读取并展示出来,从而大大提升了用户体验。
- 而Glide最为人性化的是,你甚至不需要编写任何额外的代码就能自动享受到这个极为便利的内存缓存功能,因为Glide默认就已经将它开启了。
- 那么既然已经默认开启了这个功能,还有什么可讲的用法呢?只有一点,如果你有什么特殊的原因需要禁用内存缓存功能,Glide对此提供了接口:
RequestOptions options = new RequestOptions()
.skipMemoryCache(true);
Glide.with(this)
.load(url)
.apply(options)
.into(imageView);
- 可以看到,只需要调用skipMemoryCache()方法并传入true,就表示禁用掉Glide的内存缓存功能。
7.2 接下来我们开始学习硬盘缓存方面的内容
- 磁盘缓存Glide默认也是开启的,其实在刚刚学习占位图功能的时候,我们就使用过硬盘缓存的功能了。当时为了禁止Glide对图片进行硬盘缓存而使用了如下代码:
RequestOptions options = new RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.NONE);
Glide.with(this)
.load(url)
.apply(options)
.into(imageView);
- 调用diskCacheStrategy()方法并传入DiskCacheStrategy.NONE,就可以禁用掉Glide的硬盘缓存功能了。
- 这个diskCacheStrategy()方法基本上就是Glide硬盘缓存功能的一切,它可以接收五种参数:
- DiskCacheStrategy.NONE: 表示不缓存任何内容。
- DiskCacheStrategy.DATA: 表示只缓存原始图片。
- DiskCacheStrategy.RESOURCE: 表示只缓存转换过后的图片。
- DiskCacheStrategy.ALL : 表示既缓存原始图片,也缓存转换过后的图片。
- DiskCacheStrategy.AUTOMATIC: 表示让Glide根据图片资源智能地选择使用哪一种缓存策略(默认选项)。
- 上面五种参数的解释本身并没有什么难理解的地方,但是关于转换过后的图片这个概念大家可能需要了解一下。就是当我们使用Glide去加载一张图片的时候,Glide默认并不会将原始图片展示出来,而是会对图片进行压缩和变换(我们会在稍后学习这方面的内容)。总之就是经过种种一系列操作之后得到的图片,就叫转换过后的图片。
7.3 清理缓存,注意清理磁盘缓存因为涉及到文件操作不能放主线程中
//清理内存缓存 可以在UI主线程中进行
Glide.get(this).clearMemory();
//清理磁盘缓存 需要在子线程中执行
new Thread(new Runnable() {
@Override
public void run() {
Glide.get(MainActivity.this).clearDiskCache();
}
}).start();
7.4 读取磁盘缓存数据大小
- 有些APP里面比如新闻类APP里面会有手动清理缓存功能,然后会把缓存数据大小显示出来,这个其实就是磁盘缓存数据大小,那么这个我们要如何实现呢,其实很简单,只要读取出Glide磁盘缓存文件下所有文件大小然后把算出所有数据大小之和就好了。
- 这里创建了个Glide磁盘缓存数据的工具类,使用时只需要调用获取磁盘缓存大小的方法并传入Context就可获取到Glide磁盘缓存数据大小了,代码如下:
import android.content.Context;
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
import java.io.File;
import java.math.BigDecimal;
public class GlideDiskCacheUtil {
/**
* 获取Glide造成的磁盘缓存大小
* @return DiskCacheSize
*/
public static String getDiskCacheSize(Context context) {
try {
return getFormatSize(getFolderSize(new File(context.getCacheDir() + "/"+ InternalCacheDiskCacheFactory.DEFAULT_DISK_CACHE_DIR)));
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
/**
* 获取指定文件夹内所有文件大小的和
* @param file file
* @return size
* @throws Exception
*/
private static long getFolderSize(File file) throws Exception {
long size = 0;
try {
File[] fileList = file.listFiles();
for (File aFileList : fileList) {
if (aFileList.isDirectory()) {
size = size + getFolderSize(aFileList);
} else {
size = size + aFileList.length();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return size;
}
/**
* 格式化单位
* @param size size
* @return size
*/
private static String getFormatSize(double size) {
double kiloByte = size / 1024;
if (kiloByte < 1) {
return size + "Byte";
}
double megaByte = kiloByte / 1024;
if (megaByte < 1) {
BigDecimal result1 = new BigDecimal(Double.toString(kiloByte));
return result1.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "KB";
}
double gigaByte = megaByte / 1024;
if (gigaByte < 1) {
BigDecimal result2 = new BigDecimal(Double.toString(megaByte));
return result2.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "MB";
}
double teraBytes = gigaByte / 1024;
if (teraBytes < 1) {
BigDecimal result3 = new BigDecimal(Double.toString(gigaByte));
return result3.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "GB";
}
BigDecimal result4 = new BigDecimal(teraBytes);
return result4.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "TB";
}
}
7.5 高级技巧(图片的url中带用于保护图片资源的变化的token)
- 虽说Glide将缓存功能高度封装之后,使得用法变得非常简单,但同时也带来了一些问题。
- 比如当项目的图片资源是存放在七牛云上面的,而七牛云为了对图片资源进行保护,会在图片url地址的基础之上再加上一个token参数。也就是说,一张图片的url地址可能会是如下格式:
- 而使用Glide加载这张图片的话,也就会使用这个url地址来组成缓存Key。
- 但是接下来问题就来了,token作为一个验证身份的参数并不是一成不变的,很有可能时时刻刻都在变化。而如果token变了,那么图片的url也就跟着变了,图片url变了,缓存Key也就跟着变了。结果就造成了,明明是同一张图片,就因为token不断在改变,导致Glide的缓存功能完全失效了。
- 这其实是个挺棘手的问题,大家在使用Glide的时候很有可能都会遇到这个问题。那么该如何解决这个问题呢?这个时候就体现出了研究源码的作用了,通过研究源码我们可以知道缓存Key是从GlideUrl类中的getCacheKey()方法将图片的url地址直接进行返回得到的,因此我们只需要创建一个MyGlideUrl继承自GlideUrl,在getCacheKey()方法方法中加入一些自己的处理逻辑把url中token这部分参数去掉然后在Glide加载图片时使用我们创建的MyGlideUrl传入url就可以了,代码如下所示:
import com.bumptech.glide.load.model.GlideUrl;
public class MyGlideUrl extends GlideUrl {
private String mUrl;
public MyGlideUrl(String url) {
super(url);
}
@Override
public String getCacheKey() {
//将token部分参数替换为空字符串后返回作为缓存Key
return mUrl.replace(findTokenParam(), "");
}
/**
* 查找token部分参数的方法
* @return token部分参数String
*/
private String findTokenParam() {
String tokenParam = "";
int tokenKeyIndex = mUrl.indexOf("?token=") >= 0 ? mUrl.indexOf("?token=") : mUrl.indexOf("&token=");
if (tokenKeyIndex != -1) {
int nextAndIndex = mUrl.indexOf("&", tokenKeyIndex + 1);
if (nextAndIndex != -1) {
tokenParam = mUrl.substring(tokenKeyIndex + 1, nextAndIndex + 1);
} else {
tokenParam = mUrl.substring(tokenKeyIndex);
}
}
return tokenParam;
}
}
- 使用我们创建的MyGlideUrl传入url代码:
Glide.with(this)
.load(new MyGlideUrl(url))
.into(imageView);
8. 回调与监听
8.1 into()方法
- 我们都知道Glide的into()方法中是可以传入ImageView的。那么into()方法还可以传入别的参数吗?我们可以让Glide加载出来的图片不显示到ImageView上吗?答案是肯定的,这就需要用到自定义Target功能,如果我们要进行自定义的话,通常只需要在两种Target的基础上去自定义就可以了,一种是SimpleTarget,一种是ViewTarget。
SimpleTarget,它是一种极为简单的Target,我们使用它可以将Glide加载出来的图片对象 resource获取到,而不是像之前那样只能将图片在ImageView上显示出来,虽然我们下面实现的也是将图片在ImageView上显示,但是我们能拿到图片对象resource,有了这个对象之后就可以使用它进行任意的逻辑操作了。
SimpleTarget<Drawable> simpleTarget = new SimpleTarget<Drawable>() {
@Override
public void onResourceReady(Drawable resource, Transition<? super Drawable> transition) {
imageView.setImageDrawable(resource);
}
};
public void loadImage(View view) {
Glide.with(this)
.load("https://www.baidu.com/img/bd_logo1.png")
.into(simpleTarget);
}
ViewTarget要复杂一点,这里我们通过一个例子来演示一下吧,比如我创建了一个自定义布局MyLayout,代码如下所示:
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.widget.LinearLayout;
import com.bumptech.glide.request.target.ViewTarget;
import com.bumptech.glide.request.transition.Transition;
public class MyLayout extends LinearLayout{
private ViewTarget<MyLayout,Drawable> viewTarget;
public MyLayout(Context context) {
this(context,null);
}
public MyLayout(Context context, @Nullable AttributeSet attrs) {
this(context,attrs,0);
}
public MyLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
viewTarget = new ViewTarget<MyLayout,Drawable>(this) {
@Override
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
MyLayout myLayout = getView();
myLayout.setBackgroundDrawable(resource);
}
};
}
public ViewTarget<MyLayout,Drawable> getTarget() {
return viewTarget;
}
}
- 在MyLayout的构造函数中,我们创建了一个ViewTarget的实例,并将Mylayout当前的实例this传了进去。ViewTarget中需要指定两个泛型,一个是View的类型,一个图片的类型(Drawable或Bitmap)。然后在onResourceReady()方法中,我们就可以通过getView()方法获取到MyLayout的实例,并调用它的任意方法了。比如说这里我们调用了setBackgroundDrawable()方法来将加载出来的图片作为MyLayout布局的背景图。
- 接下来看一下怎么使用这个Target吧,由于MyLayout中已经提供了getTarget()接口,我们只需要在加载图片的地方这样写就可以了:
public class MainActivity extends AppCompatActivity {
private MyLayout myLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myLayout = (MyLayout) findViewById(R.id.background);
}
public void loadImage(View view) {
String url = "http://cn.bing.com/az/hprichbg/rb/TOAD_ZH-CN7336795473_1920x1080.jpg";
Glide.with(this)
.load(url)
.into(myLayout.getTarget());
}
}
- 就是这么简单,在into()方法中传入myLayout.getTarget()即可。
8.2 preload()方法
- Glide加载图片虽说非常智能,它会自动判断该图片是否已经有缓存了,如果有的话就直接从缓存中读取,没有的话再从网络去下载。但是如果我希望提前对图片进行一个预加载,等真正需要加载图片的时候就直接从缓存中读取,不想再等待慢长的网络加载时间了,这该怎么办呢?
- 不用担心,Glide专门给我们提供了预加载的接口,也就是preload()方法,我们只需要直接使用就可以了。
- preload()方法有两个方法重载,一个不带参数,表示将会加载图片的原始尺寸,另一个可以通过参数指定加载图片的宽和高。
- 这里有一点值得关注的是在Glide v3时我们如果使用了preload()方法,要将diskCacheStrategy的缓存策略指定成加载原始图片尺寸大小。因为preload()方法默认是预加载的原始图片大小,而into()方法则默认会根据ImageView控件的大小来动态决定加载图片的大小,所以同样into()方法也要将diskCacheStrategy的缓存策略指定成加载原始图片尺寸大小。否则,如果两者(预加载和显示)diskCacheStrategy的缓存策略不一致的话,很容易会造成我们在预加载完成之后再使用into()方法加载图片,却仍然还是要从网络上去请求图片这种现象。但是到了Glide v4就不需要指定磁盘缓存策略,因为Glide v4的默认磁盘缓存策略是DiskCacheStrategy.AUTOMATIC,Glide会根据图片资源智能地选择使用哪一种缓存策略,智能帮我们把这些工作做了,所以我们就不需要指定了。
- preload()方法的用法也非常简单,直接使用它来替换into()方法即可,如下所示:
Glide.with(this)
.load("https://www.baidu.com/img/bd_logo1.png")
.preload();
- 调用了预加载之后,我们以后想再去加载这张图片就会非常快了,因为Glide会直接从缓存当中去读取图片并显示出来,代码如下所示:
Glide.with(this)
.load("https://www.baidu.com/img/bd_logo1.png")
.into(imageView);
8.3 submit()方法
- 一直以来,我们使用Glide都是为了将图片显示到界面上。虽然我们知道Glide会在图片的加载过程中对图片进行缓存,但是缓存文件到底是存在哪里的,以及如何去直接访问这些缓存文件?我们都还不知道。
- 其实Glide将图片加载接口设计成这样也是希望我们使用起来更加的方便,不用过多去考虑底层的实现细节。但如果我现在就是想要去访问图片的缓存文件该怎么办呢?这就需要用到submit()方法了。
- 和preload()方法类似,submit()方法也是可以替换into()方法的,不过submit()方法的用法明显要比preload()方法复杂不少。这个方法只会下载图片,而不会对图片进行加载。当图片下载完成之后,我们可以得到图片的存储路径,以便后续进行操作。
- 那么首先我们还是先来看下基本用法。submit()方法有两个方法重载:
- submit():下载原始尺寸的图片。
- submit(int width, int height):下载指定尺寸的图片。
- 这里就以submit()方法来举例。当调用了submit()方法后会立即返回一个FutureTarget对象,然后Glide会在后台开始下载图片文件。接下来我们调用FutureTarget的get()方法就可以去获取下载好的图片文件了,如果此时图片还没有下载完,那么get()方法就会阻塞住,一直等到图片下载完成才会有值返回,所以get()方法必须在子线程中执行。
- 下面我们通过一个例子来演示一下吧,代码如下所示:
private void downloadImage() {
new Thread(new Runnable() {
@Override
public void run() {
try {
String url = "http://p1.pstatp.com/large/166200019850062839d3";
final Context context = getApplicationContext();
FutureTarget<File> target = Glide.with(context)
.asFile()
.load(url)
.submit();
final File imageFile = target.get();
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, imageFile.getPath(), Toast.LENGTH_LONG).show();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
- 这段代码稍微有一点点长,我带着大家解读一下。首先,submit()方法必须要用在子线程当中,因为刚才说了FutureTarget的get()方法是会阻塞线程的,因此这里的第一步就是new了一个Thread。在子线程当中,我们先获取了一个Application Context,这个时候不能再用Activity作为Context了,因为会有Activity销毁了但子线程下载还没执行完这种可能出现。
- 接下来就是Glide的基本用法,只不过将into()方法替换成了submit()方法,并且还使用了一个asFile()方法来指定加载格式。submit()方法会返回一个FutureTarget对象,这个时候其实Glide已经开始在后台下载图片了,我们随时都可以调用FutureTarget的get()方法来获取下载的图片文件,只不过如果图片还没下载好线程会暂时阻塞住,等下载完成了才会把图片的File对象返回。
- 最后,我们使用runOnUiThread()切回到主线程,然后使用Toast将下载好的图片文件路径显示出来。
8.4 下载图片并保存到指定路径
submit方法可以下载图片并获取到图片缓存路径路径,但是不知道大家有没有这样的需要就是想让图片下载到指定的路径,因为这样我们之后快速使用这部分图片,也方便对这部分图片进行管理,同时不需要受限于Glide的磁盘缓存机制,因为如果由Glide自动管理缓存的话,当下载的图片超过设定的缓存大小,一些比如长时间不使用的图片就会被Glide删除,但是其实这张图片我们之后还是需要使用的,这就很尴尬了,所以我们需要把图片下载到我们指定的位置,由我们自己来进行管理。代码如下:
- 添加读写数据权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
- 编写下载图片线程类
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.Looper;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.target.Target;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
public class DownloadImage implements Runnable{
//要下载图片的url
private String url;
//Glide下载所需的Context,最好用ApplicationContext,
//如果再用Activity作为Context了,可能会有Activity销毁了但子线程下载还没执行完这种情况出现。
private Context context;
//指定下载宽度,如果想下载原始宽带指定为0
private int width;
//指定下载高度,如果想下载原始高带指定为0
private int height;
//指定下载位置
private File mFile;
//下载完之后的回调
private ImagedownLoadCallBack callBack;
public interface ImagedownLoadCallBack{
void onDownLoadSuccess(Bitmap bitmap);
void onDownLoadFailed();
}
//用于回调到主线程的Handler,便于在回调回去的方法中执行UI操作
private Handler mHandler;
public DownloadImage(String url, Context context, int width, int height, File mFile, ImagedownLoadCallBack callBack) {
this.url = url;
this.context = context;
this.width = width;
this.height = height;
this.mFile = mFile;
this.callBack = callBack;
mHandler = new Handler(Looper.getMainLooper());
}
@Override
public void run() {
Bitmap bitmap = null;
FileOutputStream fos = null;
try {
if (width==0){
width = Target.SIZE_ORIGINAL;
}
if (height==0){
height = Target.SIZE_ORIGINAL;
}
bitmap = Glide.with(context)
.asBitmap()
.load(url)
.submit(width,height)
.get();
if (bitmap != null){
//上级文件夹不存在则创建
if (!mFile.getParentFile().exists()){
mFile.getParentFile().mkdirs();
}
//文件不存在则创建
if (!mFile.exists()){
mFile.createNewFile();
}
fos = new FileOutputStream(mFile);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.flush();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bitmap != null && mFile.exists()) {
final Bitmap finalBitmap = bitmap;
mHandler.post(new Runnable() {
@Override
public void run() {
callBack.onDownLoadSuccess(finalBitmap);
}
});
} else {
mHandler.post(new Runnable() {
@Override
public void run() {
callBack.onDownLoadFailed();
}
});
}
}
}
}
- 在按钮点击方法中开启线程调用DownloadImage去下载图片到指定路径中
public void doClick(View view) {
String url = "http://cn.bing.com/az/hprichbg/rb/Dongdaemun_ZH-CN10736487148_1920x1080.jpg";
File mFile = new File(Environment.getExternalStorageDirectory()+File.separator+"Glide","glideDownload.png");
DownloadImage downloadImage = new DownloadImage(url, getApplicationContext(), 600, 600, mFile, new DownloadImage.ImagedownLoadCallBack() {
@Override
public void onDownLoadSuccess(Bitmap bitmap) {
Toast.makeText(MainActivity.this, "下载完成", Toast.LENGTH_SHORT).show();
}
@Override
public void onDownLoadFailed() {
Toast.makeText(MainActivity.this, "下载失败", Toast.LENGTH_SHORT).show();
}
});
new Thread(downloadImage).start();
}
注意:这里submit(width,height)方法指定宽高后下载下来的图片尺寸并不是完全按我们指定尺寸来的,Glide是会做处理的,会保留原始尺寸的宽高比,以缩小比例较小边取我们指定的值的大小。比如上面我们下载的那张图片原始尺寸是1920 * 1080,我们指定尺寸为600 * 600,最后下载下来的图片尺寸是1067 * 600(宽是缩小比例较小边(1080/600<1920/600)取指定值600,长则按长宽比不变进行计算求得,计算过程:长=600 * (1920/1080)=1067)。除此之外我还试了下override()方法和preload()方法指定尺寸Glide是否也做了同样的处理,结果是是的。其实这如果源码读的很细应该是可以看出来的,不过这也确实有点太细节了,毕竟源码篇幅还是很多的,这种细节还是挺难注意到的,如果有精力可以找这部分去精读下,理解会更深。
8.5 listener()方法
- 其实listener()方法的作用非常普遍,它可以用来监听Glide加载图片的状态。举个例子,比如说我们刚才使用了preload()方法来对图片进行预加载,但是我怎样确定预加载有没有完成呢?还有如果Glide加载图片失败了,我该怎样调试错误的原因呢?答案都在listener()方法当中。
- 下面来看下listener()方法的基本用法吧,不同于刚才几个方法都是要替换into()方法的,listener()是结合into()方法一起使用的,当然也可以结合preload()方法一起使用。最基本的用法如下所示:
Glide.with(this)
.load("http://cn.bing.com/az/hprichbg/rb/Dongdaemun_ZH-CN10736487148_1920x1080.jpg")
.listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
return false;
}
})
.into(imageView);
- 这里我们在into()方法之前串接了一个listener()方法,然后实现了一个RequestListener的实例。其中RequestListener需要实现两个方法,一个onResourceReady()方法,一个onLoadFailed()方法。从方法名上就可以看出来了,当图片加载完成的时候就会回调onResourceReady()方法,而当图片加载失败的时候就会回调onLoadFailed()方法,onLoadFailed()方法中会将失败的GlideException参数传进来,这样我们就可以定位具体失败的原因了。
- 没错,listener()方法就是这么简单。不过还有一点需要处理,onResourceReady()方法和onLoadFailed()方法都有一个布尔值的返回值,返回false就表示这个事件没有被处理,还会继续向下传递,返回true就表示这个事件已经被处理掉了,从而不会再继续向下传递。举个简单点的例子,如果我们在RequestListener的onResourceReady()方法中返回了true,那么就不会再回调Target的onResourceReady()方法了。
9. 图片变换功能
- 图片变换的意思就是说,Glide从加载了原始图片到最终展示给用户之前,又进行了一些变换处理,从而能够实现一些更加丰富的图片效果,如图片圆角化、圆形化、模糊化等等。
- 添加图片变换的用法非常简单,我们只需要在RequestOptions中串接transforms()方法,并将想要执行的图片变换操作作为参数传入transforms()方法即可,如下所示:
RequestOptions options = new RequestOptions()
.transforms(...);
Glide.with(this)
.load(url)
.apply(options)
.into(imageView);
- 至于具体要进行什么样的图片变换操作,这个通常都是需要我们自己来写的。不过Glide已经内置了几种图片变换操作,我们可以直接拿来使用,比如CenterCrop、FitCenter、CircleCrop等。
- ImageView默认的scaleType是FIT_CENTER,因此加载图片时会自动添加一个FitCenter的图片变换,而在这个图片变换过程中做了某些操作,会导致图片充满整个布局,可是有时我们不想要让它充满整个布局,怎么办呢?实际上,Glide给我们提供了专门的API来取消图片变换,使所有的图片变换(默认的和我们自己添加的)操作就全部失效了。取消API如下:
RequestOptions options = new RequestOptions()
.dontTransform();
- 但所有的内置图片变换操作其实都不需要使用transform()方法,Glide为了方便我们使用直接提供了现成的API:
RequestOptions options = new RequestOptions()
.centerCrop();
RequestOptions options = new RequestOptions()
.fitCenter();
RequestOptions options = new RequestOptions()
.circleCrop();
- 当然,这些内置的图片变换API其实也只是对transform()方法进行了一层封装而已,它们背后的源码仍然还是借助transform()方法来实现的。
- 这里我们就选择其中一种内置的图片变换操作来演示一下吧,circleCrop()方法是用来对图片进行圆形化裁剪的,我们动手试一下,代码如下所示:
String url = "http://guolin.tech/book.png";
RequestOptions options = new RequestOptions()
.circleCrop();
Glide.with(this)
.load(url)
.apply(options)
.into(imageView);
重新运行一下程序并点击加载图片按钮,效果如下图所示。
加载图片4.gif
- 可以看到,现在展示的图片是对原图进行圆形化裁剪后得到的图片。
- 当然,除了使用内置的图片变换操作之外,我们完全可以自定义自己的图片变换操作。理论上,在对图片进行变换这个步骤中我们可以进行任何的操作,你想对图片怎么样都可以。包括圆角化、圆形化、黑白化、模糊化等等,甚至你将原图片完全替换成另外一张图都是可以的。
- 为了方便我们的使用我们来看一个非常优秀的开源库,glide-transformations。它实现了很多通用的图片变换效果,如裁剪变换、颜色变换、模糊变换等等,使得我们可以非常轻松地进行各种各样的图片变换,这样就可以省去我们去编写自定义图片变换操作的代码了,避免重复造轮子。
- glide-transformations的项目主页地址是 https://github.com/wasabeef/glide-transformations
- 下面我们就来体验一下这个库的强大功能吧。首先需要将这个库引入到我们的项目当中,在app/build.gradle文件当中添加如下依赖:
dependencies {
implementation 'jp.wasabeef:glide-transformations:3.0.1'
}
- 我们可以对图片进行单个变换处理,也可以将多种图片变换叠加在一起使用。比如我想同时对图片进行模糊化和黑白化处理,就可以这么写:
String url = "http://guolin.tech/book.png";
RequestOptions options = new RequestOptions()
.transforms(new BlurTransformation(), new GrayscaleTransformation());
Glide.with(this)
.load(url)
.apply(options)
.into(imageView);
可以看到,同时执行多种图片变换的时候,只需要将它们都传入到transforms()方法中即可。现在重新运行一下程序,效果如下图所示。
加载图片5.gif
- 当然,这只是glide-transformations库的一小部分功能而已,更多的图片变换效果你可以到它的GitHub项目主页去学习。所有变换的用法都是这么简单哦。
10. 自定义模块
- 自定义模块功能可以将更改Glide配置,替换Glide组件等操作独立出来,使得我们能轻松地对Glide的各种配置进行自定义,并且又和Glide的图片加载逻辑没有任何交集,这也是一种低耦合编程方式的体现。下面我们就来学习一下自定义模块要如何实现。
- 首先定义一个我们自己的模块类,并让它继承自AppGlideModule,如下所示:
import android.content.Context;
import android.support.annotation.NonNull;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;
@GlideModule
public class MyAppGlideModule extends AppGlideModule {
//更改Glide配置
@Override
public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
super.applyOptions(context, builder);
}
//替换Glide组件
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
super.registerComponents(context, glide, registry);
}
}
- 可以看到,在MyAppGlideModule类当中,我们重写了applyOptions()和registerComponents()方法,这两个方法分别就是用来更改Glide配置以及替换Glide组件的。
- 注意在MyAppGlideModule类在上面,我们加入了一个@GlideModule的注解,有了@GlideModule注解Glide才能够识别这个自定义模块。
- 这样的话,我们就将Glide自定义模块的功能完成了。后面只需要在applyOptions()和registerComponents()这两个方法中加入具体的逻辑,就能实现更改Glide配置或者替换Glide组件的功能了,下面我们马上就来学习一下。
更改Glide配置
- 刚才在分析自定义模式工作原理的时候其实就已经提到了,如果想要更改Glide的默认配置,其实只需要在applyOptions()方法中提前将Glide的配置项进行初始化就可以了。那么Glide一共有哪些配置项呢?这里我给大家做了一个列举:
- setMemoryCache()
用于配置Glide的内存缓存策略,默认配置是LruResourceCache。- setBitmapPool()
用于配置Glide的Bitmap缓存池,默认配置是LruBitmapPool。- setDiskCache()
用于配置Glide的硬盘缓存策略,默认配置是InternalCacheDiskCacheFactory。- setDiskCacheExecutor()
用于配置Glide读取缓存中图片的异步执行器,默认配置是FifoPriorityThreadPoolExecutor,也就是先入先出原则。- setResizeService()
用于配置Glide读取非缓存中图片的异步执行器,默认配置也是FifoPriorityThreadPoolExecutor。- setDefaultRequestOptions()
用于配置Glide加载图片的默认请求选项,其中解码模式的配置就包含在里面,默认配置是RGB_565。
- 其实Glide的这些默认配置都非常科学且合理,使用的缓存算法也都是效率极高的,因此在绝大多数情况下我们并不需要去修改这些默认配置,这也是Glide用法能如此简洁的一个原因。
- 但是Glide科学的默认配置并不影响我们去学习自定义Glide模块的功能,因此总有某些情况下,默认的配置可能将无法满足你,这个时候就需要我们自己动手来修改默认配置了。
将加载的图片缓存到SD卡
- 下面就通过具体的实例来看一下吧。刚才说到,Glide默认的硬盘缓存策略使用的是InternalCacheDiskCacheFactory,这种缓存会将所有Glide加载的图片都存储到当前应用的私有目录下。这是一种非常安全的做法,但同时这种做法也造成了一些不便,因为私有目录下即使是开发者自己也是无法查看的,如果我想要去验证一下图片到底有没有成功缓存下来,这就有点不太好办了。
- 这种情况下,就非常适合使用自定义模块来更改Glide的默认配置。我们完全可以自己去实现DiskCache.Factory接口来自定义一个硬盘缓存策略,不过却大大没有必要这么做,因为Glide本身就内置了一个ExternalCacheDiskCacheFactory,可以允许将加载的图片都缓存到SD卡。
- 那么接下来,我们就尝试使用这个ExternalPreferredCacheDiskCacheFactory来替换默认的InternalCacheDiskCacheFactory,从而将所有Glide加载的图片都缓存到SD卡上。
- 由于在前面我们已经创建好了一个自定义模块MyGlideModule,那么现在就可以直接在这里编写逻辑了,代码如下所示:
//更改Glide配置
@Override
public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
super.applyOptions(context, builder);
builder.setDiskCache(new ExternalPreferredCacheDiskCacheFactory(context));
}
- 没错,就是这么简单,现在所有Glide加载的图片都会缓存到SD卡上了。
修改Glide默认的缓存大小
- InternalCacheDiskCacheFactory和ExternalPreferredCacheDiskCacheFactory的默认硬盘缓存大小都是250M。也就是说,如果你的应用缓存的图片总大小超出了250M,那么Glide就会按照DiskLruCache算法的原则来清理缓存的图片。
- 当然,我们是可以对这个默认的缓存大小进行修改的,而且修改方式非常简单,如下所示:
private static final long DISK_CACHE_SIZE = 500*1024*1024;
//更改Glide配置
@Override
public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
super.applyOptions(context, builder);
builder.setDiskCache(new ExternalPreferredCacheDiskCacheFactory(context,DISK_CACHE_SIZE));
}
- 只需要向ExternalPreferredCacheDiskCacheFactory或者InternalCacheDiskCacheFactory再传入一个参数就可以了,现在我们就将Glide硬盘缓存的大小调整成了500M。
- 好了,更改Glide配置的功能就是这么简单,那么接下来我们就来验证一下更改的配置到底有没有生效吧。
- 这里还是使用最基本的Glide加载语句来去加载一张网络图片:
String url = "http://guolin.tech/book.png";
Glide.with(this)
.load(url)
.into(imageView);
- 运行一下程序,图片加载出来之后我们去找找它的缓存吧。
- ExternalPreferredCacheDiskCacheFactory的默认缓存路径是在Android/data/包名/cache/image_manager_disk_cache目录当中,我们使用文件浏览器进入到这个目录,结果如下图所示。 缓存路径文件夹.png
- 可以看到,这里有两个文件,其中journal文件是DiskLruCache算法的日志文件,这个文件必不可少,且只会有一个。而另外一个文件就是那张缓存的图片了,它的文件名虽然看上去很奇怪,但是我们只需要把这个文件的后缀改成.png,然后用图片浏览器打开,结果就一目了然了,如下图所示。 第一行代码封面图.png
- 由此证明,我们已经成功将Glide的硬盘缓存路径修改到SD卡上了。
修改Glide加载图片的格式为ARGB_8888
- 我们都知道Glide和Picasso的用法是非常相似的,但是有一点差别却很大。Glide加载图片的默认格式是RGB_565,而Picasso加载图片的默认格式是ARGB_8888。ARGB_8888格式的图片效果会更加细腻,但是内存开销会比较大。而RGB_565格式的图片则更加节省内存,但是图片效果上会差一些。
- Glide和Picasso各自采取的默认图片格式谈不上熟优熟劣,只能说各自的取舍不一样。但是如果你希望Glide也能使用ARGB_8888的图片格式,这当然也是可以的。我们只需要在MyGlideModule中更改一下默认配置即可,如下所示:
private static final long DISK_CACHE_SIZE = 500*1024*1024;
//更改Glide配置
@Override
public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
super.applyOptions(context, builder);
builder.setDiskCache(new ExternalPreferredCacheDiskCacheFactory(context,DISK_CACHE_SIZE));
builder.setDefaultRequestOptions(
new RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
);
}
- 通过这样配置之后,使用Glide加载的所有图片都将会使用ARGB_8888的格式,虽然图片质量变好了,但同时内存开销也会明显增大,所以你要做好心理准备哦。
替换Glide组件(替换HTTP通讯组件)
- 替换Glide组件功能需要在自定义模块的registerComponents()方法中加入具体的替换逻辑。相比于更改Glide配置,替换Glide组件这个功能的难度就明显大了不少。Glide中的组件非常繁多,也非常复杂,但其实大多数情况下并不需要我们去做什么替换。不过,有一个组件却有着比较大的替换需求,那就是Glide的HTTP通讯组件。
- 默认情况下,Glide使用的是基于原生HttpURLConnection进行订制的HTTP通讯组件,但是现在大多数的Android开发者都更喜欢使用OkHttp,因此将Glide中的HTTP通讯组件修改成OkHttp的这个需求比较常见,那么我们就来实现下这个功能,基本就是将HTTP的通讯部分代码用到的通讯组件HttpURLConnection替换成我们需要的OkHttp。
- 将OkHttp的库引入到当前项目中,如下所示:
dependencies {
implementation 'com.squareup.okhttp3:okhttp:3.11.0'
}
- 新建一个OkHttpFetcher类,并且实现DataFetcher接口,代码如下所示:
import android.support.annotation.NonNull;
import android.util.Log;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.HttpException;
import com.bumptech.glide.load.data.DataFetcher;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.util.ContentLengthInputStream;
import com.bumptech.glide.util.Preconditions;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import okhttp3.Call;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class OkHttpFetcher implements DataFetcher<InputStream>, okhttp3.Callback {
private static final String TAG = "OkHttpFetcher";
private final Call.Factory client;
private final GlideUrl url;
private InputStream stream;
private ResponseBody responseBody;
private DataFetcher.DataCallback<? super InputStream> callback;
private volatile Call call;
@SuppressWarnings("WeakerAccess")
public OkHttpFetcher(Call.Factory client, GlideUrl url) {
this.client = client;
this.url = url;
}
@Override
public void loadData(@NonNull Priority priority,
@NonNull final DataCallback<? super InputStream> callback) {
Request.Builder requestBuilder = new Request.Builder().url(url.toStringUrl());
for (Map.Entry<String, String> headerEntry : url.getHeaders().entrySet()) {
String key = headerEntry.getKey();
requestBuilder.addHeader(key, headerEntry.getValue());
}
Request request = requestBuilder.build();
this.callback = callback;
call = client.newCall(request);
call.enqueue(this);
}
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "OkHttp failed to obtain result", e);
}
callback.onLoadFailed(e);
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
responseBody = response.body();
if (response.isSuccessful()) {
long contentLength = Preconditions.checkNotNull(responseBody).contentLength();
stream = ContentLengthInputStream.obtain(responseBody.byteStream(), contentLength);
callback.onDataReady(stream);
} else {
callback.onLoadFailed(new HttpException(response.message(), response.code()));
}
}
@Override
public void cleanup() {
try {
if (stream != null) {
stream.close();
}
} catch (IOException e) {
// Ignored
}
if (responseBody != null) {
responseBody.close();
}
callback = null;
}
@Override
public void cancel() {
Call local = call;
if (local != null) {
local.cancel();
}
}
@NonNull
@Override
public Class<InputStream> getDataClass() {
return InputStream.class;
}
@NonNull
@Override
public DataSource getDataSource() {
return DataSource.REMOTE;
}
}
- 然后新建一个OkHttpGlideUrlLoader类,并且实现ModelLoader
import android.support.annotation.NonNull;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.load.model.ModelLoader;
import com.bumptech.glide.load.model.ModelLoaderFactory;
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
import java.io.InputStream;
import okhttp3.Call;
import okhttp3.OkHttpClient;
public class OkHttpGlideUrlLoader implements ModelLoader<GlideUrl, InputStream> {
private final Call.Factory client;
@SuppressWarnings("WeakerAccess")
public OkHttpGlideUrlLoader(@NonNull Call.Factory client) {
this.client = client;
}
@Override
public boolean handles(@NonNull GlideUrl url) {
return true;
}
@Override
public LoadData<InputStream> buildLoadData(@NonNull GlideUrl model, int width, int height,
@NonNull Options options) {
return new LoadData<>(model, new OkHttpFetcher(client, model));
}
@SuppressWarnings("WeakerAccess")
public static class Factory implements ModelLoaderFactory<GlideUrl, InputStream> {
private static volatile Call.Factory internalClient;
private final Call.Factory client;
private static Call.Factory getInternalClient() {
if (internalClient == null) {
synchronized (OkHttpGlideUrlLoader.Factory.class) {
if (internalClient == null) {
internalClient = new OkHttpClient();
}
}
}
return internalClient;
}
public Factory() {
this(getInternalClient());
}
public Factory(@NonNull Call.Factory client) {
this.client = client;
}
@NonNull
@Override
public ModelLoader<GlideUrl, InputStream> build(MultiModelLoaderFactory multiFactory) {
return new OkHttpGlideUrlLoader(client);
}
@Override
public void teardown() {}
}
}
- 接下来,新建一个MyGlideModule类并实现GlideModule接口,然后在registerComponents()方法中将我们刚刚创建的OkHttpGlideUrlLoader和OkHttpFetcher注册到Glide当中,将原来的HTTP通讯组件给替换掉,如下所示:
import android.content.Context;
import android.support.annotation.NonNull;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.load.DecodeFormat;
import com.bumptech.glide.load.engine.cache.ExternalPreferredCacheDiskCacheFactory;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.module.AppGlideModule;
import com.bumptech.glide.request.RequestOptions;
import java.io.InputStream;
@GlideModule
public class MyAppGlideModule extends AppGlideModule {
private static final long DISK_CACHE_SIZE = 500*1024*1024;
//更改Glide配置
@Override
public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
super.applyOptions(context, builder);
builder.setDiskCache(new ExternalPreferredCacheDiskCacheFactory(context,DISK_CACHE_SIZE));
builder.setDefaultRequestOptions(
new RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
);
}
//替换Glide组件
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
super.registerComponents(context, glide, registry);
registry.replace(GlideUrl.class, InputStream.class, new OkHttpGlideUrlLoader.Factory());
}
}
- 可以看到,这里是调用了registry的replace()方法来替换组件的。
更简单的组件替换
- 上述方法是我们纯手工地将Glide的HTTP通讯组件进行了替换,如果你不想这么麻烦也是可以的,Glide官方给我们提供了非常简便的HTTP组件替换方式。并且除了支持OkHttp3之外,还支持Volley。
- 我们只需要在gradle当中添加几行库的配置就行了。比如使用OkHttp3来作为HTTP通讯组件的配置如下:
implementation "com.github.bumptech.glide:okhttp3-integration:4.7.1"
implementation 'com.squareup.okhttp3:okhttp:3.11.0'
- 使用Volley来作为HTTP通讯组件的配置如下:
implementation "com.github.bumptech.glide:volley-integration:4.7.1"
implementation 'com.mcxiaoke.volley:library:1.0.19'
- 当然了,这些库背后的工作原理和我们刚才自己手动实现替换HTTP组件的原理是一模一样的。而学会了手动替换组件的原理我们就能更加轻松地扩展更多丰富的功能,因此掌握这一技能还是非常重要的。
实现下载进度监听
- 我们都知道,使用Glide来加载一张网络上的图片是非常简单的,但是让人头疼的是,我们却无从得知当前图片的下载进度。如果这张图片很小的话,那么问题也不大,反正很快就会被加载出来。但如果这是一张比较大的GIF图,用户耐心等了很久结果图片还没显示出来,这个时候你就会觉得下载进度功能是十分有必要的了。
- 好的,那么下面我们就对Glide进行功能扩展,使其支持监听图片下载进度的功能。我们是基于把组件替换成了OkHttp的基础上进行实现的。
- 那么,将HTTP通讯组件替换成OkHttp之后,我们又该如何去实现监听下载进度的功能呢?这就要依靠OkHttp强大的拦截器机制了。
- 我们只要向OkHttp中添加一个自定义的拦截器,就可以在拦截器中捕获到整个HTTP的通讯过程,然后加入一些自己的逻辑来计算下载进度,这样就可以实现下载进度监听的功能了。
- 拦截器属于OkHttp的高级功能,不过即使你之前并没有接触过拦截器,我相信你也能轻松看懂的,因为它本身并不难。
- 确定了实现思路之后,那我们就开始动手吧。首先创建一个没有任何逻辑的空拦截器,新建ProgressInterceptor类并实现Interceptor接口,代码如下所示:
import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
public class ProgressInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
return response;
}
}
- 这个拦截器中我们可以说是什么都没有做。就是拦截到了OkHttp的请求,然后调用proceed()方法去处理这个请求,最终将服务器响应的Response返回。
- 接下来我们需要启用这个拦截器,修改MyGlideModule中的代码,如下所示:
import android.content.Context;
import android.support.annotation.NonNull;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.module.AppGlideModule;
import java.io.InputStream;
import okhttp3.OkHttpClient;
@GlideModule
public class MyAppGlideModule extends AppGlideModule {
private static final long DISK_CACHE_SIZE = 500*1024*1024;
//更改Glide配置
@Override
public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
super.applyOptions(context, builder);
}
//替换Glide组件
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
super.registerComponents(context, glide, registry);
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.addInterceptor(new ProgressInterceptor());
OkHttpClient okHttpClient = builder.build();
registry.replace(GlideUrl.class, InputStream.class, new OkHttpGlideUrlLoader.Factory(okHttpClient));
}
}
- 这里我们创建了一个OkHttpClient.Builder,然后调用addInterceptor()方法将刚才创建的ProgressInterceptor添加进去,最后将构建出来的新OkHttpClient对象传入到OkHttpGlideUrlLoader.Factory中即可。
- 好的,现在自定义的拦截器已经启用了,接下来就可以开始去实现下载进度监听的具体逻辑了。首先新建一个ProgressListener接口,用于作为进度监听回调的工具,如下所示:
public interface ProgressListener {
void onProgress(int progress);
}
- 然后我们在上面空的ProgressInterceptor拦截器类中加入注册下载监听和取消注册下载监听的方法。修改ProgressInterceptor中的代码,如下所示:
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
public class ProgressInterceptor implements Interceptor {
static final Map<String, ProgressListener> LISTENER_MAP = new HashMap<>();
public static void addListener(String url, ProgressListener listener) {
LISTENER_MAP.put(url, listener);
}
public static void removeListener(String url) {
LISTENER_MAP.remove(url);
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
return response;
}
}
- 可以看到,这里使用了一个Map来保存注册的监听器,Map的键是一个URL地址。之所以要这么做,是因为你可能会使用Glide同时加载很多张图片,而这种情况下,必须要能区分出来每个下载进度的回调到底是对应哪个图片URL地址的。
- 接下来就要到最复杂的部分了,也就是下载进度的具体计算。我们需要新建一个ProgressResponseBody类,并让它继承自OkHttp的ResponseBody,然后在这个类当中去编写具体的监听下载进度的逻辑,代码如下所示:
import android.util.Log;
import java.io.IOException;
import okhttp3.MediaType;
import okhttp3.ResponseBody;
import okio.Buffer;
import okio.BufferedSource;
import okio.ForwardingSource;
import okio.Okio;
import okio.Source;
public class ProgressResponseBody extends ResponseBody {
private static final String TAG = "ProgressResponseBody";
private BufferedSource bufferedSource;
private ResponseBody responseBody;
private ProgressListener listener;
public ProgressResponseBody(String url, ResponseBody responseBody) {
this.responseBody = responseBody;
listener = ProgressInterceptor.LISTENER_MAP.get(url);
}
@Override
public MediaType contentType() {
return responseBody.contentType();
}
@Override
public long contentLength() {
return responseBody.contentLength();
}
@Override
public BufferedSource source() {
if (bufferedSource == null) {
bufferedSource = Okio.buffer(new ProgressSource(responseBody.source()));
}
return bufferedSource;
}
private class ProgressSource extends ForwardingSource {
long totalBytesRead = 0;
int currentProgress;
ProgressSource(Source source) {
super(source);
}
@Override
public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
long fullLength = responseBody.contentLength();
if (bytesRead == -1) {
totalBytesRead = fullLength;
} else {
totalBytesRead += bytesRead;
}
int progress = (int) (100f * totalBytesRead / fullLength);
Log.d(TAG, "download progress is " + progress);
if (listener != null && progress != currentProgress) {
listener.onProgress(progress);
}
if (listener != null && totalBytesRead == fullLength) {
listener = null;
}
currentProgress = progress;
return bytesRead;
}
}
}
- 其实这段代码也不是很难,下面我来简单解释一下。首先,我们定义了一个ProgressResponseBody的构造方法,该构造方法中要求传入一个url参数和一个ResponseBody参数。那么很显然,url参数就是图片的url地址了,而ResponseBody参数则是OkHttp拦截到的原始的ResponseBody对象。然后在构造方法中,我们调用了ProgressInterceptor中的LISTENER_MAP来去获取该url对应的监听器回调对象,有了这个对象,待会就可以回调计算出来的下载进度了。
- 由于继承了ResponseBody类之后一定要重写contentType()、contentLength()和source()这三个方法,我们在contentType()和contentLength()方法中直接就调用传入的原始ResponseBody的contentType()和contentLength()方法即可,这相当于一种委托模式。但是在source()方法中,我们就必须加入点自己的逻辑了,因为这里要涉及到具体的下载进度计算。
- 那么我们具体看一下source()方法,这里先是调用了原始ResponseBody的source()方法来去获取Source对象,接下来将这个Source对象封装到了一个ProgressSource对象当中,最终再用Okio的buffer()方法封装成BufferedSource对象返回。
- 那么这个ProgressSource是什么呢?它是一个我们自定义的继承自ForwardingSource的实现类。ForwardingSource也是一个使用委托模式的工具,它不处理任何具体的逻辑,只是负责将传入的原始Source对象进行中转。但是,我们使用ProgressSource继承自ForwardingSource,那么就可以在中转的过程中加入自己的逻辑了。
- 可以看到,在ProgressSource中我们重写了read()方法,然后在read()方法中获取该次读取到的字节数以及下载文件的总字节数,并进行一些简单的数学计算就能算出当前的下载进度了。这里我先使用Log工具将算出的结果打印了一下,再通过前面获取到的回调监听器对象将结果进行回调。
- 好的,现在计算下载进度的逻辑已经完成了,那么我们快点在拦截器当中使用它吧。修改ProgressInterceptor中的代码,如下所示:
public class ProgressInterceptor implements Interceptor {
...
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
String url = request.url().toString();
ResponseBody body = response.body();
Response newResponse = response.newBuilder().body(new ProgressResponseBody(url, body)).build();
return newResponse;
}
}
- 这里也都是一些OkHttp的简单用法。我们通过Response的newBuilder()方法来创建一个新的Response对象,并把它的body替换成刚才实现的ProgressResponseBody,最终将新的Response对象进行返回,这样计算下载进度的逻辑就能生效了。
-
代码写到这里,我们就可以来运行一下程序了。现在无论是加载任何网络上的图片,都应该是可以监听到它的下载进度的。运行下程序,图片加载出来后可以看到我们在ProgressResponseBody中加的日志打印出来如下所示:
下载进度日志.png
11. 使用Generated API
使用和Glide 3一样的流式API接口
- Generated API是Glide 4中全新引入的一个功能,它的工作原理是使用注解处理器 (Annotation Processor) 来生成出一个API,在自定义模块中可使用该流式API一次性调用到RequestBuilder,RequestOptions和集成库中所有的选项。简单点说,就是Glide 4仍然给我们提供了一套和Glide 3一模一样的流式API接口。
- 对于熟悉Glide 3的朋友来说那是再简单不过了,基本上就是和Glide 3一模一样的用法,只不过需要把Glide关键字替换成GlideApp关键字,如下所示:
GlideApp.with(this)
.load(url)
.placeholder(R.drawable.loading)
.error(R.drawable.error)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.override(Target.SIZE_ORIGINAL)
.circleCrop()
.into(imageView);
- 不过,有可能你的IDE中会提示找不到GlideApp这个类。这个类是通过编译时注解自动生成的,首先确保你的代码中有一个自定义的模块,并且给它加上了@GlideModule注解,也就是我们在上面所讲的内容。然后在Android Studio中点击菜单栏Build -> Rebuild Project,GlideApp这个类就会自动生成了。
对现有的API进行扩展,定制出任何属于你自己的API
- 下面具体举个例子,比如说我们要求项目中所有图片的缓存策略全部都要缓存原始图片,那么每次在使用Glide加载图片的时候,都去指定diskCacheStrategy(DiskCacheStrategy.DATA)这么长长的一串代码,确实是让人比较心烦。这种情况我们就可以去定制一个自己的API了。
- 定制自己的API需要借助@GlideExtension和@GlideOption这两个注解。创建一个我们自定义的扩展类,代码如下所示:
import com.bumptech.glide.annotation.GlideExtension;
import com.bumptech.glide.annotation.GlideOption;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.RequestOptions;
@GlideExtension
public class MyGlideExtension {
private MyGlideExtension() {
}
@GlideOption
public static void cacheSource(RequestOptions options) {
options.diskCacheStrategy(DiskCacheStrategy.DATA);
}
}
- 这里我们定义了一个MyGlideExtension类,并且给加上了一个@GlideExtension注解,然后要将这个类的构造函数声明成private,这都是必须要求的写法。
- 接下来就可以开始自定义API了,这里我们定义了一个cacheSource()方法,表示只缓存原始图片,并给这个方法加上了@GlideOption注解。注意自定义API的方法都必须是静态方法,而且第一个参数必须是RequestOptions,后面你可以加入任意多个你想自定义的参数。
- 在cacheSource()方法中,我们仍然还是调用的diskCacheStrategy(DiskCacheStrategy.DATA)方法,所以说cacheSource()就是一层简化API的封装而已。
- 然后在Android Studio中点击菜单栏Build -> Rebuild Project,神奇的事情就会发生了,你会发现你已经可以使用这样的语句来加载图片了:
GlideApp.with(this)
.load(url)
.cacheSource()
.into(imageView);
有了这个强大的功能之后,我们使用Glide就能变得更加灵活了。
结束语
到此为止,Glide 4的用法我基本也总结完了。这篇博客也是我写博客以来不论是篇幅和用时都是最长的的一篇,如果你能从头看到这里你对Glide的用法也可以说是深度掌握了,基本上可以满足你项目上的所有要求了。同时你能坚持看下来你也是很棒的哦,加油吧,少年!
网友评论