原标题:Android 框架思考--工具类设计(Glide、Picasso切换实现)
作者丨AnonyPer
https://www.jianshu.com/p/dd033ae46832
我们在设计一个项目框架的时候,除了选定基本的骨架如MVC、MVP等之外,还有诸如网络库的选择、图片处理库的选择,选定一个适合我们项目的库之外,针对解耦以及可替换方面的考虑,如何接入进我们的项目中也就需要考虑一番了。本文从一个图片库入手,整理一下我对这方面的思考。
场景设定
项目前期选定 glide 作为图片加载库,然后再项目中期,领导要求(不讨论原因)图片加载库切换成 Picasso库,并且以后也有可能要被换成其他的诸如 image-loader、Fresco 或者 volley 等图片加载库,所以在设计图片加载逻辑时需要兼容这些可能变动的需求。
分析过程
要实现上述目标,减少后续代码改动范围,所以在使用的地方直接使用glide或者Picasso库的加载方法会使后续替换图片库的工作量变得巨大而且也容易出现遗漏和方法的错误,因此就要求在具体使用的地方不能出现具体图片库的代码,所以基本的就需要使用工厂模式来实现该功能。工厂模式在使用时关注功能有哪些,具体功能实现由具体的特定工厂(glide、Picasso等)来实现。
功能分析
基本的图片工具库,需要满足以下方法(特殊的需求如高斯模糊处理等暂不讨论)
展示一个基本的网络库
加载图片时占位图
加载失败的占位图
图片显示尺寸的限定
图片显示样式处理(拉伸、占满、等比缩放等,这个需求每一个图片加载库都可以自动根据imageview的属性自动适应,不需要额外做处理)
图片圆角处理
图片加载过程的状态监听
以上是设计一个图片加载模块要考虑基本的功能,其他的特殊需求可以根据项目要求具体再加,诸如图片缓存策略、图片滤镜等,这些展开说内容就过多了。
根据上面的需求分析,可以抽离出以下几个类:
配置文件类
importandroid.widget.ImageView;
/**
* Application
* Created by anonyper on 2018/4/2.
*/
publicclassImageConfig{
intdefaultRes; //默认占位符
intfailRes; //失败占位符
intradius; // 圆角
ImageView.ScaleType scaleType; //图片展示样式
intwidth = - 1; //图片宽
intheight = - 1; //图片高
/**
* 构造函数
*
* @paramdefaultRes
* @paramfailRes
* @paramradius
* @paramwidth
* @paramheight
* @paramscaleType
*/
publicImageConfig(intdefaultRes, intfailRes, intradius, intwidth, intheight, ImageView.ScaleType scaleType){
this.defaultRes = defaultRes;
this.failRes = failRes;
this.radius = radius;
this.width = width;
this.height = height;
this.scaleType = scaleType;
}
publicImageConfig(intdefaultRes, intfailRes, intradius, intwidth, intheight){
this(defaultRes, failRes, radius, width, height, ImageView.ScaleType.FIT_CENTER);
}
publicImageConfig(intdefaultRes, intfailRes, intwidth, intheight){
this(defaultRes, failRes, 0, width, height);
}
publicImageConfig(intdefaultRes, intfailRes, intradius){
this(defaultRes, failRes, radius, - 1, - 1, ImageView.ScaleType.FIT_CENTER);
}
publicImageConfig(intdefaultRes, intfailRes){
this(defaultRes, failRes, 0);
}
publicImageConfig(intdefaultRes){
this(defaultRes, - 1);
}
publicintgetDefaultRes(){
returndefaultRes;
}
publicvoidsetDefaultRes(intdefaultRes){
this.defaultRes = defaultRes;
}
publicintgetFailRes(){
returnfailRes;
}
publicvoidsetFailRes(intfailRes){
this.failRes = failRes;
}
publicintgetRadius(){
returnradius;
}
publicvoidsetRadius(intradius){
this.radius = radius;
}
publicImageView. ScaleType getScaleType(){
returnscaleType;
}
publicvoidsetScaleType(ImageView.ScaleType scaleType){
this.scaleType = scaleType;
}
publicintgetWidth(){
returnwidth;
}
publicvoidsetWidth(intwidth){
this.width = width;
}
publicintgetHeight(){
returnheight;
}
publicvoidsetHeight(intheight){
this.height = height;
}
}
加载过程监听类
/**
* 图片加载过程的回调
* 回调监听这些方法不一定所有的库都有
* Application
* Created by anonyper on 2018/4/2.
*/
publicinterfaceImageLoadProcessInterface{
/**
* 开始加载
*/
voidStarted();
/**
* 资源准备妥当
*/
voidonResourceReady();
/**
* 资源已经释放
*/
voidCleared();
/**
* 资源加载失败
*/
voidFailed();
}
加载接口类
/**
* 图片加载的接口
* Application
* Created by anonyper on 2018/4/2.
*/
publicinterfaceImageLoadInterface{
/**
* 显示路径中的图片(网络、文件中)
*
* @parammContext
* @paramview
* @paramurl
* @paramconfig 配置参数
* @paramimageLoadProcessInterface 加载过程监听
*/
voiddisplay(Context mContext, finalImageView view, String url, ImageConfig config, ImageLoadProcessInterface imageLoadProcessInterface);
/**
* 开始加载
*
* @paramcontext
*/
voidresumeLoad(Context context, String url);
/**
* 暂停加载
*
* @paramcontext
*/
voidpauseLoad(Context context, String url);
/**
* 清除一个资源的加载
*
* @paramcontext
*/
voidclearImageView(Context context, ImageView imageView, String url);
}
以上三个类,和glide、Picasso类都不相关。
图片工具类
在这个类中封装调用glide或者Picasso,外部调用时不用关心内部实现。
/**
* 图片加载基础的工具类
* Application
* Created by anonyper on 2018/4/2.
*/
publicclassImageLoadBaseTool{
privatestaticfinalString TAG = "ImageTool";
privatestaticImageLoadInterface imageLoad = null;
static{
imageLoad = newImageLoadByGlide(); //glide
// imageLoad = new ImageLoadByPicasso();//picasso
}
/**
* imageView中加载项目内资源
*
* @parammContext
* @paramview
* @paramresId
*/
publicstaticvoiddisplay(Context mContext, finalImageView view, @DrawableRes intresId){
display(mContext, view, null, resId);
}
/**
* 加载网络图片/本地图片
*
* @parammContext
* @paramview
* @paramurl
*/
publicstaticvoiddisplay(Context mContext, ImageView view, String url){
display(mContext, view, url,- 1);
}
/**
* 加载图片
*
* @parammContext 上下文
* @paramview imageview
* @paramurl 图片地址
* @paramdefaultImage 默认显示内容
*/
publicstaticvoiddisplay(Context mContext, ImageView view, String url, intdefaultImage){
display(mContext, view, url, defaultImage, null);
}
/**
* @parammContext
* @paramview
* @paramurl
* @paramimageLoadProcessInterface
*/
publicstaticvoiddisplay(Context mContext, ImageView view, String url, ImageLoadProcessInterface imageLoadProcessInterface){
display(mContext, view, url, - 1, imageLoadProcessInterface);
}
/**
* @parammContext 上下文
* @paramview imageview
* @paramurl 地址
* @paramdefaultImage 默认图片
* @paramimageLoadProcessInterface 监听
*/
publicstaticvoiddisplay(Context mContext, ImageView view, String url, intdefaultImage, ImageLoadProcessInterface imageLoadProcessInterface){
display(mContext, view, url, defaultImage, - 1, imageLoadProcessInterface);
}
publicstaticvoiddisplay(Context mContext, ImageView view, String url, intdefaultImage, intfailImage, ImageLoadProcessInterface imageLoadProcessInterface){
display(mContext, view, url, newImageConfig(defaultImage, failImage, 0), imageLoadProcessInterface);
}
publicstaticvoiddisplay(Context mContext, ImageView view, String url, ImageConfig config, ImageLoadProcessInterface imageLoadProcessInterface){
displayUrl(mContext, view, url, config, imageLoadProcessInterface);
}
/**
* glide加载图片
*
* @paramimageView view
* @paramurl url
*/
privatestaticvoiddisplayUrl(Context mContext, finalImageView imageView, finalString url, finalImageConfig config, finalImageLoadProcessInterface imageLoadProcessInterface){
try{
imageLoad.display(mContext, imageView, url, config, imageLoadProcessInterface);
} catch(Exception e) {
e.printStackTrace();
}
}
/**
* 恢复加载图片
*
* @paramcontext
*/
publicstaticvoidresumeLoad(Context context, String url){
if(imageLoad != null) {
imageLoad.resumeLoad(context, url);
}
}
/**
* 清除一个资源的加载
*
* @paramcontext
*/
publicstaticvoidclearImageView(Context context, ImageView imageView, String url){
if(imageLoad != null) {
imageLoad.clearImageView(context, imageView, url);
}
}
/**
* 暂停加载图片
*
* @paramcontext
*/
publicstaticvoidpauseLoad(Context context, String url){
if(imageLoad != null) {
imageLoad.pauseLoad(context, url);
}
}
}
上面代码中的 ImageLoadByGlide 和 ImageLoadByPicasso 是分别用 glide 和 Picasso 实现的 ImageLoadInterface 接口,切换图片库的时候,只需要实现ImageLoadInterface 方法,然后在 ImageLoadBaseTool 类中切换具体的ImageLoadInterface 实力即可。下面先分析 glide 使用。
glide、Picasso 图片库切换实现
为了测试glide、Picasso的切换功能,所以在项目中同时引入了glide、Picasso,如下:
build.gradle引入
dependencies {
implementation fileTree(dir: 'libs', include: [ '*.jar'])
implementation "com.android.support:appcompat-v7:$rootProject.ext.supportLibraryVersion"
implementation( "com.squareup.picasso:picasso:$rootProject.ext.picasso_version") { //这样写的作用是从 picasso 的依赖中去除 "com.android.support"
exclude group: "com.android.support"
}
implementation( "com.github.bumptech.glide:glide:$rootProject.ext.glide_version") {
exclude group: "com.android.support"
}
//之所以去除依赖,是因为picasso 和 glide 对于com.android.support依赖的版本不一致,不加这句同时引入picasso 和 glide会报错
//这里面的implementation和低版本的gradle中的compile用法一样
}
ImageLoadByGlide类实现
/**
* 图片显示的公共类 使用glide
* Application
* Created by anonyper on 2018/4/2.
*/
publicclassImageLoadByGlideimplementsImageLoadInterface{
privatestaticfinalString TAG = "GlideUtils";
/**
* glide加载图片
*
* @paramimageView view
* @paramurl url
*/
publicvoiddisplay(Context mContext, finalImageView imageView, finalString url, finalImageConfig config, finalImageLoadProcessInterface imageLoadProcessInterface){
if(mContext == null) {
LogUtil.e( "GlideUtils", "GlideUtils -> display -> mContext is null");
return;
}
// 不能崩
if(imageView == null) {
LogUtil.e( "GlideUtils", "GlideUtils -> display -> imageView is null");
return;
}
Context context = imageView.getContext();
// View你还活着吗?
if(context instanceofActivity) {
if(((Activity) context).isFinishing()) { //activity是否结束
return;
}
}
try{
if((config == null|| config.defaultRes <= 0) && TextUtils.isEmpty(url)) {
LogUtil.e( "GlideUtils", "GlideUtils -> display -> url is null and config is null");
return;
}
RequestOptions requestOptions = newRequestOptions();
if(config != null) {
if(config.defaultRes > 0) {
requestOptions.placeholder(config.defaultRes);
}
if(config.failRes > 0) {
requestOptions.error(config.failRes);
}
if(config.scaleType != null) {
switch(config.scaleType) {
caseCENTER_CROP:
requestOptions.centerCrop();
break;
caseFIT_CENTER:
requestOptions.fitCenter();
break;
default:
requestOptions.fitCenter();
break;
}
} else{
requestOptions.fitCenter();
}
if(config.radius > 0) {
requestOptions.transform( newRoundedCorners(config.radius));
}
}
ImageViewTarget simpleTarget = newBitmapImageViewTarget(imageView) {
@Override
publicvoidStarted(Drawable placeholder){
super.Started(placeholder);
LogUtil.i( "image", "Started");
if(imageLoadProcessInterface != null) {
imageLoadProcessInterface.Started();
}
}
@Override
publicvoidonResourceReady(@NonNull Bitmap resource, @Nullable Transition superBitmap> transition){
super.onResourceReady(resource, transition);
LogUtil.i( "image", "onResourceReady");
if(imageLoadProcessInterface != null) {
imageLoadProcessInterface.onResourceReady();
}
}
@Override
publicvoidFailed(@Nullable Drawable errorDrawable){
super.Failed(errorDrawable);
LogUtil.i( "image", "Failed");
if(imageLoadProcessInterface != null) {
imageLoadProcessInterface.Failed();
}
}
@Override
publicvoidCleared(Drawable placeholder){
super.Cleared(placeholder);
LogUtil.i( "image", "Cleared");
if(imageLoadProcessInterface != null) {
imageLoadProcessInterface.Cleared();
}
}
@Override
publicvoidgetSize(@NonNull SizeReadyCallback cb){
if(config != null&& config.width >= 0&& config.height >= 0)
cb.onSizeReady(config.width, config.height);
else{
super.getSize(cb);
}
}
};
if(simpleTarget != null) {
Glide.with(context).asBitmap().load(url).apply(requestOptions).into(simpleTarget);
} else{
Glide.with(context).asBitmap().load(url).apply(requestOptions).into(imageView);
}
} catch(Exception e) {
e.printStackTrace();
}
}
/**
* 恢复加载图片
*
* @paramcontext
*/
publicvoidresumeLoad(Context context, String url){
if(context != null)
Glide.with(context).resumeRequests();
}
/**
* 清除一个资源的加载
*
* @paramcontext
*/
publicvoidclearImageView(Context context, ImageView imageView, String url){
if(context != null&& imageView != null)
Glide.with(context).clear(imageView);
}
/**
* 暂停加载图片
*
* @paramcontext
*/
publicvoidpauseLoad(Context context, String url){
if(context != null)
Glide.with(context).pauseRequests();
}
}
ImageLoadByPicasso类实现
/**
* 图片显示的公共类 使用picasso
* Application
* Created by anonyper on 2018/4/2.
*/
publicclassImageLoadByPicassoimplementsImageLoadInterface{
privatestaticfinalString TAG = "PicassoUtils";
/**
* glide加载图片
*
* @paramimageView view
* @paramurl url
*/
publicvoiddisplay(Context mContext, finalImageView imageView, String url, finalImageConfig config, finalImageLoadProcessInterface imageLoadProcessInterface){
if(mContext == null) {
LogUtil.e( "PicassoUtils", "PicassoUtils -> display -> mContext is null");
return;
}
// 不能崩
if(imageView == null) {
LogUtil.e( "PicassoUtils", "PicassoUtils -> display -> imageView is null");
return;
}
Context context = imageView.getContext();
// View你还活着吗?
if(context instanceofActivity) {
if(((Activity) context).isFinishing()) { //activity是否结束
return;
}
}
try{
if((config == null|| config.defaultRes <= 0) && TextUtils.isEmpty(url)) {
LogUtil.e( "PicassoUtils", "PicassoUtils -> display -> url is null and config is null");
return;
}
RequestCreator requestCreator = null;
Uri loadUri = null;
if(url.startsWith( "http")) {
//网络图片
loadUri = Uri.parse(url);
} else{
//本地文件
if(url.startsWith( "file://")) {
//文件的方式
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
//Android 7.0系统开始 使用本地真实的Uri路径不安全,使用FileProvider封装共享Uri
url = Uri.parse(url).getPath();
}
}
File file = newFile(url);
if(file != null&& file.exists()) {
//本地文件
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
//Android 7.0系统开始 使用本地真实的Uri路径不安全,使用FileProvider封装共享Uri
loadUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
} else{
loadUri = Uri.fromFile(file);
}
} else{
//可能是资源路径的地址
loadUri = Uri.parse(url);
}
}
requestCreator = Picasso.get().load(loadUri);
if(config != null) {
if(config.defaultRes > 0) {
requestCreator.placeholder(config.defaultRes);
}
if(config.failRes > 0) {
requestCreator.error(config.failRes);
}
if(config.width > 0&& config.height > 0) {
requestCreator.resize(config.width, config.height);
}
if(config.radius > 0) {
requestCreator.transform( newTransformation() {
@Override
publicBitmap transform(Bitmap source){
finalPaint paint = newPaint();
paint.setAntiAlias( true);
Bitmap target = Bitmap.createBitmap(source.getWidth(), source.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = newCanvas(target);
RectF rect = newRectF( 0, 0, source.getWidth(), source.getHeight());
canvas.drawRoundRect(rect, config.radius, config.radius, paint);
paint.setXfermode( newPorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(source, 0, 0, paint);
source.recycle();
returntarget;
}
@Override
publicString key(){
return"radius-transform";
}
});
}
}
if(imageLoadProcessInterface != null) {
requestCreator.tag(url).into(imageView, newCallback() {
@Override
publicvoidonSuccess(){
if(imageLoadProcessInterface != null) {
imageLoadProcessInterface.onResourceReady();
}
}
@Override
publicvoid(Exception e){
if(imageLoadProcessInterface != null) {
imageLoadProcessInterface.Failed();
}
}
});
} else{
requestCreator.tag(url).into(imageView);
}
} catch(Exception e) {
e.printStackTrace();
}
}
/**
* 恢复加载图片
*
* @paramcontext
*/
publicvoidresumeLoad(Context context, String url){
if(!TextUtils.isEmpty(url))
Picasso.get().resumeTag(url);
}
/**
* 清除一个资源的加载
*
* @paramcontext
*/
publicvoidclearImageView(Context context, ImageView imageView, String url){
if(!TextUtils.isEmpty(url))
Picasso.get().invalidate(url);
}
/**
* 暂停加载图片
*
* @paramcontext
*/
publicvoidpauseLoad(Context context, String url){
if(!TextUtils.isEmpty(url))
Picasso.get().pauseTag(url);
}
}
注意事项:7.0以上版本使用之前的uri会认为是不安全的,但是当前版本的Picasso(2.71828)没有处理这个问题,所以不做转化直接传入 file 地址时是不会显示图片的,具体处理代码见上面。
切换不同的图片库
在上面ImageLoadBaseTool中的
privatestaticImageLoadInterface imageLoad = null;
static{
imageLoad = newImageLoadByGlide(); //glide
// imageLoad = new ImageLoadByPicasso();//picasso
}
实现了不同图片库的切换
具体调用
/**
* 展示图片
*
* @parampath 这个地方使用的是本地图片路径
*/
voidshowImage(ImageView imageView, String path){
// path = "https://upload-images.jianshu.io/upload_images/5207488-9b7d8d755f83092b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1000/format/webp";
// path = "file://"+new File(path).getPath();
ImageLoadBaseTool.display( this, imageView, path, newImageConfig(R.mipmap.ic_launcher, R.mipmap.ic_launcher_round, 25), newImageLoadProcessInterface() {
@Override
publicvoidStarted(){
}
@Override
publicvoidonResourceReady(){
}
@Override
publicvoidCleared(){
}
@Override
publicvoidFailed(){
}
});
}
以上就是图片模块设计的思路,可以实现不同的图片加载库之间来回切换但不用大范围的更改之前的代码。
具体的图片库使用技巧,网上资料很多,可自行查阅。
源代码下载:
图片库选择—glide、Picasso切换
https://download.csdn.net/download/she_cool_/10853237
长
按
关
责任编辑: