原文地址:https://juejin.im/post/58b280b92f301e0068078669
GitHub: 統(tǒng)一的圖片加載架構(gòu)
前言
對(duì)于圖片加載框架芬沉,大家用到的可能是Glide躺同,Picasso或者Fresco,這基本上是主流的圖片加載框架丸逸,我們使用它的時(shí)候蹋艺,大都感覺(jué)如臂使指,簡(jiǎn)直愉快的不要不要的黄刚。但是我們還是發(fā)現(xiàn)至少有兩個(gè)問(wèn)題捎谨,以Glide為例,第一隘击,當(dāng)需求變動(dòng),你需要對(duì)圖片加載失敗時(shí)的情景添加一個(gè)單獨(dú)的占位符研铆,這個(gè)時(shí)候你就不得不在每一個(gè)使用到Glide的地方去添加這樣的設(shè)置埋同;第二,當(dāng)你需要對(duì)項(xiàng)目進(jìn)行重構(gòu)時(shí)棵红,或者目前的圖片加載框架無(wú)法實(shí)現(xiàn)某些需求凶赁,而需要替換的時(shí)候,你可能還是需要對(duì)原有項(xiàng)目大動(dòng)干戈逆甜。
大家回顧自己手頭上的代碼虱肄,不知道是否都面臨這樣的隱患?反正當(dāng)我看到我們團(tuán)隊(duì)的項(xiàng)目代碼的時(shí)候交煞,我的頭總是比平時(shí)大兩倍...你問(wèn)我為啥咏窿?一堆歷史遺留問(wèn)題,比如最早就直接在項(xiàng)目中使用Glide,后來(lái)我建議說(shuō)素征,至少稍微做點(diǎn)封裝集嵌,畢竟吃相不能太難看,于是才做了一層封裝御毅,卻依然經(jīng)不起新需求的考驗(yàn)根欧,更別提替換框架的程度了了(這可能就是為什么我們團(tuán)隊(duì)轉(zhuǎn)向了RN,因?yàn)檎l(shuí)都不想看過(guò)去的代碼了)。
如果你以為這是因?yàn)槲沂且粋€(gè)完美主義者端蛆,那么可能沒(méi)有嘗試過(guò)一行一行粘貼復(fù)制凤粗,刪除重構(gòu)的日子。
廢話講完今豆,我們正是開(kāi)始吧
封裝的新使命
我們先聊聊封裝嫌拣,封裝的好處大家都很熟悉柔袁,對(duì)外提供簡(jiǎn)單接口屏蔽內(nèi)部復(fù)雜,保護(hù)數(shù)據(jù)亭罪,保證安全....等瘦馍,大家可能基本上都倒背如流了, 如今我們?cè)陂_(kāi)發(fā)Android項(xiàng)目的時(shí)候封裝的主要目的卻不再是這些了应役,為什么情组,因?yàn)槲覀兯兄T如okhttp,retrofit,Glide,等等框架本身就實(shí)現(xiàn)了完美的封裝箩祥,并達(dá)成了對(duì)外提供簡(jiǎn)單接口屏蔽內(nèi)部復(fù)雜院崇,保護(hù)數(shù)據(jù),保證安全等目的袍祖,如果僅僅是為了這些目的底瓣,我們大可不必在做封裝。
那么我們封裝的新的使命是什么呢蕉陋,是為了達(dá)成對(duì)模塊的控制捐凭,什么意思呢?還是以圖片加載框架為例凳鬓,假如你直接在業(yè)務(wù)代碼中使用了Glide,Picasso或者Fresco的話茁肠,也就意味著,你把圖片加載的控制權(quán)完全交給了他們缩举,后面你想對(duì)圖片加載流程做任何改動(dòng)垦梆,你都需要一個(gè)一個(gè)去修改,那么你就喪失了對(duì)圖片加載模塊的控制權(quán)仅孩。所以托猩,我所說(shuō)的對(duì)于模塊的控制,是你隨時(shí)能夠以很小的代價(jià)修改甚至替換整個(gè)模塊辽慕。
這也是為什么現(xiàn)在各種發(fā)開(kāi)框架已經(jīng)把自己封裝的如此之好的情況下京腥,我們依然需要對(duì)它做封裝的原因。
好了溅蛉,接下來(lái)绞旅,我們就分析具體問(wèn)題。
從封裝Glide開(kāi)始
以Glide為例温艇,Glid通過(guò)鏈?zhǔn)秸{(diào)用因悲,可以隨意的調(diào)用各種圖片加載相關(guān)的設(shè)定,如緩存策略勺爱,動(dòng)畫(huà)晃琳,占位符等等,各類api數(shù)不勝數(shù),而我們現(xiàn)在先要把這些調(diào)用抽象成一個(gè)接口卫旱,進(jìn)而就能輕松實(shí)現(xiàn)對(duì)它的封裝人灼。
一個(gè)簡(jiǎn)單的Glide的調(diào)用可能是這樣的:
Glide.with(getContext())
.load(url)
.skipMemoryCache(true)
.placeholder(drawable)
.centerCrop()
.animate(animator)
.into(img);
盡管沒(méi)有使用Glide所有的圖片加載相關(guān)的設(shè)置,但是大家應(yīng)該能感受到顾翼,它的圖片加載設(shè)置選項(xiàng)十分豐富投放,也很隨意,那么我們究竟應(yīng)該如何把它封裝到一個(gè)接口里面去呢适贸?可能你首先想到是這種:
public interface ImageLoader{
static void showImage(ImageView v, Context context,String url, boolean skipMemoryCache,int placeholder,ViewPropertyAnimation.Animator animator)
}
這顯然是很有問(wèn)題的灸芳,對(duì)于一個(gè)有很多可選項(xiàng)的接口做封裝,既要保留豐富的可選項(xiàng)拜姿,還要保證統(tǒng)一而簡(jiǎn)潔的調(diào)用烙样。這么一長(zhǎng)串參數(shù)顯然有傷大雅。
那么應(yīng)該如何設(shè)計(jì)呢蕊肥?我們可以從這個(gè)角度來(lái)分析谒获,對(duì)于圖片加載而言,什么是最基本最重要的必選項(xiàng)壁却,什么是可有可無(wú)的可選項(xiàng):
- 必選項(xiàng):url(圖片來(lái)源)批狱,ImageView(圖片容器),上下文環(huán)境(Context)
- 可選項(xiàng):除此必選項(xiàng)之外的所有
那么我們的接口初具雛形了
public interface ImageLoader{
void showImage(ImageView imageview, String url, Context context,ImageLoaderOptions options);
void showImage(ImageView imageview,int drawable,Context context,ImageLoaderOptions options);
}
這樣是不是就好了呢展东?也不是赔硫,我們還可以在繼續(xù)探索,
我們發(fā)現(xiàn)ImageView內(nèi)部其實(shí)包含了Context這個(gè)參數(shù)琅锻,完全可以省略卦停,所以我們的基本參數(shù)應(yīng)該是:url,ImageView,options向胡,
public interface ImageLoader{
void showImage(ImageView imageview, String url, ImageLoaderOptions options);
void showImage(ImageView imageview, int drawable,ImageLoaderOptions options);
}
然后我們?cè)賮?lái)看看方法中定義的ImageLoaderOptions恼蓬,這個(gè)其實(shí)比較簡(jiǎn)單,基本上Glide有多少可選項(xiàng)僵芹,你就可以往里面加多少屬性处硬。由于這些屬性都是可選擇的,因此我們需要使用Builder模式來(lái)構(gòu)建它拇派,具體就不贅述了荷辕。
那么,到這里件豌,我們對(duì)于Glide的封裝的設(shè)計(jì)就基本完成了疮方。
統(tǒng)一的圖片加載架構(gòu)
我們說(shuō)了想要打造一個(gè)統(tǒng)一的圖片加載框架,也就是說(shuō)茧彤,不管Glide骡显,還是Fresco,或者Picasso都能在這套架構(gòu)下愉快的玩耍。其實(shí)我們只要在封裝Glide的基礎(chǔ)上進(jìn)一步的做出改進(jìn)即可惫谤,因?yàn)楫?dāng)我們封裝Glide的時(shí)候壁顶,就已經(jīng)是對(duì)圖片加載的抽象了。
我們首先來(lái)看溜歪,之前抽象的接口總體上在其他的圖片加載框架中都是可用的若专,不過(guò)由于Fresco的特殊設(shè)計(jì),自己實(shí)現(xiàn)了圖片容器蝴猪,導(dǎo)致了一點(diǎn)問(wèn)題调衰,但是這也很簡(jiǎn)單,我們?cè)诮涌诶锩嬗肰iew作為圖片容器即可拯腮。
public interface ImageLoader{
void showImage(View v, String url, ImageLoaderOptions options);
void showImage(View v, int drawable,ImageLoaderOptions options);
}
好了窖式,上面這個(gè)接口基本上可以完美兼容Glide,Picasso动壤,F(xiàn)resco這三種加載庫(kù)萝喘,現(xiàn)在的問(wèn)題是如何實(shí)現(xiàn)他們的可替換。這個(gè)時(shí)候我們就需要一種設(shè)計(jì)模式(策略模式迫不及待的跳出來(lái)說(shuō)琼懊,選我選我8篝ぁ)
沒(méi)錯(cuò),就是策略模式哼丈,它的設(shè)計(jì)圖如下:
(圖片畫(huà)的不好启妹,大家多多包含)
至此,我們?cè)谠O(shè)計(jì)上已經(jīng)完成了一個(gè)統(tǒng)一的圖片加載架構(gòu)的設(shè)計(jì)醉旦,但是有一個(gè)問(wèn)題我特意留到了最后饶米,就是ImageLoaderOptions的內(nèi)部的構(gòu)造。
當(dāng)我們只需要封裝一個(gè)Glide的時(shí)候车胡,ImageLoaderOptions可以和Glide中的那些設(shè)置項(xiàng)完全匹配檬输,只要你愿意,你可以把Glide里面的所有圖片加載的相關(guān)的設(shè)置項(xiàng)都放進(jìn)去匈棘。但是丧慈,如果我們要兼容三個(gè)加載框架甚至更多的時(shí)候,還能這樣做么主卫?
理論上是可以的逃默,不過(guò)當(dāng)你這么干了,那么ImageLoaderOptions內(nèi)部可能是可能是這樣的:
public class ImageLoaderOptions {
//Glide的設(shè)置項(xiàng)
private int placeHolder=-1; //當(dāng)沒(méi)有成功加載的時(shí)候顯示的圖片
private ImageReSize size=null; //重新設(shè)定容器寬高
private int errorDrawable=-1; //加載錯(cuò)誤的時(shí)候顯示的drawable
private boolean isCrossFade=false; //是否漸變平滑的顯示圖片
private boolean isSkipMemoryCache = false; //是否跳過(guò)內(nèi)存緩存
private ViewPropertyAnimation.Animator animator = null; // 圖片加載動(dòng)畫(huà)
...
...
//Fresco的設(shè)置項(xiàng)
private int placeHolder=-1; //當(dāng)沒(méi)有成功加載的時(shí)候顯示的圖片
private Drawable pressedStateOverlay =null; //按下時(shí)顯示的圖層
private boolean isCrossFade=false; //是否漸變平滑的顯示圖片
...
...
}
大家很容易發(fā)現(xiàn)簇搅,其實(shí)各個(gè)圖片加載框架之間的設(shè)置項(xiàng)很多功能都是重疊的完域,比如占位符,漸進(jìn)加載瘩将,緩存等等吟税,也有一些設(shè)置項(xiàng)是類似的关噪,因此實(shí)際上我們應(yīng)該把他們合并在一起,也就是說(shuō)乌妙,當(dāng)我們思考對(duì)于ImageLoaderOptions的設(shè)計(jì)的時(shí)候使兔,我們應(yīng)該首先把幾個(gè)框架共同和相似的設(shè)置項(xiàng)合并,因?yàn)檫@代表著圖片加載領(lǐng)域最普遍最重要的需求藤韵。其次我們?cè)侔葱杓尤胱约盒枰母鱾€(gè)框架之間有差異的設(shè)置項(xiàng)虐沥。
下面是我對(duì)于這個(gè)統(tǒng)一圖片加載架構(gòu)的具體實(shí)現(xiàn),大家可以僅作參考泽艘。
接口定義
public interface ImageLoaderStrategy{
void showImage(View v, String url, ImageLoaderOptions options);
void showImage(View v, int drawable,ImageLoaderOptions options);
}
設(shè)置項(xiàng)定義
public class ImageLoaderOptions {
//你可以把三個(gè)圖片加載框架所有的共同或相似設(shè)置項(xiàng)搬過(guò)來(lái)欲险,現(xiàn)在僅僅用以下幾種作為范例演示。
private int placeHolder=-1; //當(dāng)沒(méi)有成功加載的時(shí)候顯示的圖片
private ImageReSize size=null; //重新設(shè)定容器寬高
private int errorDrawable=-1; //加載錯(cuò)誤的時(shí)候顯示的drawable
private boolean isCrossFade=false; //是否漸變平滑的顯示圖片
private boolean isSkipMemoryCache = false; //是否跳過(guò)內(nèi)存緩存
private ViewPropertyAnimation.Animator animator = null; // 圖片加載動(dòng)畫(huà)
private ImageLoaderOptions(ImageReSize resize, int placeHolder, int errorDrawable, boolean isCrossFade, boolean isSkipMemoryCache, ViewPropertyAnimation.Animator animator){
this.placeHolder=placeHolder;
this.size=resize;
this.errorDrawable=errorDrawable;
this.isCrossFade=isCrossFade;
this.isSkipMemoryCache=isSkipMemoryCache;
this.animator=animator;
}
public class ImageReSize{
int reWidth=0;
int reHeight=0;
public ImageReSize(int reWidth,int reHeight){
if (reHeight<=0){
reHeight=0;
}
if (reWidth<=0) {
reWidth=0;
}
this.reHeight=reHeight;
this.reWidth=reWidth;
}
}
public static final class Builder {
private int placeHolder=-1;
private ImageReSize size=null;
private int errorDrawable=-1;
private boolean isCrossFade =false;
private boolean isSkipMemoryCache = false;
private ViewPropertyAnimation.Animator animator = null;
public Builder (){
}
public Builder placeHolder(int drawable){
this.placeHolder=drawable;
return this;
}
public Builder reSize(ImageReSize size){
this.size=size;
return this;
}
public Builder anmiator(ViewPropertyAnimation.Animator animator){
this.animator=animator;
return this;
}
public Builder errorDrawable(int errorDrawable){
this.errorDrawable=errorDrawable;
return this;
}
public Builder isCrossFade(boolean isCrossFade){
this.isCrossFade=isCrossFade;
return this;
}
public Builder isSkipMemoryCache(boolean isSkipMemoryCache){
this.isSkipMemoryCache=isSkipMemoryCache;
return this;
}
public ImageLoaderOptions build(){
return new ImageLoaderOptions(this.size,this.placeHolder,this.errorDrawable,this.isCrossFade,this.isSkipMemoryCache,this.animator);
}
}
下面以Glide實(shí)現(xiàn)該接口的方式:
public class GlideImageLoaderStrategy implements ImageLoaderStrategy {
@Override
public void showImage(View v, String url, ImageLoaderOptions options) {
if (v instanceof ImageView) {
//將類型轉(zhuǎn)換為ImageView
ImageView imageView= (ImageView) v;
//裝配基本的參數(shù)
DrawableTypeRequest dtr = Glide.with(imageView.getContext()).load(url);
//裝配附加參數(shù)
loadOptions(dtr, options).into(imageView);
}
}
@Override
public void showImage(View v, int drawable, ImageLoaderOptions options) {
if (v instanceof ImageView) {
ImageView imageView= (ImageView) v;
DrawableTypeRequest dtr = Glide.with(imageView.getContext()).load(drawable);
loadOptions(dtr, options).into(imageView);
}
}
//這個(gè)方法用來(lái)裝載由外部設(shè)置的參數(shù)
private DrawableTypeRequest loadOptions(DrawableTypeRequest dtr,ImageLoaderOptions options){
if (options==null) {
return dtr;
}
if (options.getPlaceHolder()!=-1) {
dtr.placeholder(options.getPlaceHolder());
}
if (options.getErrorDrawable()!=-1){
dtr.error(options.getErrorDrawable());
}
if (options.isCrossFade()) {
dtr.crossFade();
}
if (options.isSkipMemoryCache()){
dtr.skipMemoryCache(options.isSkipMemoryCache());
}
if (options.getAnimator()!=null) {
dtr.animate(options.getAnimator());
}
if (options.getSize()!=null) {
dtr.override(options.getSize().reWidth,options.getSize().reHeight);
}
return dtr;
}
}
Picsso,Fresco的接口實(shí)現(xiàn)類依照Glide匹涮。
下面就是最后一步天试,實(shí)現(xiàn)整個(gè)圖片加載架構(gòu)的管理類,用于對(duì)外提供圖片加載服務(wù)和圖片加載框架的替換
public class ImageLoaderStrategyManager implements ImageLoaderStrategy {
private static final ImageLoaderStrategyManager INSTANCE = new ImageLoaderStrategyManager();
private ImageLoaderStrategy imageLoader;
private ImageLoaderStrategyManager(){
//默認(rèn)使用Glide
imageLoader=new GlideImageLoaderStrategy();
}
public static ImageLoaderStrategyManager getInstance(){
return INSTANCE;
}
//可實(shí)時(shí)替換圖片加載框架
public void setImageLoader(ImageLoaderStrategy loader) {
if (loader != null) {
imageLoader=loader;
}
}
@Override
public void showImage(@NonNull View mView, @NonNull String mUrl, @Nullable ImageLoaderOptions options) {
imageLoader.showImage(mView,mUrl,options);
}
@Override
public void showImage(@NonNull View mView, @NonNull int mDraeable, @Nullable ImageLoaderOptions options) {
imageLoader.showImage(mView,mDraeable,options);
}
}
至此然低,整個(gè)圖片加載架構(gòu)都已經(jīng)設(shè)計(jì)完畢了喜每,我們也可以基本實(shí)現(xiàn)了對(duì)圖片加載模塊的控制。
這個(gè)小的圖片加載架構(gòu)是不是已經(jīng)很完美了呢雳攘?其實(shí)也不是带兜,由于Fresco的特殊,當(dāng)我們切換到Fresco吨灭,或者從Fresco切換到其他加載框架的時(shí)候刚照,我們可能仍然需要到處去修改xml文件的圖片容器節(jié)點(diǎn)(ImageView/DraweeView),因?yàn)镕resco使用的時(shí)自家的組件喧兄。不過(guò)我也考慮過(guò)一種解決方案无畔,那就是把圖片容器(ImageView/DraweeView)節(jié)點(diǎn)放在一個(gè)單獨(dú)的xml文件中,使用merge的方式添加到布局文件中吠冤,并在代碼層面使統(tǒng)一用View 來(lái)獲取圖片容器(ImageView/DraweeView)的實(shí)例做相應(yīng)操作浑彰。