前言
通过前面两篇的讲解,我们对于Glide的一些使用都有了基本了解,知道了使用Glide加载图片只需要一行代码即可:
Glide.with(this).load(url).into(imageView);
而在这一行代码的背后,Glide帮我们执行了成千上万行的逻辑。
项目需求:
1,将glide下载的缓存图片重新保存在本地系统相册
2,将Glide加载出来的图片对象获取到,在加载到ImageView上之前对该对象进行一些处理,比如宽高动态适配等,并且不是像之前那样只能将图片在ImageView上显示出来,任意的View都可以into。
3,Glide下载的图片缓存路径以及预加载
into()方法
使用了这么久的Glide,我们都知道into()方法中是可以传入ImageView的。那么into()方法还可以传入别的参数吗?我可以让Glide加载出来的图片不显示到ImageView上吗?答案是肯定的,这就需要用到自定义Target功能。
Target是什么?首先我们先看下 into(imageView)方法对应的源码:
通过源码的最后一行我们看到,执行了into(imageview)方法之后其实在底层调用了into(glide.buildImageViewTarget(view, transcodeClass));
方法,其中glide.buildImageViewTarget(view, transcodeClass)的作用就是创建一个Target对象,也就是说into()方法的执行其实是根据参数into()了一个Target,我们再看下glide.buildImageViewTarget()方法中的源码:
buildTarget()方法会根据传入的class参数来构建不同的Target对象,如果你在使用Glide加载图片的时候调用了asBitmap()方法,那么这里就会构建出BitmapImageViewTarget对象,否则的话构建的都是GlideDrawableImageViewTarget对象。至于上述代码中的DrawableImageViewTarget对象,这个通常都是用不到的,我们可以暂时不用管它。
通过上面的分析,我们已经知道了,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加载出来的图片对象resource了,有了这个对象之后你可以使用它进行任意的逻辑操作,这里我只是简单地把它显示到了ImageView上。
上面的代码执行的效果和直接into(ImageView imageView)的效果是一样的,但是通过上面的方式我们获得了图片对象的实例,然后就可以随意做更多的事情了。
当然,SimpleTarget中的泛型并不一定只能是GlideDrawable,如果你能确定你正在加载的是一张静态图而不是GIF图的话,我们还能直接拿到这张图的Bitmap对象,如下所示:
SimpleTarget<Bitmap> simpleTarget = new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) {
// 比如获取到图片的Bitmap对象之后就可以拿到原始图片的宽高,然后进行动态适配
int imageWidth = resource.getWidth(); // 获取到图片的宽度
int imageHeight = resource.getHeight(); // 获取到图片的高度
Log.d("test","原始图片的宽度为:"+imageWidth);
Log.d("test","原始图片的高度为:"+imageHeight);
// 如果宽高相等,拿到ImageView控件父容器的para对象
RelativeLayout.LayoutParams para = (RelativeLayout.LayoutParams) holder.rl.getLayoutParams();
// 动态设置imageview的宽高,然后再加载图片
// 获取到屏幕的宽度
DisplayMetrics dm = new DisplayMetrics();
mActivity.getWindowManager().getDefaultDisplay().getMetrics(dm); // 获取手机屏幕的大小
para.height = (int)(dm.widthPixels/3*1.7);
para.width = para.height*imageWidth/imageHeight;
// 加载图片
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对象了。
ViewTarget
从刚才的继承结构图上就能看出,Glide在内部自动帮我们创建的GlideDrawableImageViewTarget就是ViewTarget的子类。只不过GlideDrawableImageViewTarget被限定只能作用在ImageView上,而ViewTarget的功能更加广泛,它可以作用在任意的View上。
那么如何实现作用在任意View上的功能呢?案例如下:
1.比如我创建了一个自定义布局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()即可。现在重新运行一下程序,效果如下图所示。
preload()方法:预加载
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()方法有两个方法重载,你可以调用带参数的preload()方法来明确指定图片的宽和高,也可以调用不带参数的preload()方法,它会在内部自动将图片的宽和高都指定成Target.SIZE_ORIGINAL,也就是图片的原始尺寸。
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)是用于在主线程中下载图片的。
1,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直接使用的是刚才下载好的缓存文件。
2,downloadOnly(Y target)方法的用法
其实downloadOnly(int width, int height)方法必须使用在子线程当中,最主要还是因为它在内部帮我们自动创建了一个RequestFutureTarget,是这个RequestFutureTarget要求必须在子线程当中执行。而downloadOnly(Y target)方法则要求我们传入一个自己创建的Target,因此就不受RequestFutureTarget的限制了。
但是downloadOnly(Y target)方法的用法也会相对更复杂一些,因为我们又要自己创建一个Target了,而且这次必须直接去实现最顶层的Target接口,比之前的SimpleTarget和ViewTarget都要复杂不少。
那么下面我们就来实现一个最简单的DownloadImageTarget吧,注意Target接口的泛型必须指定成File对象,这是downloadOnly(Y target)方法要求的,代码如下所示:
public class DownloadImageTarget implements Target<File> {
private static final String TAG = "DownloadImageTarget";
@Override
public void onStart() {
}
@Override
public void onStop() {
}
@Override
public void onDestroy() {
}
@Override
public void onLoadStarted(Drawable placeholder) {
}
@Override
public void onLoadFailed(Exception e, Drawable errorDrawable) {
}
@Override
public void onResourceReady(File resource, GlideAnimation<? super File> glideAnimation) {
Log.d(TAG, resource.getPath());
}
@Override
public void onLoadCleared(Drawable placeholder) {
}
@Override
public void getSize(SizeReadyCallback cb) {
cb.onSizeReady(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL);
}
@Override
public void setRequest(Request request) {
}
@Override
public Request getRequest() {
return null;
}
}
由于是要直接实现Target接口,因此需要重写的方法非常多。这些方法大多是数Glide加载图片生命周期的一些回调,我们可以不用管它们,其中只有两个方法是必须实现的,一个是getSize()方法,一个是onResourceReady()方法。
通过对Glide的源码进行解析,我们知道Glide在开始加载图片之前会先计算图片的大小,然后回调到onSizeReady()方法当中,之后才会开始执行图片加载。而这里,计算图片大小的任务就交给我们了。只不过这是一个最简单的Target实现,我在getSize()方法中就直接回调了Target.SIZE_ORIGINAL,表示图片的原始尺寸。
然后onResourceReady()方法我们就非常熟悉了,图片下载完成之后就会回调到这里,我在这个方法中只是打印了一下下载的图片文件的路径。
这样一个最简单的DownloadImageTarget就定义好了,使用它也非常的简单,我们不用再考虑什么线程的问题了,而是直接把它的实例传入downloadOnly(Y target)方法中即可,如下所示:
public void downloadImage(View view) {
String url = "http://cn.bing.com/az/hprichbg/rb/TOAD_ZH-CN7336795473_1920x1080.jpg";
Glide.with(this)
.load(url)
.downloadOnly(new DownloadImageTarget());
}
现在重新运行一下代码并点击Download Image按钮,然后观察控制台日志的输出,结果如下图所示:
这样我们就使用了downloadOnly(Y target)方法同样获取到下载的图片文件的缓存路径了。
将glide下载的缓存图片保存在本地
在这里插入代码片项目需求:使用glide加载显示网络图片,然后在图片上长按出现保存到本地选项,点击可以保存到本地。
解决方案:因为是网络图片,所以保存到本地是否会有下载的过程??
Glide加载显示图片已经将图片下载并且缓存到了本地,我们只需将该缓存重新保存在手机系统相册即可,那么如何将glide的缓存
保存到本地呢?
实现的方法如下:(需要写入SD卡的权限)
// 思路:从glide中获取到指定url的bitmap对象,然后将其转换成二进制再写入本地,获取bitmap必须放在子线程中来完成,
@NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
void saveImage(final String s1) {
//将ImageView中的图片转换成Bitmap
/*iv.buildDrawingCache();
Bitmap bitmap = iv.getDrawingCache();*/
new Thread(new Runnable() {
@Override
public void run() {
Bitmap bitmap = null;
try {
bitmap = Glide.with(ImageActivity.this)
.load(image)
.asBitmap()
.into(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
if(bitmap != null){
//将Bitmap 转换成二进制,写入本地
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
byte[] byteArray = stream.toByteArray();
File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/yiyou");
Log.d("test","dir的路径地址为:"+dir.getPath());
if (!dir.isFile()) {
dir.mkdir();
}
/*// 获取当前时间戳字符串,精确到毫秒
SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMddHHmm"); //精确到毫秒
String suffix = fmt.format(new Date());*/
String s = s1;
File file = new File(dir, s);
Message msg = new Message();
msg.what = 0x22;
msg.obj = file.getPath();
mHandler.sendMessage(msg);
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(byteArray, 0, byteArray.length);
fos.flush();
//用广播通知相册进行更新相册
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
Uri uri = Uri.fromFile(file);
intent.setData(uri);
sendBroadcast(intent);
// Toast.makeText(ImageActivity.this, "图片保存成功,保存路径为"+, Toast.LENGTH_SHORT).show();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}).start();
}
listener()方法:监听Glide加载图片的状态
其实listener()方法的作用非常普遍,它可以用来监听Glide加载图片的状态。举个例子,比如说我们刚才使用了preload()方法来对图片进行预加载,但是我怎样确定预加载有没有完成呢?还有如果Glide加载图片失败了,我该怎样调试错误的原因呢?答案都在listener()方法当中。
首先来看下listener()方法的基本用法吧,不同于刚才几个方法都是要替换into()方法的,listener()是结合into()方法一起使用的,当然也可以结合preload()方法一起使用。最基本的用法如下所示:
public void loadImage(View view) {
String url = "http://cn.bing.com/az/hprichbg/rb/TOAD_ZH-CN7336795473_1920x1080.jpg";
Glide.with(this)
.load(url)
.listener(new RequestListener<String, GlideDrawable>() {
@Override
public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) {
return false;
}
@Override
public boolean onResourceReady(GlideDrawable resource, String model,
Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
return false;
}
})
.into(imageView);
}
这里我们在into()方法之前串接了一个listener()方法,然后实现了一个RequestListener的实例。其中RequestListener需要实现两个方法,一个onResourceReady()方法,一个onException()方法。从方法名上就可以看出来了,当图片加载完成的时候就会回调onResourceReady()方法,而当图片加载失败的时候就会回调onException()方法,onException()方法中会将失败的Exception参数传进来,这样我们就可以定位具体失败的原因了。
没错,listener()方法就是这么简单。不过还有一点需要处理,onResourceReady()方法和onException()方法都有一个布尔值的返回值,返回false就表示这个事件没有被处理,还会继续向下传递,返回true就表示这个事件已经被处理掉了,从而不会再继续向下传递。举个简单点的例子,如果我们在RequestListener的onResourceReady()方法中返回了true,那么就不会再回调Target的onResourceReady()方法了。
如果我们在onException()方法中返回了true,那么Glide请求中使用error(int resourceId)方法设置的异常占位图就失效了。