Android上的图片加载框架已经非常成熟,从最早的老牌图片加载框架UniversalImageLoader,到后来Google推出的Volley,再到后来的新兴军Glide和Picasso,当然还有Facebook的Fresco。从易用性上来讲,Glide和Picasso应该都是完胜其他框架的,这两个框架都实在是太简单好用了,大多数情况下加载图片都是一行代码就能解决的。
那么再拿Glide和Picasso对比呢,首先这两个框架的用法非常相似,但其实它们各有特色。Picasso比Glide更加简洁和轻量,Glide比Picasso功能更为丰富。之前已经有人对这两个框架进行过全方面的对比,大家如果想了解更多的话可以去参考一下这篇文章Google推荐的图片加载库Glide介绍 。本篇文章我们主要讲讲Glide的使用技巧。
导入
studio下,在app/build.gradle文件当中添加如下依赖:
compile 'com.github.bumptech.glide:glide:3.7.0'
需要support-v4库的支持,如果你的项目没有support-v4库(项目默认已经添加了),还需要添加support-v4依赖:
compile 'com.android.support:support-v4:23.3.0'
Eclipse下,需要下载jar包,地址。
权限:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
使用
基本用法
private String url = "https://img-my.csdn.net/uploads/201309/01/1378037128_5291.jpg";
...
load.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
Glide.with(MainActivity.this)
.load(url)
.into(img);
}
Glide.with()使用
- with(Context context)
使用Application上下文,Glide请求将不受Activity/Fragment生命周期控制。 - with(Activity activity)
使用Activity作为上下文,Glide的请求会受到Activity生命周期控制。 - with(FragmentActivity activity)
Glide的请求会受到FragmentActivity生命周期控制。 - with(android.app.Fragment fragment)
Glide的请求会受到Fragment 生命周期控制。 - with(android.support.v4.app.Fragment fragment)
Glide的请求会受到Fragment生命周期控制。
返回关联了相应上下文的RequestManager实例。
requestManager.load()使用
Glide基本可以load任何可以拿到的媒体资源,如:
- load SD卡资源
load("file://"+ Environment.getExternalStorageDirectory().getAbsolutePath()+"/test.jpg")
- load assets资源
load("file:///android_asset/fc.gif")
- load res/raw资源
load("android.resource://包名/raw/sss")
load("android.resource://包名/raw/"+R.raw.sss)
- load drawable资源
load("android.resource://包名/drawable/ttt")
load("android.resource://包名/drawable/"+R.drawable.ttt)
- load ContentProvider资源
load("content://media/external/images/media/139469")
- load http资源
load("https://img-my.csdn.net/uploads/201309/01/1378037128_5291.jpg")
- load https资源
load("https://img.alicdn.com/tps/TB1uyhoMpXXXXcLXVXXXXXXXXXX-476-538.jpg_240x5000q50.jpg_.webp")
当然,load不限于String类型,还可以:
load(Uri uri),load(File file),load(Integer resourceId),load(URL url),load(byte[] model),load(T model),loadFromMediaStore(Uri uri)。
load的资源也可以是本地视频,如果想要load网络视频或更高级的操作可以使用VideoView等其它控件完成。
返回GenericRequestBuilder实例。
GenericRequestBuilder.into()使用
into(Y target).//设置资源将被加载到的Target。
into(ImageView view). //设置资源将被加载到的ImageView。取消该ImageView之前所有的加载并释放资源。
into(int width, int height). //后台线程加载时要加载资源的宽高值(单位为pixel)。
扩展用法
GenericRequestBuilder用于处理选项设置和开始一般resource类型资源的加载。
设置占位图和错误图
placeholder(int resourceId).//设置资源加载过程中的占位Drawable。
placeholder(Drawable drawable).//设置资源加载过程中的占位Drawable。
error(int resourceId).//设置load失败时显示的Drawable。
error(Drawable drawable).//设置load失败时显示的Drawable。
fallback(int resourceId). //设置model为空时要显示的Drawable。如果没设置fallback,model为空时将显示error的Drawable,如果error的Drawable也没设置,就显示placeholder的Drawable。
fallback(Drawable drawable).//设置model为空时显示的Drawable。
设置缓存策略
diskCacheStrategy(DiskCacheStrategy strategy).//设置缓存策略。
- DiskCacheStrategy.SOURCE:缓存原始数据
- DiskCacheStrategy.RESULT:缓存变换(如缩放、裁剪等)后的资源数据
- DiskCacheStrategy.NONE:什么都不缓存
- DiskCacheStrategy.ALL:缓存SOURC和RESULT
默认采用DiskCacheStrategy.RESULT策略,对于download only操作要使用DiskCacheStrategy.SOURCE。
清空缓存
//禁止内存缓存(默认开启):
skipMemoryCache(true)
//清除内存缓存,必须在UI线程中调用
Glide.get(context).clearMemory();
//禁止磁盘缓存(默认开启):
diskCacheStrategy(DiskCacheStrategy.NONE)
//清除磁盘缓存,必须在后台线程中调用,建议同时clearMemory()
Glide.get(applicationContext).clearDiskCache();
设置请求优先级
priority(Priority priority). //指定加载的优先级,优先级越高越优先加载,但不保证所有图片都按序加载。
枚举:
- Priority.IMMEDIATE
- Priority.HIGH
- Priority.NORMAL
- Priority.LOW
默认为Priority.NORMAL。
设置缩略图
thumbnail(float sizeMultiplier). //请求给定系数的缩略图。如果缩略图比全尺寸图先加载完,就显示缩略图,否则就不显示。系数sizeMultiplier必须在(0,1)之间,可以递归调用该方法。
/**用原图的1/10作为缩略图*/
Glide.with(this)
.load(url)
.thumbnail(0.1f)
.into(iv);
//用其它图片作为缩略图
DrawableRequestBuilder<Integer> thumbnailRequest = Glide.with(this)
.load(R.drawable.news);
Glide.with(this)
.load(url)
.thumbnail(thumbnailRequest)
.into(iv);
设置动画效果
animate(int animationId). //在异步加载资源完成时会执行该动画(从内存或硬盘获取到图片并不执行动画)。
animate(ViewPropertyAnimation.Animator animator). //在异步加载资源完成时会执行该动画(从内存或硬盘获取到图片并不执行动画)。
Glide自带的淡入淡出动画
.crossFade() //淡入淡出,也是默认动画
.crossFade(int duration) //定义淡入淡出的时间间隔
我们还可以禁用动画
dontAnimate(). //移除所有的动画。
设置图片的宽高
override(int width, int height). //重新设置图片的宽高值(单位为pixel)。
用Glide时,如果图片不需要自动适配ImageView,调用override(horizontalSize, verticalSize),它会在将图片显示在ImageView之前调整图片的大小。
这个设置可能也是有利于没有明确目标,但已知尺寸的视图上。
缩放图片scaletype
这里还可以结合scaletype使用使图片呈现不同的显示效果:
centerInside()
center()
centerCrop() //缩放图片让图片充满整个ImageView的边框,然后裁掉超出的部分。ImageView会被完全填充满,但是图片可能不能完全显示出。
fitCenter() //缩放图片让两边都相等或小于ImageView的所需求的边框。图片会被完整显示,可能不能完全填充整个ImageView。
加载Gif
/**普通显示GIF*/
Glide.with(context).load(gifUrl).into(iv);
/**控制GIF动画次数*/
Glide.with(context).load(gifUrl).into(new GlideDrawableImageViewTarget(iv, 1));
//添加GIF检查,如果不是GIF就会显示加载失败位图
Glide.with(context).load(gifUrl).asGif().into(iv);
注意:加载gif的时候需要将硬盘缓存策略指定成DiskCacheStrategy.SOURCE。否则gif加载特别缓慢,甚至加载不出来。
asBitmap()/asGif()
asBitmap(). //无论资源是不是gif动画,都作为Bitmap对待。如果是gif动画会停在第一帧。
asGif(). //把资源作为GifDrawable对待。如果资源不是gif动画将会失败,会回调.error()。
显示本地视频
String filePath ="/storage/emulated/0/Pictures/example_video.mp4";
Glide.with(context)
.load(Uri.fromFile(new File(filePath)))
.into(iv);
重要提示:这里只对本地视频有效。对于并非存在本地的视频(如网络URL)并不支持!如果你想要从网络URL播放视频,可以使用VideoView。
预加载preload
preload(int width, int height). //预加载resource到缓存中(单位为pixel)
preload().//将会预加载图片的原始尺寸
需要注意的是,我们如果使用了preload()方法,最好要将diskCacheStrategy的缓存策略指定成DiskCacheStrategy.SOURCE。因为这样preload()方法默认是预加载的原始图片大小,而into()方法则默认会根据ImageView控件的大小来动态决定加载图片的大小。否则很容易会造成我们在预加载完成之后再使用into()方法加载图片,却仍然还是要从网络上去请求图片这种现象。
同时,之后仍然需要使用diskCacheStrategy()方法将硬盘缓存策略指定成DiskCacheStrategy.SOURCE,以保证Glide一定会去读取刚才预加载的图片缓存。
仅下载downloadOnly
和preload()方法类似,downloadOnly()方法也是可以替换into()方法的,不过downloadOnly()方法的用法明显要比preload()方法复杂不少。顾名思义,downloadOnly()方法表示只会下载图片,而不会对图片进行加载。当图片下载完成之后,我们可以得到图片的存储路径,以便后续进行操作。
//它有两个方法重载,一个接收图片的宽度和高度,另一个接收一个泛型对象
downloadOnly(int width, int height) //用于在子线程中下载图片
downloadOnly(Y target) //用于在主线程中下载图片
(1)当调用了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 = "https://img-my.csdn.net/uploads/201309/01/1378037128_5291.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();
}
(2)downloadOnly(Y target)方法的用法也会相对更复杂一些,因为我们又要自己创建一个Target了。下面我们就来实现一个最简单的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()方法。在getSize()方法中就直接回调了Target.SIZE_ORIGINAL,表示图片的原始尺寸。然后onResourceReady()方法我们就非常熟悉了,图片下载完成之后就会回调到这里,我在这个方法中只是打印了一下下载的图片文件的路径。
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());
}
现在重新运行一下代码并加载图片,同样打出了图片文件的路径。
Glide监听
listener()是结合into()方法一起使用的,当然也可以结合preload()方法一起使用。最基本的用法如下所示:
首先,创建一个listener作为一个字段对象,避免被垃圾回收:
private RequestListener<String, GlideDrawable> requestListener = new RequestListener<String, GlideDrawable>() {
@Override
public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) {
//可以抓取问题,并决定你需要做什么,比如记录日志
//如果Glide应当处理这个后果,比如显示一个出错占位图,在onException方法中返回false是很重要的。
return false;
}
@Override
public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
return false;
}
};
然后在Glide中的构造方法里设置listener:
Glide
.with( context )
.load(url)
.listener(requestListener)
.error(R.drawable.fail)
.into(iv);
当图片加载完成的时候就会回调onResourceReady()方法,而当图片加载失败的时候就会回调onException()方法,onException()方法中会将失败的Exception参数传进来,这样我们就可以定位具体失败的原因了。
不过还有一点需要处理,onResourceReady()方法和onException()方法都有一个布尔值的返回值,返回false就表示这个事件没有被处理,还会继续向下传递,返回true就表示这个事件已经被处理掉了,从而不会再继续向下传递。举个简单点的例子,如果我们在RequestListener的onResourceReady()方法中返回了true,那么就不会再回调Target的onResourceReady()方法了。
图片的变换
推荐使用独立的图片处理库:wasabeef/glide-transformations,这个库有2个不同版本。扩展库包括更多的变换,并且是用手机的GPU进行计算。需要一个额外的依赖,所以这两个版本的设置还有点不一样。你应当看看支持的变换的列表,再决定你需要用哪个版本。
设置是很简单的!对于基本版,你可以在你的build.gradle里加一行:
compile 'jp.wasabeef:glide-transformations:2.0.0'
如果你想要使用GPU变换:
repositories {
jcenter()
mavenCentral()
}
dependencies {
compile 'jp.wasabeef:glide-transformations:2.0.0'
compile 'jp.co.cyberagent.android.gpuimage:gpuimage-library:1.3.0'
}
Glide有两个不同的方式进行变换。第一个是传递一个你的类的实例作为.transform()的参数。不管是图片还是gif,都可以进行变换。另一个则是使用.bitmapTransform(),它只接受bitmap的变换。
之后我们就可以使用GenericRequestBuilder或其子类的transform()或bitmapTransform()方法设置图片转换了:
//圆形裁剪
.bitmapTransform(new CropCircleTransformation(this))
//圆角处理
.bitmapTransform(new RoundedCornersTransformation(this,30,0, RoundedCornersTransformation.CornerType.ALL))
//灰度处理
.bitmapTransform(new GrayscaleTransformation(this))
//高斯模糊
.bitmapTransform(new BlurTransformation(this))
//遮盖效果
.bitmapTransform(new MaskTransformation(this,R.mipmap.ic_launcher))
//卡通过滤效果
.bitmapTransform(new ToonFilterTransformation(this))
//加深颜色过滤效果
.bitmapTransform(new SepiaFilterTransformation(this))
//马赛克效果
.bitmapTransform(new PixelationFilterTransformation(this))
//其他效果:
SepiaFilterTransformation
ContrastFilterTransformation
InvertFilterTransformation
SketchFilterTransformation
SwirlFilterTransformation
BrightnessFilterTransformation
KuwaharaFilterTransformation
VignetteFilterTransformation
通常,Glide的流接口(fluent interface)允许方法被连接在一起,然而变换并不是这样的。确保你只调用.transform()或者.bitmapTransform()一次,不然,之前的设置将会被覆盖!然而,你可以通过传递多个转换对象当作参数到.transform()(或者.bitmapTransform())中来进行多重变换:
Glide
.with(this)
.load(url)
.transform(new GreyscaleTransformation(this), new BlurTransformation(this))
.into(iv);
这段代码中,我们先对图片进行了灰度变换,然后模糊处理。Glide会为你自动进行两个转换。厉害吧。
提示:当你使用变换的时候,你不能使用.centerCrop()或者.fitCenter()。
当然如果想自己写Transformation,最简单的方式就是继承BitmapTransformation,下面我们实现一个旋转变换:
private static class RotateTransformation extends BitmapTransformation {
private float rotateRotationAngle = 0f;
public RotateTransformation(Context context, float rotateRotationAngle) {
super( context );
this.rotateRotationAngle = rotateRotationAngle;
}
@Override
protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
//对Bitmap进行各种变换处理。
Matrix matrix = new Matrix();
matrix.postRotate(rotateRotationAngle);
return Bitmap.createBitmap(toTransform, 0, 0, toTransform.getWidth(), toTransform.getHeight(), matrix, true);
}
@Override
public String getId() {
// 返回代表该变换的唯一Id,会作为cache key的一部分。
// 注意:最好不要用getClass().getName(),因为容易受混淆影响。如果变换过程不影响缓存数据,可以返回空字符串。
return "rotate" + rotateRotationAngle;
}
}
使用时只需使用transform()或bitmapTransform()方法即可:
Glide.with(context)
.load(url)
.asBitmap()
.transform( new RotateTransformation(context, 90f))
.into(iv);
定制view中使用SimpleTarget和ViewTarget
假设我们并没有ImageView作为图片加载的目标。我们只需要Bitmap本身。Glide提供了一个用Target获取Bitmap资源的方法。Target只是用来回调,它会在所有的加载和处理完毕时返回想要的结果。Glide提供了多种多样有各自明确目的Target。我们先从SimpleTarget介绍。
SimpleTarget
private SimpleTarget target = new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(Bitmap bitmap, GlideAnimation glideAnimation) {
// do something with the bitmap
}
};
private void loadImageSimpleTarget() {
Glide
// .with(context) // could be an issue!
.with(context.getApplicationContext()) // safer!
.load(url)
.asBitmap() //强制返回一个Bitmap对象
.into(target);
}
这里需要注意几件事:
第一个是SimpleTarget对象的定义。java/Android可以允许你在.into()内匿名定义,但这会显著增加在Glide处理完图片请求前Android垃圾回收清理匿名target对象的可能性。最终,会导致图片被加载了,但是回调永远不会被调用。所以,请确保将你的回调定义为一个字段对象,防止被万恶的Android垃圾回收给清理掉。
第二个关键部分是Glide的.with( context )。这个问题实际上是Glide一个特性问题:当你传递了一个context,例如当前app的activity,当activity停止后,Glide会自动停止当前的请求。如果你的target是独立于app的生命周期。这里的解决方案是使用application的context:.with( context.getApplicationContext() )。当app自己停止运行的时候,Glide才会取消掉图片的请求。
第三个潜在问题是Target没有一个明确的大小。如果你传递一个ImageView作为.into()的参数,Glide会使用ImageView的大小来限制图片的大小。例如如果要加载的图片是1000x1000像素,但是ImageView的尺寸只有250x250像素,Glide会降低图片到小尺寸,以节省处理时间和内存。显然,由于target没有具体大小,这对target并不起效。但是,如果你有个期望的具体大小,你可以增强回调。如果你知道图片应当为多大,那么在你的回调定义里应当指明,以节省内存:
//像素为单位
private SimpleTarget target = new SimpleTarget<Bitmap>(250, 250) {
@Override
public void onResourceReady(Bitmap bitmap, GlideAnimation glideAnimation) {
// do something with the bitmap
}
};
其实还有另外一种方法可以获取Bitmap:
Bitmap bitmap = Glide.with(MainActivity.this).load(url).asBitmap().into(250, 250).get();
该方式只能在子线程中获得。
ViewTarget
有很多原因导致我们不能直接使用ImageView。前面已经介绍了如何获取Bitmap。现在,我们将更深入学习。假设你有个自定义的View。由于没有已知的方法在哪里设置图片,Glide并不支持加载图片到定制的View内。然而用ViewTarget会让这个更简单。
让我们看一个简单的定制View,它继承于FrameLayout,内部使用了一个ImageView:
public class FutureStudioView extends FrameLayout {
ImageView iv;
public void initialize(Context context) {
inflate(context, R.layout.custom_view_futurestudio, this);
iv = (ImageView) findViewById(R.id.custom_view_image);
}
public FutureStudioView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context);
}
public FutureStudioView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize(context);
}
public void setImage(Drawable drawable) {
iv.setImageDrawable(drawable);
}
}
由于我们定制的view并不是继承自ImageView,这里不能使用常规的.into()方法。因此,我们只能创建一个ViewTarget,用来传递给.into()方法:
private void loadImageViewTarget() {
FutureStudioView customView = (FutureStudioView) findViewById(R.id.custom_view);
viewTarget = new ViewTarget<FutureStudioView, GlideDrawable>(customView) {
@Override
public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> glideAnimation) {
this.view.setImage(resource.getCurrent());
}
};
Glide
.with( context.getApplicationContext() ) // safer!
.load(url)
.into(viewTarget);
}
整合网络协议栈
Glide的开发者不强迫你使用他们推荐的网络库。所以,Glide是无关HTTP/S的。理论上,它能实现基本的网络功能,在任何情况下工作。它需要一个Glide的ModelLoader的接口设置。为了让这个更简单,Glide提供了两个网络库的支持:OkHttp 和 Volley。
OkHttp 2
我们假设你想要用OkHttp 2作为你的Glide网络库。可以手动通过定义一个GlideModule来实现整合。如果你想要避免手动整合,那就打开你的build.gradle,然后添加下面的依赖:
dependencies {
// your other dependencies
// ...
// Glide
compile 'com.github.bumptech.glide:glide:3.7.0'
// Glide's OkHttp2 Integration
compile 'com.github.bumptech.glide:okhttp-integration:1.4.0@aar'
compile 'com.squareup.okhttp:okhttp:2.7.5'
}
Gradle会自动合入必须的GlideModule到你的Android.Manifest中。Glide会识别manifest里的东西,并为所有的网络连接使用OkHttp。
Volley
另一方面,如果你偏向于使用Volley,你必须把你的build.gradle依赖改成下面的:
dependencies {
// your other dependencies
// ...
// Glide
compile 'com.github.bumptech.glide:glide:3.7.0'
// Glide's Volley Integration
compile 'com.github.bumptech.glide:volley-integration:1.4.0@aar'
compile 'com.mcxiaoke.volley:library:1.0.8'
}
这会添加Volley和集成库(integration library)到你的项目中。集成库添加GlideModule到你的Android.Manifest文件中。Glide会自动识别,然后使用Volley作为网络连接库。没有其他的配置要做了!
警号:如果你同时定义了2个库在你的build.gradle,两个都会被添加。由于Glide不能按照任何特别的顺序加载它们,不知道哪个库实际上被调用,可能会导致不稳定的结果。请确保你只添加了一个集成库。
OkHttp 3
如果你想要使用最新的OkHttp 3作为网络协议栈,通过提供的集成库整个它:
dependencies {
// your other dependencies
// ...
// Glide
compile 'com.github.bumptech.glide:glide:3.7.0'
// Glide's OkHttp3 Integration
compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
compile 'com.squareup.okhttp3:okhttp:3.2.0'
}
目前Glide并不支持除了Volley、OkHttp2和OkHttp3以外的库。
自定义 GlideModule
上面,我们介绍了如何设置一些网络协议栈去加载图片。本质上,集成网络库没啥其他特殊的,就是定义一个GlideModule来定制Glide的行为。接下来我们会对Glide Modules进行概述。
Glide modules是一个全局改变Glide行为的抽象的方式。你需要创建Glide的实例,来访问GlideBuilder。可以通过创建一个公共的类,实现GlideModule的接口来定制Glide:
public class SimpleGlideModule implements GlideModule {
@Override public void applyOptions(Context context, GlideBuilder builder) {
// todo
}
@Override public void registerComponents(Context context, Glide glide) {
// todo
}
}
你已经知道需要创建一个额外的类去自定义Glide。下一步是要在全局中声明这个类,这样Glide知道它应该加载并使用它。Glide会扫描AndroidManifest.xml的Glide modules的meta定义。这样,你必须在AndroidManifest.xml里的<\application>标签下声明刚创建的Glide module。
<manifest
...
<application>
<meta-data android:name=android:name="com.hx.glide.CustomCachingGlideModule"
android:value="GlideModule" />
...
</application>
</manifest>
确保你设置android:name为你自己的包名+类名,这样才能正确引用。你不需要添加其他的代码到其中。如果你想要禁止Glide Module,只要从AndroidManifest.xml里移除它。Java类里的代码可以留着供以后使用。当在AndroidManifest.xml里没有引用的时候,它永远不会被加载。
applyOptions
现在我们看一下接口的第一个方法:applyOptions(Context context, GlideBuilder builder)。这个方法将GlideBuilder的对象当作参数,并且是void返回类型,所以你在这个方法里能调用GlideBuilder可以用的方法。
.setMemoryCache(MemoryCache memoryCache)
.setBitmapPool(BitmapPool bitmapPool)
.setDiskCache(DiskCache.Factory diskCacheFactory)
.setDiskCacheService(ExecutorService service)
.setResizeService(ExecutorService service)
.setDecodeFormat(DecodeFormat decodeFormat)
显而易见,GlideBuilder对象可以让你访问到Glide的核心部分。使用文中的方法,你可以改变磁盘缓存、内存缓存等等。
自定义内存缓存:
public class CustomCachingGlideModule implements GlideModule {
@Override public void applyOptions(Context context, GlideBuilder builder) {
//Glide内存使用MemorySizeCalculator类去决定内存缓存和bitmap池的大小
MemorySizeCalculator calculator = new MemorySizeCalculator(context);
int defaultMemoryCacheSize = calculator.getMemoryCacheSize();
int defaultBitmapPoolSize = calculator.getBitmapPoolSize();
int customMemoryCacheSize = (int) (1.2 * defaultMemoryCacheSize);
int customBitmapPoolSize = (int) (1.2 * defaultBitmapPoolSize);
//自定义内存缓存
builder.setMemoryCache(new LruResourceCache( customMemoryCacheSize));
builder.setBitmapPool(new LruBitmapPool( customBitmapPoolSize));
}
@Override public void registerComponents(Context context, Glide glide) {
// nothing to do here
}
}
自定义磁盘缓存:
public class CustomCachingGlideModule implements GlideModule {
@Override public void applyOptions(Context context, GlideBuilder builder) {
//设置最大值到100M
int cacheSize100MegaBytes = 104857600;
builder.setDiskCache(new InternalCacheDiskCacheFactory(context,cacheSize100MegaBytes));
//builder.setDiskCache(new ExternalCacheDiskCacheFactory(context, cacheSize100MegaBytes));
}
@Override public void registerComponents(Context context, Glide glide) {
// nothing to do here
}
}
上面的两个方案都不让你选择一个特定的路径。如果你需要移动磁盘缓存到某个特定的位置,你可以利用DiskLruCacheFactory:
...
/**自定义磁盘缓存路径*/
String downloadDirectoryPath = Environment.getDownloadCacheDirectory().getPath();
builder.setDiskCache(new DiskLruCacheFactory(downloadDirectoryPath, cacheSize100MegaBytes));
自定义缓存实现策略:
到目前为止,我们已经展示了如何移动,设置缓存大小。然而,所有调用指向缓存的原始实现。如果你有自己的缓存实现呢?
好吧,你已经知道我们总是创建一个Glide的默认缓存实现实例。你可以创建它的实例并在上面看到的所有方法里传递它来完成你自己的实现。你只要确保你的缓存代码实现了接口方法:
- Memory cache needs to implement: MemoryCache
- Bitmap pool needs to implement BitmapPool
- Disk cache needs to implement: DiskCache
registerComponents
使用ModelLoader自定义数据源:
例如我们使用了七牛云存储,要根据不同的要求请求不同尺寸不同质量的图片,这时我们就可以使用自定义数据源
1.)定义处理URL接口
public interface IDataModel {
String buildDataModelUrl(int width, int height);
}
2.)实现处理URL接口
JpgDataModel:
public class JpgDataModel implements IDataModel {
private String dataModelUrl;
public JpgDataModel(String dataModelUrl) {
this.dataModelUrl = dataModelUrl;
}
@Override
public String buildDataModelUrl(int width, int height) {
//http://78re52.com1.z0.glb.clouddn.com/resource/gogopher.jpg?imageView2/1/w/200/h/200/format/jpg
return String.format("%s?imageView2/1/w/%d/h/%d/format/jpg", dataModelUrl, width, height);
}
}
WebpDataModel:
public class WebpDataModel implements IDataModel {
private String dataModelUrl;
public WebpDataModel(String dataModelUrl) {
this.dataModelUrl = dataModelUrl;
}
@Override
public String buildDataModelUrl(int width, int height) {
//http://78re52.com1.z0.glb.clouddn.com/resource/gogopher.jpg?imageView2/1/w/200/h/200/format/webp
return String.format("%s?imageView2/1/w/%d/h/%d/format/webp", dataModelUrl, width, height);
}
}
3.)实现ModelLoader
public class MyDataLoader extends BaseGlideUrlLoader<IDataModel> {
public MyDataLoader(Context context) {
super(context);
}
public MyDataLoader(ModelLoader<GlideUrl, InputStream> urlLoader) {
super(urlLoader, null);
}
@Override
protected String getUrl(IDataModel model, int width, int height) {
return model.buildDataModelUrl(width, height);
}
/**
*/
public static class Factory implements ModelLoaderFactory<IDataModel, InputStream> {
@Override
public ModelLoader<IDataModel, InputStream> build(Context context, GenericLoaderFactory factories) {
return new MyDataLoader(factories.buildModelLoader(GlideUrl.class, InputStream.class));
}
@Override
public void teardown() {
}
}
}
4.)根据不同的要求采用不同的策略加载图片
//加载jpg图片
Glide.with(this).using(new MyDataLoader(this)).load(new JpgDataModel(imageUrl)).into(imageView);
//加载webp图片
Glide.with(this).using(new MyDataLoader(this)).load(new WebpDataModel(imageUrl)).into(imageView);
5.)如何跳过.using()
public class MyGlideModule implements GlideModule {
...
@Override
public void registerComponents(Context context, Glide glide) {
glide.register(IDataModel.class, InputStream.class, new MyDataLoader.Factory());
}
}
上面的实现跳过using()
//加载jpg图片
Glide.with(this).load(new JpgDataModel(imageUrl)).into(imageView);
//加载webp图片
Glide.with(this).load(new WebpDataModel(imageUrl)).into(imageView);
再举一个例子,使用自定义GlideModule实现QQ讨论组和微信群聊头像。
(1)
<meta-data
android:name="com.example.utils.GlideConfiguration"
android:value="GlideModule" />
(2)
public class GlideConfiguration implements GlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
ViewTarget.setTagId(R.id.glide_tag);
builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888);
builder.setDiskCache(new DiskLruCacheFactory(DataConstant.ICENTER_PICTURE_DIR, 100 * 1024 * 1024));
}
@Override
public void registerComponents(Context context, Glide glide) {
Class<List<String>> clazz = (Class) List.class;
glide.register(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
glide.register(clazz, InputStream.class, new GroupAvatarModelLoader.Factory());
}
}
(3)
public class GroupAvatarModelLoader implements ModelLoader<List<String>, InputStream> {
private final Context context;
private GroupAvatarModelLoader(Context context) {
this.context = context;
Log.d(GroupAvatarLoader.TAG, "GroupAvatarModelLoader init success.");
}
@Override
public DataFetcher<InputStream> getResourceFetcher(List<String> model, int width, int height) {
return new GroupAvatarFetcher(this.context, model, width, height);
}
public static class Factory implements ModelLoaderFactory<List<String>, InputStream> {
@Override
public ModelLoader<List<String>, InputStream> build(Context context, GenericLoaderFactory factories) {
return new GroupAvatarModelLoader(context);
}
@Override
public void teardown() {
// Do nothing.
}
}
}
(4)
class GroupAvatarFetcher implements DataFetcher<InputStream> {
private final Context context;
private final List<String> avatarList;
private final int width, height;
private InputStream resultStream;
public GroupAvatarFetcher(Context context, @NonNull List<String> avatarList, int width, int height) {
this.context = context;
this.avatarList = new ArrayList<>();
this.avatarList.addAll(avatarList);
this.width = width;
this.height = height;
}
@Override
public InputStream loadData(Priority priority) throws Exception {
List<Bitmap> bitmaps = new ArrayList<>();
for (String string : avatarList) {
bitmaps.add(Glide.with(context).load(string).asBitmap().into(width, height).get());
}
if (bitmaps.size() == 0) {
return null;
}
int dimension = width;
Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(output);
int count = Math.min(bitmaps.size(), GroupAvatarJoinLayout.max());
float[] size = GroupAvatarJoinLayout.size(count);
// 旋转角度
float[] rotation = GroupAvatarJoinLayout.rotation(count);
if (null == size || null == rotation) {
return null;
}
// paint
Paint paint = new Paint();
paint.setAntiAlias(true);
Matrix matrixJoin = new Matrix();
// scale as join size
matrixJoin.postScale(size[0], size[0]);
canvas.save();
canvas.drawColor(Color.TRANSPARENT);
for (int index = 0; index < bitmaps.size(); index++) {
Bitmap bitmap = bitmaps.get(index);
//如何该bitmap被回收了,则跳过
if (bitmap == null || bitmap.isRecycled()) {
continue;
}
// MATRIX
Matrix matrix = new Matrix();
// scale as destination
matrix.postScale((float) dimension / bitmap.getWidth(),
(float) dimension / bitmap.getHeight());
canvas.save();
matrix.postConcat(matrixJoin);
float[] offset = GroupAvatarJoinLayout.offset(count, index, dimension, size);
canvas.translate(offset[0], offset[1]);
// 缩放
Bitmap newBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
bitmap.getHeight(), matrix, true);
// 裁剪
Bitmap bitmapOk = GroupAvatarBitmaps.createMaskBitmap(newBitmap, newBitmap.getWidth(), newBitmap.getHeight(), (int) rotation[index], 0.15f);
canvas.drawBitmap(bitmapOk, 0, 0, paint);
canvas.restore();
}
return bitmap2InputStream(output);
}
private InputStream bitmap2InputStream(Bitmap bm) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bm.compress(Bitmap.CompressFormat.PNG, 100, baos);
resultStream = new ByteArrayInputStream(baos.toByteArray());
return resultStream;
}
@Override
public void cleanup() {
if (resultStream != null) {
try {
resultStream.close();
} catch (IOException e) {
// Ignore
}
}
}
@Override
public String getId() {
StringBuilder stringBuilder = new StringBuilder();
for (String string : avatarList) {
stringBuilder.append(string);
}
return stringBuilder.toString();
}
@Override
public void cancel() {
// Do nothing.
}
}
(5)
class GroupAvatarJoinLayout {
public static final String TAG = GroupAvatarJoinLayout.class.getSimpleName();
public static int max() {
return 5;
}
private static final float[][] rotations = {new float[]{360}, new float[]{45, 360},
new float[]{120, 0, -120}, new float[]{90, 180, -90, 0},
new float[]{144, 72, 0, -72, -144},};
public static float[] rotation(int count) {
return count > 0 && count <= rotations.length ? rotations[count - 1] : null;
}
private static final float[][] sizes = {new float[]{0.9f, 0.9f},
new float[]{0.5f, 0.65f}, new float[]{0.45f, 0.8f},
new float[]{0.45f, 0.91f}, new float[]{0.38f, 0.80f}};
public static float[] size(int count) {
return count > 0 && count <= sizes.length ? sizes[count - 1] : null;
}
public static float[] offset(int count, int index, float dimension, float[] size) {
switch (count) {
case 1:
return offset1(index, dimension, size);
case 2:
return offset2(index, dimension, size);
case 3:
return offset3(index, dimension, size);
case 4:
return offset4(index, dimension, size);
case 5:
return offset5(index, dimension, size);
default:
break;
}
return new float[]{0f, 0f};
}
/**
* 5个头像
*
* @param index 下标
* @param dimension 画布边长(正方形)
* @param size size[0]缩放 size[1]边距
* @return 下标index X,Y轴坐标
*/
private static float[] offset5(int index, float dimension, float[] size) {
// 圆的直径
float cd = (float) dimension * size[0];
// 边距
float s1 = -cd * size[1];
float x1 = 0;
float y1 = s1;
float x2 = (float) (s1 * Math.cos(19 * Math.PI / 180));
float y2 = (float) (s1 * Math.sin(18 * Math.PI / 180));
float x3 = (float) (s1 * Math.cos(54 * Math.PI / 180));
float y3 = (float) (-s1 * Math.sin(54 * Math.PI / 180));
float x4 = (float) (-s1 * Math.cos(54 * Math.PI / 180));
float y4 = (float) (-s1 * Math.sin(54 * Math.PI / 180));
float x5 = (float) (-s1 * Math.cos(19 * Math.PI / 180));
float y5 = (float) (s1 * Math.sin(18 * Math.PI / 180));
// Log.d(TAG, "x1:" + x1 + "/y1:" + y1);
// Log.d(TAG, "x2:" + x2 + "/y2:" + y2);
// Log.d(TAG, "x3:" + x3 + "/y3:" + y3);
// Log.d(TAG, "x4:" + x4 + "/y4:" + y4);
// Log.d(TAG, "x5:" + x5 + "/y5:" + y5);
// 居中 Y轴偏移量
float xx1 = (dimension - cd - y3 - s1) / 2;
// 居中 X轴偏移量
float xxc1 = (dimension - cd) / 2;
// xx1 = xxc1 = -s1;
// xx1 = xxc1 = 0;
switch (index) {
case 0:
// return new float[] { s1 + xxc1, xx1 };
return new float[]{x1 + xxc1, y1 + xx1};
case 1:
return new float[]{x2 + xxc1, y2 + xx1};
case 2:
return new float[]{x3 + xxc1, y3 + xx1};
case 3:
return new float[]{x4 + xxc1, y4 + xx1};
case 4:
return new float[]{x5 + xxc1, y5 + xx1};
default:
break;
}
return new float[]{0f, 0f};
}
/**
* 4个头像
*
* @param index 下标
* @param dimension 画布边长(正方形)
* @param size size[0]缩放 size[1]边距
* @return 下标index X,Y轴坐标
*/
private static float[] offset4(int index, float dimension, float[] size) {
// 圆的直径
float cd = (float) dimension * size[0];
// 边距
float s1 = cd * size[1];
float x1 = 0;
float y1 = 0;
float x2 = s1;
float y2 = y1;
float x3 = s1;
float y3 = s1;
float x4 = x1;
float y4 = y3;
// Log.d(TAG, "x1:" + x1 + "/y1:" + y1);
// Log.d(TAG, "x2:" + x2 + "/y2:" + y2);
// Log.d(TAG, "x3:" + x3 + "/y3:" + y3);
// Log.d(TAG, "x4:" + x4 + "/y4:" + y4);
// 居中 X轴偏移量
float xx1 = (dimension - cd - s1) / 2;
switch (index) {
case 0:
return new float[]{x1 + xx1, y1 + xx1};
case 1:
return new float[]{x2 + xx1, y2 + xx1};
case 2:
return new float[]{x3 + xx1, y3 + xx1};
case 3:
return new float[]{x4 + xx1, y4 + xx1};
default:
break;
}
return new float[]{0f, 0f};
}
/**
* 3个头像
*
* @param index 下标
* @param dimension 画布边长(正方形)
* @param size size[0]缩放 size[1]边距
* @return 下标index X,Y轴坐标
*/
private static float[] offset3(int index, float dimension, float[] size) {
// 圆的直径
float cd = (float) dimension * size[0];
// 边距
float s1 = cd * size[1];
// 第二个圆的 Y坐标
float y2 = s1 * (3 / 2);
// 第二个圆的 X坐标
float x2 = s1 - y2 / 1.73205f;
// 第三个圆的 X坐标
float x3 = s1 * 2 - x2;
// 居中 Y轴偏移量
float xx1 = (dimension - cd - y2) / 2;
// 居中 X轴偏移量
float xxc1 = (dimension - cd) / 2 - s1;
// xx1 = xxc1 = 0;
switch (index) {
case 0:
return new float[]{s1 + xxc1, xx1};
case 1:
return new float[]{x2 + xxc1, y2 + xx1};
case 2:
return new float[]{x3 + xxc1, y2 + xx1};
default:
break;
}
return new float[]{0f, 0f};
}
/**
* 2个头像
*
* @param index 下标
* @param dimension 画布边长(正方形)
* @param size size[0]缩放 size[1]边距
* @return 下标index X,Y轴坐标
*/
private static float[] offset2(int index, float dimension, float[] size) {
// 圆的直径
float cd = (float) dimension * size[0];
// 边距
float s1 = cd * size[1];
float x1 = 0;
float y1 = 0;
float x2 = s1;
float y2 = s1;
// Log.d(TAG, "x1:" + x1 + "/y1:" + y1);
// Log.d(TAG, "x2:" + x2 + "/y2:" + y2);
// 居中 X轴偏移量
float xx1 = (dimension - cd - s1) / 2;
switch (index) {
case 0:
return new float[]{x1 + xx1, y1 + xx1};
case 1:
return new float[]{x2 + xx1, y2 + xx1};
default:
break;
}
return new float[]{0f, 0f};
}
/**
* 1个头像
*
* @param index 下标
* @param dimension 画布边长(正方形)
* @param size size[0]缩放 size[1]边距
* @return 下标index X,Y轴坐标
*/
private static float[] offset1(int index, float dimension, float[] size) {
// 圆的直径
float cd = (float) dimension * size[0];
float offset = (dimension - cd) / 2;
return new float[]{offset, offset};
}
}
(6)
使用:
List<String> iconList = ...
Glide.with(context).load(iconList).into(targetImageView);
注意事项
优化
Glide.with(context).resumeRequests()
Glide.with(context).pauseRequests()
当列表在滑动的时候,调用pauseRequests()取消请求,滑动停止时,调用resumeRequests()恢复请求。
ImageView的setTag问题
如果使用Glide的into(imageView)为ImageView设置图片的同时使用ImageView的setTag(final Object tag)方法,将会导致异常。
java.lang.IllegalArgumentException: You must not call setTag() on a view Glide is targeting
解决:如果你需要为ImageView设置Tag,必须使用setTag(int key, final Object tag)及getTag(int key)方法,其中key必须是合法的资源ID以确保key的唯一性,典型做法就是在资源文件ids.xml中声明type=”id”的item资源。
<item name="image_tag" type="id"/>
Glide.with(context).load(url).into(imageViewHolder.image);
imageViewHolder.image.setTag(R.id.image_tag, i);
imageViewHolder.image.setOnClickListener(new View.OnClickListener() {
@Override
public void OnClick(){
int position = (int) v.getTag(R.id.image_tag);
...
}
});
异步线程加载图片的崩溃问题
java.lang.IllegalArgumentException: You cannot start a load for a destroyed activity
解决:
1. 不要再非主线程里面使用Glide加载图片,如果真的使用了,请把context参数换成getApplicationContext。
2. 正确管理Background Threads(异步线程),当Activity停止或销毁时,停止所有相关的异步线程,停止所有后续的UI操作。