優雅地實現Android主流圖片加載框架封裝,可無侵入切換框架
那麽是否存在一種方式 能夠一勞永逸地解決這個痛點呢?下面我們來分析一下圖片加載框架面對的現狀和解決思路。
問題現狀
一個優秀的框架一般在代碼設計的時候已經封裝很不錯了,對於開發者而言框架的使用也是很方便,但是為什麽說我們往往還要去做這方面的框架封裝呢?原因很簡單,實際項目開發中,我們不得不面對著日新月異的需求變化,想要在這個變化中最大程度的實現代碼的可擴展性和變通性(當然還可以偷懶),不能因為牽一發而動全身,同時要將框架適配到實際項目,框架的再封裝設計顯得尤為重要。
不多廢話,我們可以開始今天的圖片封裝之路了。
設計思路
圖片框架的封裝主要需要滿足以下三點:
低耦合,方便將來的代碼擴展。至少要支持目前市場上使用率最高的圖片框架Fresco、Glide、Picasso三者之間的切換
滿足項目中各種需求
調用方便
談到圖片封裝,最先想到的是把一些常用的功能點作為參數傳入到方法內,然後調用圖片加載框架實現我們圖片的加載工作。比如說像下面這樣
public interface ImageLoader { void loadImage(ImageView view, String path, int placeholderId, int errorId,boolean skipMemory); void loadImage(ImageView view, File file, int placeholderId, int errorId, boolean skipMemory);
}
然後分別寫對應的ImageLoader實現類FrescoImageLoader、GlideImageLoader、PicassoImageLoader,最後采用策略的設計模式實現代碼的切換。那麽這種方式實際效果如何呢?實際開發中很明顯的一個 問題就是,對於每一個需要的參數都需要進行對應的封裝,就不止上面所提到的兩個方法,我們需要封裝大量的方法去滿足實際的項目需要,而且每個框架的很多屬性不一致,如果切換圖片框架的話,還是需要大量的切換成本的。
於是我們想到了下面的這種思路
public interface ILoaderStrategy {
void loadImage(LoaderOptions options); /** * 清理內存緩存 */ void clearMemoryCache(); /** * 清理磁盤緩存 */ void clearDiskCache();
}
提取各個框架通用的View,path/file文件路徑,通過LoaderOptions解決大量不同參數傳入的問題。這裏需要說明的是,LoaderOptions中采用控件View,而不是ImageView,主要考慮到Fresco圖片框架采用了DraweeView,這裏保留了設計的擴展性。而圖片參數類LoaderOptions采用了Builder設計模式:
public class LoaderOptions {
public int placeholderResId;
public int errorResId;
public boolean isCenterCrop;
public boolean isCenterInside;
public boolean skipLocalCache; //是否緩存到本地
public boolean skipNetCache;
public Bitmap.Config config = Bitmap.Config.RGB_565;
public int targetWidth;
public int targetHeight;
public float bitmapAngle; //圓角角度
public float degrees; //旋轉角度.註意:picasso針對三星等本地圖片,默認旋轉回0度,即正常位置。此時不需要自己rotate
public Drawable placeholder;
public View targetView;//targetView展示圖片
public BitmapCallBack callBack;
public String url;
public File file;
public int drawableResId;
public Uri uri;
public LoaderOptions(String url) {
this.url = url;
}
public LoaderOptions(File file) {
this.file = file;
}
public LoaderOptions(int drawableResId) {
this.drawableResId = drawableResId;
}
public LoaderOptions(Uri uri) {
this.uri = uri;
}
public void into(View targetView) {
this.targetView = targetView;
ImageLoader.getInstance().loadOptions(this);
}
public void bitmap(BitmapCallBack callBack) {
this.callBack = callBack;
ImageLoader.getInstance().loadOptions(this);
}
public LoaderOptions placeholder(@DrawableRes int placeholderResId) {
this.placeholderResId = placeholderResId;
return this;
}
public LoaderOptions placeholder(Drawable placeholder) {
this.placeholder = placeholder;
return this;
}
public LoaderOptions error(@DrawableRes int errorResId) {
this.errorResId = errorResId;
return this;
}
public LoaderOptions centerCrop() {
isCenterCrop = true;
return this;
}
public LoaderOptions centerInside() {
isCenterInside = true;
return this;
}
public LoaderOptions config(Bitmap.Config config) {
this.config = config;
return this;
}
public LoaderOptions resize(int targetWidth, int targetHeight) {
this.targetWidth = targetWidth;
this.targetHeight = targetHeight;
return this;
}
/**
* 圓角
* @param bitmapAngle 度數
* @return
*/
public LoaderOptions angle(float bitmapAngle) {
this.bitmapAngle = bitmapAngle;
return this;
}
public LoaderOptions skipLocalCache(boolean skipLocalCache) {
this.skipLocalCache = skipLocalCache;
return this;
}
public LoaderOptions skipNetCache(boolean skipNetCache) {
this.skipNetCache = skipNetCache;
return this;
}
public LoaderOptions rotate(float degrees) {
this.degrees = degrees;
return this;
}
}
當然了,如果覺得有項目中需要可以以LoderOptions為基類繼續擴展LoderOptions,不過現在這樣在LoaderOptions上自行擴展基本上可以滿足所有日常需要了。現在解決了代碼設計的方向,那麽接下來 我們要采取策略的方式實現圖片框架的解耦。
import android.view.View;
import com.squareup.picasso.Callback;
import java.io.File;
/**
* 圖片管理類,提供對外接口。
* 靜態代理模式,開發者只需要關心ImageLoader + LoaderOptions
* Created by MhListener on 2017/6/27.
*/
public class ImageLoader{
private static ILoaderStrategy sLoader;
private static volatile ImageLoader sInstance;
private ImageLoader() {
}
//單例模式
public static ImageLoader getInstance() {
if (sInstance == null) {
synchronized (ImageLoader.class) {
if (sInstance == null) {
//若切換其它圖片加載框架,可以實現一鍵替換
sInstance = new ImageLoader();
}
}
}
return sInstance;
}
//提供實時替換圖片加載框架的接口
public void setImageLoader(ILoaderStrategy loader) {
if (loader != null) {
sLoader = loader;
}
}
public LoaderOptions load(String path) {
return new LoaderOptions(path);
}
public LoaderOptions load(int drawable) {
return new LoaderOptions(drawable);
}
public LoaderOptions load(File file) {
return new LoaderOptions(file);
}
public LoaderOptions load(Uri uri) {
return new LoaderOptions(uri);
}
public void loadOptions(LoaderOptions options) {
sLoader.loadImage(options);
}
public void clearMemoryCache() {
sLoader.clearMemoryCache();
}
public void clearDiskCache() {
sLoader.clearDiskCache();
}
}
最後我們開始圖片加載框架的具體實現方式,這裏我實現了Picasso圖片加載,開發者可以根據此例自行擴展GlideLoader或者FrescoLoader。
public class PicassoLoader implements ILoaderStrategy {
private volatile static Picasso sPicassoSingleton;
private final String PICASSO_CACHE = "picasso-cache";
private static LruCache sLruCache = new LruCache(App.gApp);
private static Picasso getPicasso() {
if (sPicassoSingleton == null) {
synchronized (PicassoLoader.class) {
if (sPicassoSingleton == null) {
sPicassoSingleton = new Picasso.Builder(App.gApp).memoryCache(sLruCache).build();
}
}
}
return sPicassoSingleton;
}
@Override
public void clearMemoryCache() {
sLruCache.clear();
}
@Override
public void clearDiskCache() {
File diskFile = new File(App.gApp.getCacheDir(), PICASSO_CACHE);
if (diskFile.exists()) {
//這邊自行寫刪除代碼
// FileUtil.deleteFile(diskFile);
}
}
@Override
public void loadImage(LoaderOptions options) {
RequestCreator requestCreator = null;
if (options.url != null) {
requestCreator = getPicasso().load(options.url);
} else if (options.file != null) {
requestCreator = getPicasso().load(options.file);
}else if (options.drawableResId != 0) {
requestCreator = getPicasso().load(options.drawableResId);
} else if (options.uri != null){
requestCreator = getPicasso().load(options.uri);
}
if (requestCreator == null) {
throw new NullPointerException("requestCreator must not be null");
}
if (options.targetHeight > 0 && options.targetWidth > 0) {
requestCreator.resize(options.targetWidth, options.targetHeight);
}
if (options.isCenterInside) {
requestCreator.centerInside();
} else if (options.isCenterCrop) {
requestCreator.centerCrop();
}
if (options.config != null) {
requestCreator.config(options.config);
}
if (options.errorResId != 0) {
requestCreator.error(options.errorResId);
}
if (options.placeholderResId != 0) {
requestCreator.placeholder(options.placeholderResId);
}
if (options.bitmapAngle != 0) {
requestCreator.transform(new PicassoTransformation(options.bitmapAngle));
}
if (options.skipLocalCache) {
requestCreator.memoryPolicy(MemoryPolicy.NO_CACHE, MemoryPolicy.NO_STORE);
}
if (options.skipNetCache) {
requestCreator.networkPolicy(NetworkPolicy.NO_CACHE, NetworkPolicy.NO_STORE);
}
if (options.degrees != 0) {
requestCreator.rotate(options.degrees);
}
if (options.targetView instanceof ImageView) {
requestCreator.into(((ImageView)options.targetView));
} else if (options.callBack != null){
requestCreator.into(new PicassoTarget(options.callBack));
}
}
class PicassoTarget implements Target {
BitmapCallBack callBack;
protected PicassoTarget(BitmapCallBack callBack) {
this.callBack = callBack;
}
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
if (this.callBack != null) {
this.callBack.onBitmapLoaded(bitmap);
}
}
@Override
public void onBitmapFailed(Exception e, Drawable errorDrawable) {
if (this.callBack != null) {
this.callBack.onBitmapFailed(e);
}
}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
}
}
class PicassoTransformation implements Transformation {
private float bitmapAngle;
protected PicassoTransformation(float corner){
this.bitmapAngle = corner;
}
@Override
public Bitmap transform(Bitmap source) {
float roundPx = bitmapAngle;//圓角的橫向半徑和縱向半徑
Bitmap output = Bitmap.createBitmap(source.getWidth(),
source.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(output);
final int color = 0xff424242;
final Paint paint = new Paint();
final Rect rect = new Rect(0, 0, source.getWidth(),source.getHeight());
final RectF rectF = new RectF(rect);
paint.setAntiAlias(true);
canvas.drawARGB(0, 0, 0, 0);
paint.setColor(color);
canvas.drawRoundRect(rectF, roundPx, roundPx, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(source, rect, rect, paint);
source.recycle();
return output;
}
@Override
public String key() {
return "bitmapAngle()";
}
}
}
好了,到了這裏,關於圖片框架的封裝已經全部完成。而且該圖片框架的封裝已經成功應用到公司項目上,目前反饋良好。如有問題,歡迎交流指教!
如果感興趣的話,歡迎在github給個star。代碼已上傳github鏈接
如果有考慮引用該封裝的話,可以采用下面的方式:
//根目錄下build.gradle配置
allprojects {
repositories {
...
maven { url ‘https://jitpack.io‘ }
}
}
//項目build.gradle依賴
dependencies {
compile ‘com.github.mhlistener:ImageLoader:1.0.5‘
}
//使用方式
1.Application中全局設置
ImageLoader.getInstance().setGlobalImageLoader(new PicassoLoader());
2.界面中使用封裝
ImageView imageView = findViewById(R.id.imageview);
String url = "http://ww2.sinaimg.cn/large/7a8aed7bgw1eutsd0pgiwj20go0p0djn.jpg";
ImageLoader.getInstance()
.load(url)
.angle(80)
.resize(400, 600)
.centerCrop()
.config(Bitmap.Config.RGB_565)
.placeholder(R.mipmap.test)
.error(R.mipmap.test)
.skipLocalCache(true)
.into(imageView);
優雅地實現Android主流圖片加載框架封裝,可無侵入切換框架