Android框架思考--工具类设计(Glide、Picasso切换实现)

464 阅读9分钟
原文链接: www.jianshu.com

Android框架思考--工具类(Glide、Picasso切换)

我们在设计一个项目框架的时候,除了选定基本的骨架如MVC、MVP等之外,还有诸如网络库的选择、图片处理库的选择,选定一个适合我们项目的库之外,针对解耦以及可替换方面的考虑,如何接入进我们的项目中也就需要考虑一番了。本文从一个图片库入手,整理一下我对这方面的思考。

场景设定

项目前期选定glide作为图片加载库,然后再项目中期,领导要求(不讨论原因)图片加载库切换成Picasso库,并且以后也有可能要被换成其他的诸如image-loader、Fresco或者volley等图片加载库,所以在设计图片加载逻辑时需要兼容这些可能变动的需求。

分析过程

要实现上述目标,减少后续代码改动范围,所以在使用的地方直接使用glide或者Picasso库的加载方法会使后续替换图片库的工作量变得巨大而且也容易出现遗漏和方法的错误,因此就要求在具体使用的地方不能出现具体图片库的代码,所以基本的就需要使用工厂模式来实现该功能。工厂模式在使用时关注功能有哪些,具体功能实现由具体的特定工厂(glide、Picasso等)来实现。

功能分析

基本的图片工具库,需要满足以下方法(特殊的需求如高斯模糊处理等暂不讨论)

  • 展示一个基本的网络库

  • 加载图片时占位图

  • 加载失败的占位图

  • 图片显示尺寸的限定

  • 图片显示样式处理(拉伸、占满、等比缩放等,这个需求每一个图片加载库都可以自动根据imageview的属性自动适应,不需要额外做处理)

  • 图片圆角处理

  • 图片加载过程的状态监听

以上是设计一个图片加载模块要考虑基本的功能,其他的特殊需求可以根据项目要求具体再加,诸如图片缓存策略、图片滤镜等,这些展开说内容就过多了。

根据上面的需求分析,可以抽离出以下几个类:

  • 配置文件类

import android.widget.ImageView;
​
/**
 * Application
 * Created by anonyper on 2018/4/2.
 */
​
public class ImageConfig {
 int defaultRes;//默认占位符
 int failRes;//失败占位符
 int radius;// 圆角
 ImageView.ScaleType scaleType;//图片展示样式
 int width = -1;//图片宽
 int height = -1;//图片高
​
 /**
 * 构造函数
 *
 * @param defaultRes
 * @param failRes
 * @param radius
 * @param width
 * @param height
 * @param scaleType
 */
 public ImageConfig(int defaultRes, int failRes, int radius, int width, int height, ImageView.ScaleType scaleType) {
 this.defaultRes = defaultRes;
 this.failRes = failRes;
 this.radius = radius;
 this.width = width;
 this.height = height;
 this.scaleType = scaleType;
 }
​
 public ImageConfig(int defaultRes, int failRes, int radius, int width, int height) {
 this(defaultRes, failRes, radius, width, height, ImageView.ScaleType.FIT_CENTER);
 }
​
 public ImageConfig(int defaultRes, int failRes, int width, int height) {
 this(defaultRes, failRes, 0, width, height);
 }
​
 public ImageConfig(int defaultRes, int failRes, int radius) {
 this(defaultRes, failRes, radius, -1, -1, ImageView.ScaleType.FIT_CENTER);
 }
​
 public ImageConfig(int defaultRes, int failRes) {
 this(defaultRes, failRes, 0);
 }
​
 public ImageConfig(int defaultRes) {
 this(defaultRes, -1);
 }
​
 public int getDefaultRes() {
 return defaultRes;
 }
​
 public void setDefaultRes(int defaultRes) {
 this.defaultRes = defaultRes;
 }
​
 public int getFailRes() {
 return failRes;
 }
​
 public void setFailRes(int failRes) {
 this.failRes = failRes;
 }
​
 public int getRadius() {
 return radius;
 }
​
 public void setRadius(int radius) {
 this.radius = radius;
 }
​
 public ImageView.ScaleType getScaleType() {
 return scaleType;
 }
​
 public void setScaleType(ImageView.ScaleType scaleType) {
 this.scaleType = scaleType;
 }
​
 public int getWidth() {
 return width;
 }
​
 public void setWidth(int width) {
 this.width = width;
 }
​
 public int getHeight() {
 return height;
 }
​
 public void setHeight(int height) {
 this.height = height;
 }
}
  • 加载过程监听类

/**
 * 图片加载过程的回调
 * 回调监听这些方法不一定所有的库都有
 * Application
 * Created by anonyper on 2018/4/2.
 */
​
public interface ImageLoadProcessInterface {
​
 /**
 * 开始加载
 */
 void onLoadStarted();
​
 /**
 * 资源准备妥当
 */
 void onResourceReady();
​
 /**
 * 资源已经释放
 */
 void onLoadCleared();
​
 /**
 * 资源加载失败
 */
 void onLoadFailed();
​
}
  • 加载接口类

com.kotlin.anonyper.testapplication.image;
​
import android.content.Context;
import android.widget.ImageView;
​
​
/**
 * 图片加载的接口
 * Application
 * Created by anonyper on 2018/4/2.
 */
​
public interface ImageLoadInterface {
​
 /**
 * 显示路径中的图片(网络、文件中)
 *
 * @param mContext
 * @param view
 * @param url
 * @param config                    配置参数
 * @param imageLoadProcessInterface 加载过程监听
 */
 void display(Context mContext, final ImageView view, String url, ImageConfig config, ImageLoadProcessInterface imageLoadProcessInterface);
​
 /**
 * 开始加载
 *
 * @param context
 */
 void resumeLoad(Context context, String url);
​
 /**
 * 暂停加载
 *
 * @param context
 */
 void pauseLoad(Context context, String url);
​
 /**
 * 清除一个资源的加载
 *
 * @param context
 */
 void clearImageView(Context context, ImageView imageView, String url);
​
}

以上三个类,和glide、Picasso类都不相关。

  • 图片工具类

在这个类中封装调用glide或者Picasso,外部调用时不用关心内部实现。

package com.kotlin.anonyper.testapplication.image;
​
import android.app.Activity;
import android.content.Context;
import android.support.annotation.DrawableRes;
import android.text.TextUtils;
import android.widget.ImageView;
​
import com.kotlin.anonyper.testapplication.LogUtil;
​
/**
 * 图片加载基础的工具类
 * Application
 * Created by anonyper on 2018/4/2.
 */
​
public class ImageLoadBaseTool {
​
 private static final String TAG = "ImageTool";
 private static ImageLoadInterface imageLoad = null;
​
 static {
 imageLoad = new ImageLoadByGlide();//glide
//        imageLoad = new ImageLoadByPicasso();//picasso
 }
​
 /**
 * imageView中加载项目内资源
 *
 * @param mContext
 * @param view
 * @param resId
 */
 public static void display(Context mContext, final ImageView view, @DrawableRes int resId) {
 display(mContext, view, null, resId);
​
 }
​
​
 /**
 * 加载网络图片/本地图片
 *
 * @param mContext
 * @param view
 * @param url
 */
 public static void display(Context mContext, ImageView view, String url) {
​
 display(mContext, view, url,-1);
 }
​
 /**
 * 加载图片
 *
 * @param mContext     上下文
 * @param view         imageview
 * @param url          图片地址
 * @param defaultImage 默认显示内容
 */
 public static void display(Context mContext, ImageView view, String url, int defaultImage) {
 display(mContext, view, url, defaultImage, null);
 }
​
​
 /**
 * @param mContext
 * @param view
 * @param url
 * @param imageLoadProcessInterface
 */
 public static void display(Context mContext, ImageView view, String url, ImageLoadProcessInterface imageLoadProcessInterface) {
 display(mContext, view, url, -1, imageLoadProcessInterface);
 }
​
 /**
 * @param mContext                  上下文
 * @param view                      imageview
 * @param url                       地址
 * @param defaultImage              默认图片
 * @param imageLoadProcessInterface 监听
 */
 public static void display(Context mContext, ImageView view, String url, int defaultImage, ImageLoadProcessInterface imageLoadProcessInterface) {
 display(mContext, view, url, defaultImage, -1, imageLoadProcessInterface);
 }
​
 public static void display(Context mContext, ImageView view, String url, int defaultImage, int failImage, ImageLoadProcessInterface imageLoadProcessInterface) {
 display(mContext, view, url, new ImageConfig(defaultImage, failImage, 0), imageLoadProcessInterface);
 }
​
 public static void display(Context mContext, ImageView view, String url, ImageConfig config, ImageLoadProcessInterface imageLoadProcessInterface) {
 displayUrl(mContext, view, url, config, imageLoadProcessInterface);
 }
​
​
 /**
 * glide加载图片
 *
 * @param imageView view
 * @param url       url
 */
 private static void displayUrl(Context mContext, final ImageView imageView, final String url, final ImageConfig config, final ImageLoadProcessInterface imageLoadProcessInterface) {
​
 try {
 imageLoad.display(mContext, imageView, url, config, imageLoadProcessInterface);
​
 } catch (Exception e) {
 e.printStackTrace();
 }
 }
​
​
 /**
 * 恢复加载图片
 *
 * @param context
 */
 public static void resumeLoad(Context context, String url) {
 if (imageLoad != null) {
 imageLoad.resumeLoad(context, url);
 }
 }
​
 /**
 * 清除一个资源的加载
 *
 * @param context
 */
 public static void clearImageView(Context context, ImageView imageView, String url) {
 if (imageLoad != null) {
 imageLoad.clearImageView(context, imageView, url);
 }
 }
​
 /**
 * 暂停加载图片
 *
 * @param context
 */
 public static void pauseLoad(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类实现

package com.kotlin.anonyper.testapplication.image;
​
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.widget.ImageView;
​
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.BitmapImageViewTarget;
import com.bumptech.glide.request.target.ImageViewTarget;
import com.bumptech.glide.request.target.SizeReadyCallback;
import com.bumptech.glide.request.transition.Transition;
import com.kotlin.anonyper.testapplication.LogUtil;
​
​
/**
 * 图片显示的公共类 使用glide
 * Application
 * Created by anonyper on 2018/4/2.
 */
​
public class ImageLoadByGlide implements ImageLoadInterface {
​
 private static final String TAG = "GlideUtils";
​
 /**
 * glide加载图片
 *
 * @param imageView view
 * @param url       url
 */
 public void display(Context mContext, final ImageView imageView, final String url, final ImageConfig config, final ImageLoadProcessInterface 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 instanceof Activity) {
 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 = new RequestOptions();
 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) {
 case CENTER_CROP:
 requestOptions.centerCrop();
 break;
 case FIT_CENTER:
 requestOptions.fitCenter();
 break;
 default:
 requestOptions.fitCenter();
 break;
 }
 } else {
 requestOptions.fitCenter();
 }
 if (config.radius > 0) {
 requestOptions.transform(new RoundedCorners(config.radius));
 }
 }
 ImageViewTarget simpleTarget = new BitmapImageViewTarget(imageView) {
 @Override
 public void onLoadStarted(Drawable placeholder) {
 super.onLoadStarted(placeholder);
 LogUtil.i("image", "onLoadStarted");
 if (imageLoadProcessInterface != null) {
 imageLoadProcessInterface.onLoadStarted();
 }
 }
​
 @Override
 public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
 super.onResourceReady(resource, transition);
 LogUtil.i("image", "onResourceReady");
 if (imageLoadProcessInterface != null) {
 imageLoadProcessInterface.onResourceReady();
 }
 }
​
 @Override
 public void onLoadFailed(@Nullable Drawable errorDrawable) {
 super.onLoadFailed(errorDrawable);
 LogUtil.i("image", "onLoadFailed");
 if (imageLoadProcessInterface != null) {
 imageLoadProcessInterface.onLoadFailed();
 }
 }
​
 @Override
 public void onLoadCleared(Drawable placeholder) {
 super.onLoadCleared(placeholder);
 LogUtil.i("image", "onLoadCleared");
 if (imageLoadProcessInterface != null) {
 imageLoadProcessInterface.onLoadCleared();
 }
 }
​
 @Override
 public void getSize(@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();
 }
 }
​
​
 /**
 * 恢复加载图片
 *
 * @param context
 */
 public void resumeLoad(Context context, String url) {
 if (context != null)
 Glide.with(context).resumeRequests();
 }
​
 /**
 * 清除一个资源的加载
 *
 * @param context
 */
 public void clearImageView(Context context, ImageView imageView, String url) {
 if (context != null && imageView != null)
 Glide.with(context).clear(imageView);
 }
​
 /**
 * 暂停加载图片
 *
 * @param context
 */
 public void pauseLoad(Context context, String url) {
 if (context != null)
 Glide.with(context).pauseRequests();
 }
​
​
}

ImageLoadByPicasso类实现

package com.kotlin.anonyper.testapplication.image;
​
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.net.Uri;
import android.os.Build;
import android.support.v4.content.FileProvider;
import android.text.TextUtils;
import android.widget.ImageView;
​
import com.kotlin.anonyper.testapplication.LogUtil;
import com.squareup.picasso.Callback;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.RequestCreator;
import com.squareup.picasso.Transformation;
​
import java.io.File;
​
/**
 * 图片显示的公共类 使用picasso
 * Application
 * Created by anonyper on 2018/4/2.
 */
​
public class ImageLoadByPicasso implements ImageLoadInterface {
​
 private static final String TAG = "PicassoUtils";
​
​
 /**
 * glide加载图片
 *
 * @param imageView view
 * @param url       url
 */
 public void display(Context mContext, final ImageView imageView, String url, final ImageConfig config, final ImageLoadProcessInterface 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 instanceof Activity) {
 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 = new File(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(new Transformation() {
 @Override
 public Bitmap transform(Bitmap source) {
 final Paint paint = new Paint();
 paint.setAntiAlias(true);
 Bitmap target = Bitmap.createBitmap(source.getWidth(), source.getHeight(), Bitmap.Config.ARGB_8888);
 Canvas canvas = new Canvas(target);
 RectF rect = new RectF(0, 0, source.getWidth(), source.getHeight());
 canvas.drawRoundRect(rect, config.radius, config.radius, paint);
 paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
 canvas.drawBitmap(source, 0, 0, paint);
 source.recycle();
 return target;
 }
​
 @Override
 public String key() {
 return "radius-transform";
 }
 });
 }
 }
​
 if (imageLoadProcessInterface != null) {
 requestCreator.tag(url).into(imageView, new Callback() {
 @Override
 public void onSuccess() {
 if (imageLoadProcessInterface != null) {
 imageLoadProcessInterface.onResourceReady();
 }
 }
​
 @Override
 public void onError(Exception e) {
 if (imageLoadProcessInterface != null) {
 imageLoadProcessInterface.onLoadFailed();
 }
 }
 });
 } else {
 requestCreator.tag(url).into(imageView);
 }
​
 } catch (Exception e) {
 e.printStackTrace();
 }
 }
​
 /**
 * 恢复加载图片
 *
 * @param context
 */
 public void resumeLoad(Context context, String url) {
 if (!TextUtils.isEmpty(url))
 Picasso.get().resumeTag(url);
 }
​
 /**
 * 清除一个资源的加载
 *
 * @param context
 */
 public void clearImageView(Context context, ImageView imageView, String url) {
 if (!TextUtils.isEmpty(url))
 Picasso.get().invalidate(url);
 }
​
 /**
 * 暂停加载图片
 *
 * @param context
 */
 public void pauseLoad(Context context, String url) {
 if (!TextUtils.isEmpty(url))
 Picasso.get().pauseTag(url);
 }
​
​
}

注意事项:7.0以上版本使用之前的uri会认为是不安全的,但是当前版本的Picasso(2.71828)没有处理这个问题,所以不做转化直接传入file地址时是不会显示图片的,具体处理代码见上面。

切换不同的图片库

在上面ImageLoadBaseTool中的

private static ImageLoadInterface imageLoad = null;
​
 static {
 imageLoad = new ImageLoadByGlide();//glide
//        imageLoad = new ImageLoadByPicasso();//picasso
 }

实现了不同图片库的切换

具体调用

/**
 * 展示图片
 *
 * @param path 这个地方使用的是本地图片路径
 */
 void showImage(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, new ImageConfig(R.mipmap.ic_launcher, R.mipmap.ic_launcher_round, 25), new ImageLoadProcessInterface() {
 @Override
 public void onLoadStarted() {

 }
​
 @Override
 public void onResourceReady() {
​
 }
​
 @Override
 public void onLoadCleared() {
​
 }
​
 @Override
 public void onLoadFailed() {
​
 }
 });
 }

以上就是图片模块设计的思路,可以实现不同的图片加载库之间来回切换但不用大范围的更改之前的代码。

具体的图片库使用技巧,网上资料很多,可自行查阅。

源代码下载:图片库选择—glide、Picasso切换