Glide 源码学习五:回调与监听,Android 快速转战 Kotlin 教程
[Android 面试题:Glide](
)
[Glide 源码学习一:Glide 框架介绍、with 方法详解](
)
[Glide 源码学习二:load()详解](
)
[Glide 源码学习三:into()详解](
)
[Glide 源码学习四:缓存](
)
[Glide 源码学习五:回调与监听](
)
[Glide 源码学习六:图片变换](
)
[Glide 源码学习七:自定义模块功能](
)
[Glide 源码学习八:实现带进度的 Glide 图片加载功能](
)
[Glide 源码学习九:带你全面了解 Glide 4 的用法](
)
相关文章:
=====
[Android 图片加载框架最全解析(四),玩转 Glide 的回调与监听](
)
回调的源码实现
=======
作为一名 Glide 老手,相信大家对于 Glide 的基本用法已经非常熟练了。我们都知道,使用 Glide 在界面上加载并展示一张图片只需要一行代码:
Glide.with(this).load(url).into(imageView);
而在这一行代码的背后,Glide 帮我们执行了成千上万行的逻辑。其实在第二篇文章当中,我们已经分析了这一行代码背后的完整执行流程,但是这里我准备再带着大家单独回顾一下回调这部分的源码,这将有助于我们今天这篇文章的学习。
首先来看一下 into()方法,这里我们将 ImageView 的实例传入到 into()方法当中,Glide 将图片加载完成之后,图片就能显示到 ImageView 上了。这是怎么实现的呢?我们来看一下 into()方法的源码:
public Target<TranscodeType> into(ImageView view) {
Util.assertMainThread();
if (view == null) {
throw new IllegalArgumentException("You must pass in a non null View");
}
if (!isTransformationSet && view.getScaleType() != null) {
switch (view.getScaleType()) {
case CENTER_CROP:
applyCenterCrop();
break;
case FIT_CENTER:
case FIT_START:
case FIT_END:
applyFitCenter();
break;
default:
// Do nothing.
}
}
return into(glide.buildImageViewTarget(view, transcodeClass));
}
可以看到,最后一行代码会调用 glide.buildImageViewTarget()方法构建出一个 Target 对象,然后再把它传入到另一个接收 Target 参数的 into()方法中。Target 对象则是用来最终展示图片用的,如果我们跟进到 glide.buildImageViewTarget()方法中,你会看到如下的源码:
public class ImageViewTargetFactory {
@SuppressWarnings("unchecked")
public <Z> Target<Z> buildTarget(ImageView view, Class<Z> clazz) {
if (GlideDrawable.class.isAssignableFrom(clazz)) {
return (Target<Z>) new GlideDrawableImageViewTarget(view);
} else if (Bitmap.class.equals(clazz)) {
return (Target<Z>) new BitmapImageViewTarget(view);
} else if (Drawable.class.isAssignableFrom(clazz)) {
return (Target<Z>) new DrawableImageViewTarget(view);
} else {
throw new IllegalArgumentException("Unhandled class: " + clazz
", try .as*(Class).transcode(ResourceTranscoder)");
}
}
}
buildTarget()方法会根据传入的 class 参数来构建不同的 Target 对象,如果你在使用 Glide 加载图片的时候调用了 asBitmap()方法,那么这里就会构建出 BitmapImageViewTarget 对象,否则的话构建的都是 GlideDrawableImageViewTarget 对象。至于上述代码中的 DrawableImageViewTarget 对象,这个通常都是用不到的,我们可以暂时不用管它。
之后就会把这里构建出来的 Target 对象传入到 GenericRequest 当中,而 Glide 在图片加载完成之后又会回调 GenericRequest 的 onResourceReady()方法,我们来看一下这部分源码:
public final class GenericRequest<A, T, Z, R> implements Request, SizeReadyCallback,
ResourceCallback {
private Target<R> target;
...
private void onResourceReady(Resource<?> resource, R result) {
boolean isFirstResource = isFirstReadyResource();
status = Status.COMPLETE;
this.resource = resource;
if (requestListener == null || !requestListener.onResourceReady(result, model, target,
loadedFromMemoryCache, isFirstResource)) {
GlideAnimation<R> animation = animationFactory.build(loadedFromMemoryCache, isFirstResource);
target.onResourceReady(result, animation);
}
notifyLoadSuccess();
}
...
}
这里在第 14 行调用了 target.onResourceReady()方法,而刚才我们已经知道,这里的 target 就是 GlideDrawableImageViewTarget 对象,那么我们再来看一下它的源码:
public class GlideDrawableImageViewTarget extends ImageViewTarget<GlideDrawable> {
...
@Override
public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> animation) {
if (!resource.isAnimated()) {
float viewRatio = view.getWidth() / (float) view.getHeight();
float drawableRatio = resource.getIntrinsicWidth() / (float) resource.getIntrinsicHeight();
if (Math.abs(viewRatio - 1f) <= SQUARE_RATIO_MARGIN
&& Math.abs(drawableRatio - 1f) <= SQUARE_RATIO_MARGIN) {
resource = new SquaringDrawable(resource, view.getWidth());
}
}
super.onResourceReady(resource, animation);
this.resource = resource;
resource.setLoopCount(maxLoopCount);
resource.start();
}
@Override
protected void setResource(GlideDrawable resource) {
view.setImageDrawable(resource);
}
...
}
可以看到,这里在 onResourceReady()方法中处理了图片展示,还有 GIF 播放的逻辑,那么一张图片也就显示出来了,这也就是 Glide 回调的基本实现原理。
好的,那么原理就先分析到这儿,接下来我们就来看一下在回调和监听方面还有哪些知识是可以扩展的。
into()方法
========
使用了这么久的 Glide,我们都知道 into()方法中是可以传入 ImageView 的。那么 into()方法还可以传入别的参数吗?我可以让 Glide 加载出来的图片不显示到 ImageView 上吗?答案是肯定的,这就需要用到自定义 Target 功能。
其实通过上面的分析,我们已经知道了,into()方法还有一个接收 Target 参数的重载。即使我们传入的参数是 ImageView,Glide 也会在内部自动构建一个 Target 对象。而如果我们能够掌握自定义 Target 技术的话,就可以更加随心所欲地控制 Glide 的回调了。
我们先来看一下 Glide 中 Target 的继承结构图吧,如下所示:
可以看到,Target 的继承结构还是相当复杂的,实现 Target 接口的子类非常多。不过你不用被这么多的子类所吓到,这些大多数都是 Glide 已经实现好的具备完整功能的 Target 子类,如果我们要进行自定义的话,通常只需要在两种 Target 的基础上去自定义就可以了,一种是 SimpleTarget,一种是 ViewTarget。
接下来我就分别以这两种 Target 来举例,学习一下自定义 Target 的功能。
首先来看 SimpleTarget,顾名思义,它是一种极为简单的 Target,我们使用它可以将 Glide 加载出来的图片对象获取到,而不是像之前那样只能将图片在 ImageView 上显示出来。
那么下面我们来看一下 SimpleTarget 的用法示例吧,其实非常简单:
SimpleTarget<GlideDrawable> simpleTarget = new SimpleTarget<GlideDrawable>() {
@Override
public void onResourceReady(GlideDrawable resource, GlideAnimation glideAnimation) {
imageView.setImageDrawable(resource);
}
};
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(simpleTarget);
}
怎么样?不愧是 SimpleTarget 吧,短短几行代码就搞了。这里我们创建了一个 SimpleTarget 的实例,并且指定它的泛型是 GlideDrawable,然后重写了 onResourceReady()方法。在 onResourceReady()方法中,我们就可以获取到 Glide 加载出来的图片对象了,也就是方法参数中传过来的 GlideDrawable 对象。有了这个对象之后你可以使用它进行任意的逻辑操作,这里我只是简单地把它显示到了 ImageView 上。
SimpleTarget 的实现创建好了,那么只需要在加载图片的时候将它传入到 into()方法中就可以了,现在运行一下程序,效果如下图所示。
虽然目前这个效果和直接在 into()方法中传入 ImageView 并没有什么区别,但是我们已经拿到了图片对象的实例,然后就可以随意做更多的事情了。
当然,SimpleTarget 中的泛型并不一定只能是 GlideDrawable,如果你能确定你正在加载的是一张静态图而不是 GIF 图的话,我们还能直接拿到这张图的 Bitmap 对象,如下所示:
SimpleTarget<Bitmap> simpleTarget = new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) {
imageView.setImageBitmap(resource);
}
};
public void loadImage(View view) {
String url = "http://cn.bing.com/az/hprichbg/rb/TOAD_ZH-CN7336795473_1920x1080.jpg";
Glide.with(this)
.load(url)
.asBitmap()
.into(simpleTarget);
}
可以看到,这里我们将 SimpleTarget 的泛型指定成 Bitmap,然后在加载图片的时候调用了 asBitmap()方法强制指定这是一张静态图,这样就能在 onResourceReady()方法中获取到这张图的 Bitmap 对象了。
好了,SimpleTarget 的用法就是这么简单,接下来我们学习一下 ViewTarget 的用法。
事实上,从刚才的继承结构图上就能看出,Glide 在内部自动帮我们创建的 GlideDrawableImageViewTarget 就是 ViewTarget 的子类。只不过 GlideDrawableImageViewTarget 被限定只能作用在 ImageView 上,而 ViewTarget 的功能更加广泛,它可以作用在任意的 View 上。
这里我们还是通过一个例子来演示一下吧,比如我创建了一个自定义布局 MyLayout,如下所示:
public class MyLayout extends LinearLayout {
private ViewTarget<MyLayout, GlideDrawable> viewTarget;
public MyLayout(Context context, AttributeSet attrs) {
super(context, attrs);
viewTarget = new ViewTarget<MyLayout, GlideDrawable>(this) {
@Override
public void onResourceReady(GlideDrawable resource, GlideAnimation glideAnimation) {
MyLayout myLayout = getView();
myLayout.setImageAsBackground(resource);
}
};
}
public ViewTarget<MyLayout, GlideDrawable> getTarget() {
return viewTarget;
}
public void setImageAsBackground(GlideDrawable resource) {
setBackground(resource);
}
}
在 MyLayout 的构造函数中,我们创建了一个 ViewTarget 的实例,并将 Mylayout 当前的实例 this 传了进去。ViewTarget 中需要指定两个泛型,一个是 View 的类型,一个图片的类型(GlideDrawable 或 Bitmap)。然后在 onResourceReady()方法中,我们就可以通过 getView()方法获取到 MyLayout 的实例,并调用它的任意接口了。比如说这里我们调用了 setImageAsBackground()方法来将加载出来的图片作为 MyLayout 布局的背景图。
接下来看一下怎么使用这个 Target 吧,由于 MyLayout 中已经提供了 getTarget()接口,我们只需要在加载图片的地方这样写就可以了:
public class MainActivity extends AppCompatActivity {
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()即可。现在重新运行一下程序,效果如下图所示。
好的,关于自定义 Target 的功能我们就介绍这么多,这些虽说都是自定义 Target 最基本的用法,但掌握了这些用法之后,你就能应对各种各样复杂的逻辑了。
preload()方法
===========
Glide 加载图片虽说非常智能,它会自动判断该图片是否已经有缓存了,如果有的话就直接从缓存中读取,没有的话再从网络去下载。但是如果我希望提前对图片进行一个预加载,等真正需要加载图片的时候就直接从缓存中读取,不想再等待慢长的网络加载时间了,这该怎么办呢?
对于很多 Glide 新手来说这确实是一个烦恼的问题,因为在没有学习本篇文章之前,into()方法中必须传入一个 ImageView 呀,而传了 ImageView 之后图片就显示出来了,这还怎么预加载呢?
不过在学习了本篇文章之后,相信你已经能够想到解决方案了。因为 into()方法中除了传入 ImageView 之后还可以传入 Target 对象,如果我们在 Target 对象的 onResourceReady()方法中做一个空实现,也就是不做任何逻辑处理,那么图片自然也就显示不出来了,而 Glide 的缓存机制却仍然还会正常工作,这样不就实现预加载功能了吗?
没错,上述的做法完全可以实现预加载功能,不过有没有感觉这种实现方式有点笨笨的。事实上,Glide 专门给我们提供了预加载的接口,也就是 preload()方法,我们只需要直接使用就可以了。
preload()方法有两个方法重载,一个不带参数,表示将会加载图片的原始尺寸,另一个可以通过参数指定加载图片的宽和高。
preload()方法的用法也非常简单,直接使用它来替换 into()方法即可,如下所示:
Glide.with(this)
.load(url)
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.preload();
需要注意的是,我们如果使用了 preload()方法,最好要将 diskCacheStrategy 的缓存策略指定成 DiskCacheStrategy.SOURCE。因为 preload()方法默认是预加载的原始图片大小,而 into()方法则默认会根据 ImageView 控件的大小来动态决定加载图片的大小。因此,如果不将 diskCacheStrategy 的缓存策略指定成 DiskCacheStrategy.SOURCE 的话,很容易会造成我们在预加载完成之后再使用 into()方法加载图片,却仍然还是要从网络上去请求图片这种现象。
调用了预加载之后,我们以后想再去加载这张图片就会非常快了,因为 Glide 会直接从缓存当中去读取图片并显示出来,代码如下所示:
Glide.with(this)
.load(url)
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.into(imageView);
注意,这里我们仍然需要使用 diskCacheStrategy()方法将硬盘缓存策略指定成 DiskCacheStrategy.SOURCE,以保证 Glide 一定会去读取刚才预加载的图片缓存。
preload()方法的用法大概就是这么简单,但是仅仅会使用显然层次有些太低了,下面我们就满足一下好奇心,看看它的源码是如何实现的。
和 into()方法一样,preload()方法也是在 GenericRequestBuilder 类当中的,代码如下所示:
public class GenericRequestBuil
der<ModelType, DataType, ResourceType, TranscodeType> implements Cloneable {
...
public Target<TranscodeType> preload(int width, int height) {
final PreloadTarget<TranscodeType> target = PreloadTarget.obtain(width, height);
return into(target);
}
public Target<TranscodeType> preload() {
return preload(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL);
}
...
}
正如刚才所说,preload()方法有两个方法重载,你可以调用带参数的 preload()方法来明确指定图片的宽和高,也可以调用不带参数的 preload()方法,它会在内部自动将图片的宽和高都指定成 Target.SIZE_ORIGINAL,也就是图片的原始尺寸。
然后我们可以看到,这里在第 5 行调用了 PreloadTarget.obtain()方法获取一个 PreloadTarget 的实例,并把它传入到了 into()方法当中。从刚才的继承结构图中可以看出,PreloadTarget 是 SimpleTarget 的子类,因此它是可以直接传入到 into()方法中的。
那么现在的问题就是,PreloadTarget 具体的实现到底是什么样子的了,我们看一下它的源码,如下所示:
public final class PreloadTarget<Z> extends SimpleTarget<Z> {
public static <Z> PreloadTarget<Z> obtain(int width, int height) {
return new PreloadTarget<Z>(width, height);
}
private PreloadTarget(int width, int height) {
super(width, height);
}
@Override
public void onResourceReady(Z resource, GlideAnimation<? super Z> glideAnimation) {
Glide.clear(this);
}
}
PreloadTarget 的源码非常简单,obtain()方法中就是 new 了一个 PreloadTarget 的实例而已,而 onResourceReady()方法中也没做什么事情,只是调用了 Glide.clear()方法。
这里的 Glide.clear()并不是清空缓存的意思,而是表示加载已完成,释放资源的意思,因此不用在这里产生疑惑。
其实 PreloadTarget 的思想和我们刚才提到设计思路是一样的,就是什么都不做就可以了。因为图片加载完成之后只将它缓存而不去显示它,那不就相当于预加载了嘛。
preload()方法不管是在用法方面还是源码实现方面都还是非常简单的,那么关于这个方法我们就学到这里。
downloadOnly()方法
================
一直以来,我们使用 Glide 都是为了将图片显示到界面上。虽然我们知道 Glide 会在图片的加载过程中对图片进行缓存,但是缓存文件到底是存在哪里的,以及如何去直接访问这些缓存文件?我们都还不知道。
其实 Glide 将图片加载接口设计成这样也是希望我们使用起来更加的方便,不用过多去考虑底层的实现细节。但如果我现在就是想要去访问图片的缓存文件该怎么办呢?这就需要用到 downloadOnly()方法了。
和 preload()方法类似,downloadOnly()方法也是可以替换 into()方法的,不过 downloadOnly()方法的用法明显要比 preload()方法复杂不少。顾名思义,downloadOnly()方法表示只会下载图片,而不会对图片进行加载。当图片下载完成之后,我们可以得到图片的存储路径,以便后续进行操作。
那么首先我们还是先来看下基本用法。downloadOnly()方法是定义在 DrawableTypeRequest 类当中的,它有两个方法重载,一个接收图片的宽度和高度,另一个接收一个泛型对象,如下所示:
downloadOnly(int width, int height)
downloadOnly(Y target)
这两个方法各自有各自的应用场景,其中 downloadOnly(int width, int height)是用于在子线程中下载图片的,而 downloadOnly(Y target)是用于在主线程中下载图片的。
那么我们先来看 downloadOnly(int width, int height)的用法。当调用了 downloadOnly(int width, int height)方法后会立即返回一个 FutureTarget 对象,然后 Glide 会在后台开始下载图片文件。接下来我们调用 FutureTarget 的 get()方法就可以去获取下载好的图片文件了,如果此时图片还没有下载完,那么 get()方法就会阻塞住,一直等到图片下载完成才会有值返回。
下面我们通过一个例子来演示一下吧,代码如下所示:
public void downloadImage(View view) {
new Thread(new Runnable() {
@Override
public void run() {
try {
String url = "http://cn.bing.com/az/hprichbg/rb/TOAD_ZH-CN7336795473_1920x1080.jpg";
final Context context = getApplicationContext();
FutureTarget<File> target = Glide.with(context)
.load(url)
.downloadOnly(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL);
final File imageFile = target.get();
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(context, imageFile.getPath(), Toast.LENGTH_LONG).show();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
这段代码稍微有一点点长,我带着大家解读一下。首先刚才说了,downloadOnly(int width, int height)方法必须要用在子线程当中,因此这里的第一步就是 new 了一个 Thread。在子线程当中,我们先获取了一个 Application Context,这个时候不能再用 Activity 作为 Context 了,因为会有 Activity 销毁了但子线程还没执行完这种可能出现。
接下来就是 Glide 的基本用法,只不过将 into()方法替换成了 downloadOnly()方法。downloadOnly()方法会返回一个 FutureTarget 对象,这个时候其实 Glide 已经开始在后台下载图片了,我们随时都可以调用 FutureTarget 的 get()方法来获取下载的图片文件,只不过如果图片还没下载好线程会暂时阻塞住,等下载完成了才会把图片的 File 对象返回。
最后,我们使用 runOnUiThread()切回到主线程,然后使用 Toast 将下载好的图片文件路径显示出来。
现在重新运行一下代码,效果如下图所示。
这样我们就能清晰地看出来图片完整的缓存路径是什么了。
之后我们可以使用如下代码去加载这张图片,图片就会立即显示出来,而不用再去网络上请求了:
public void loadImage(View view) {
String url = "http://cn.bing.com/az/hprichbg/rb/TOAD_ZH-CN7336795473_1920x1080.jpg";
Glide.with(this)
.load(url)
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.into(imageView);
}
需要注意的是,这里必须将硬盘缓存策略指定成 DiskCacheStrategy.SOURCE 或者 DiskCacheStrategy.ALL,否则 Glide 将无法使用我们刚才下载好的图片缓存文件。
现在重新运行一下代码,效果如下图所示。
可以看到,图片的加载和显示是非常快的,因为 Glide 直接使用的是刚才下载好的缓存文件。
那么这个 downloadOnly(int width, int height)方法的工作原理到底是什么样的呢?我们来简单快速地看一下它的源码吧。
首先在 DrawableTypeRequest 类当中可以找到定义这个方法的地方,如下所示:
public class DrawableTypeRequest<ModelType> extends DrawableRequestBuilder<ModelType>
implements DownloadOptions {
...
public FutureTarget<File> downloadOnly(int width, int height) {
return getDownloadOnlyRequest().downloadOnly(width, height);
}
private GenericTranscodeRequest<ModelType, InputStream, File> getDownloadOnlyRequest() {
return optionsApplier.apply(new GenericTranscodeRequest<ModelType, InputStream, File>(
File.class, this, streamModelLoader, InputStream.class, File.class, optionsApplier));
}
}
这里会先调用 getDownloadOnlyRequest()方法得到一个 GenericTranscodeRequest 对象,然后再调用它的 downloadOnly()方法,代码如下所示:
public class GenericTranscodeRequest<ModelType, DataType, ResourceType>
implements DownloadOptions {
...
public FutureTarget<File> downloadOnly(int width, int height) {
return getDownloadOnlyRequest().into(width, height);
}
private GenericRequestBuilder<ModelType, DataType, File, File> getDownloadOnlyRequest() {
ResourceTranscoder<File, File> transcoder = UnitTranscoder.get();
DataLoadProvider<DataType, File> dataLoadProvider = glide.buildDataProvider(dataClass, File.class);
FixedLoadProvider<ModelType, DataType, File, File> fixedLoadProvider =
new FixedLoadProvider<ModelType, DataType, File, File>(modelLoader, transcoder, dataLoadProvider);
return optionsApplier.apply(
new GenericRequestBuilder<ModelType, DataType, File, File>(fixedLoadProvider,
File.class, this))
.priority(Priority.LOW)
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.skipMemoryCache(true);
}
}
评论