封裝並實現統一的圖片載入架構

拉丁吳發表於2017-02-27

GitHub: 統一的圖片載入架構

前言

對於圖片載入框架,大家用到的可能是Glide,Picasso或者Fresco,這基本上是主流的圖片載入框架,我們使用它的時候,大都感覺如臂使指,簡直愉快的不要不要的。但是我們還是發現至少有兩個問題,以Glide為例,第一,當需求變動,你需要對圖片載入失敗時的情景新增一個單獨的佔位符,這個時候你就不得不在每一個使用到Glide的地方去新增這樣的設定;第二,當你需要對專案進行重構時,或者目前的圖片載入框架無法實現某些需求,而需要替換的時候,你可能還是需要對原有專案大動干戈。

大家回顧自己手頭上的程式碼,不知道是否都面臨這樣的隱患?反正當我看到我們團隊的專案程式碼的時候,我的頭總是比平時大兩倍...你問我為啥?一堆歷史遺留問題,比如最早就直接在專案中使用Glide,後來我建議說,至少稍微做點封裝,畢竟吃相不能太難看,於是才做了一層封裝,卻依然經不起新需求的考驗,更別提替換框架的程度了了(這可能就是為什麼我們團隊轉向了RN,因為誰都不想看過去的程式碼了)。

如果你以為這是因為我是一個完美主義者,那麼可能沒有嘗試過一行一行貼上複製,刪除重構的日子。

廢話講完,我們正是開始吧

封裝的新使命

我們先聊聊封裝,封裝的好處大家都很熟悉,對外提供簡單介面遮蔽內部複雜,保護資料,保證安全....等,大家可能基本上都倒背如流了, 如今我們在開發Android專案的時候封裝的主要目的卻不再是這些了,為什麼,因為我們所有諸如okhttp,retrofit,Glide,等等框架本身就實現了完美的封裝,並達成了對外提供簡單介面遮蔽內部複雜,保護資料,保證安全等目的,如果僅僅是為了這些目的,我們大可不必在做封裝。

那麼我們封裝的新的使命是什麼呢,是為了達成對模組的控制,什麼意思呢?還是以圖片載入框架為例,假如你直接在業務程式碼中使用了Glide,Picasso或者Fresco的話,也就意味著,你把圖片載入的控制權完全交給了他們,後面你想對圖片載入流程做任何改動,你都需要一個一個去修改,那麼你就喪失了對圖片載入模組的控制權。所以,我所說的對於模組的控制,是你隨時能夠以很小的代價修改甚至替換整個模組。

這也是為什麼現在各種發開框架已經把自己封裝的如此之好的情況下,我們依然需要對它做封裝的原因。

好了,接下來,我們就分析具體問題。


從封裝Glide開始

以Glide為例,Glid通過鏈式呼叫,可以隨意的呼叫各種圖片載入相關的設定,如快取策略,動畫,佔位符等等,各類api數不勝數,而我們現在先要把這些呼叫抽象成一個介面,進而就能輕鬆實現對它的封裝。

一個簡單的Glide的呼叫可能是這樣的:

        Glide.with(getContext())
                .load(url)
                .skipMemoryCache(true)
                .placeholder(drawable)
                .centerCrop()
                .animate(animator)
                .into(img);複製程式碼

儘管沒有使用Glide所有的圖片載入相關的設定,但是大家應該能感受到,它的圖片載入設定選項十分豐富,也很隨意,那麼我們究竟應該如何把它封裝到一個介面裡面去呢?可能你首先想到是這種:

public interface ImageLoader{
    static void showImage(ImageView v, Context context,String url, boolean skipMemoryCache,int placeholder,ViewPropertyAnimation.Animator animator)
}複製程式碼

這顯然是很有問題的,對於一個有很多可選項的介面做封裝,既要保留豐富的可選項,還要保證統一而簡介的呼叫。這麼一長串引數顯然有傷大雅。

那麼應該如何設計呢?我們可以從這個角度來分析,對於圖片載入而言,什麼是最基本最重要的必選項,什麼是可有可無的可選項:

  • 必選項:url(圖片來源),ImageView(圖片容器),上下文環境(Context)
  • 可選項:除此必選項之外的所有

那麼我們的介面初具雛形了

public interface ImageLoader{
    void showImage(ImageView imageview, String url, Context context,ImageLoaderOptions options);
    void showImage(ImageView imageview,int drawable,Context context,ImageLoaderOptions options);
}複製程式碼

這樣是不是就好了呢?也不是,我們還可以在繼續探索,
我們發現ImageView內部其實包含了Context這個引數,完全可以省略,所以我們的基本引數應該是:url,ImageView,options,

public interface ImageLoader{
    void showImage(ImageView imageview,  String url, ImageLoaderOptions options);
    void showImage(ImageView imageview, int drawable,ImageLoaderOptions options);
}複製程式碼

然後我們再來看看方法中定義的ImageLoaderOptions,這個其實比較簡單,基本上Glide有多少可選項,你就可以往裡面加多少屬性。由於這些屬性都是可選擇的,因此我們需要使用Builder模式來構建它,具體就不贅述了。

那麼,到這裡,我們對於Glide的封裝的設計就基本完成了。


統一的圖片載入架構

我們說了想要打造一個統一的圖片載入框架,也就是說,不管Glide,還是Fresco,或者Picasso都能在這套架構下愉快的玩耍。其實我們只要在封裝Glide的基礎上進一步的做出改進即可,因為當我們封裝Glide的時候,就已經是對圖片載入的抽象了。

我們首先來看,之前抽象的介面總體上在其他的圖片載入框架中都是可用的,不過由於Fresco的特殊設計,自己實現了圖片容器,導致了一點問題,但是這也很簡單,我們在介面裡面用View作為圖片容器即可。

public interface ImageLoader{
    void showImage(View v,  String url, ImageLoaderOptions options);
    void showImage(View v, int drawable,ImageLoaderOptions options);
}複製程式碼

好了,上面這個介面基本上可以完美相容Glide,Picasso,Fresco這三種載入庫,現在的問題是如何實現他們的可替換。這個時候我們就需要一種設計模式(策略模式迫不及待的跳出來說,選我選我!)

沒錯,就是策略模式,它的設計圖如下:


(圖片畫的不好,大家多多包含)

至此,我們在設計上已經完成了一個統一的圖片載入架構的設計,但是有一個問題我特意留到了最後,就是ImageLoaderOptions的內部的構造。

當我們只需要封裝一個Glide的時候,ImageLoaderOptions可以和Glide中的那些設定項完全匹配,只要你願意,你可以把Glide裡面的所有圖片載入的相關的設定項都放進去。但是,如果我們要相容三個載入框架甚至更多的時候,還能這樣做麼?

理論上是可以的,不過當你這麼幹了,那麼ImageLoaderOptions內部可能是可能是這樣的:

public class ImageLoaderOptions {
    //Glide的設定項
    private int placeHolder=-1; //當沒有成功載入的時候顯示的圖片
    private ImageReSize size=null; //重新設定容器寬高
    private int errorDrawable=-1;  //載入錯誤的時候顯示的drawable
    private boolean isCrossFade=false; //是否漸變平滑的顯示圖片
    private  boolean isSkipMemoryCache = false; //是否跳過記憶體快取
    private   ViewPropertyAnimation.Animator animator = null; // 圖片載入動畫
    ...
    ...
     //Fresco的設定項
    private int placeHolder=-1; //當沒有成功載入的時候顯示的圖片
    private Drawable  pressedStateOverlay =null;  //按下時顯示的圖層
    private boolean isCrossFade=false; //是否漸變平滑的顯示圖片

    ...
    ...
}複製程式碼

大家很容易發現,其實各個圖片載入框架之間的設定項很多功能都是重疊的,比如佔位符,漸進載入,快取等等,也有一些設定項是類似的,因此實際上我們應該把他們合併在一起,也就是說,當我們思考對於ImageLoaderOptions的設計的時候,我們應該首先把幾個框架共同和相似的設定項合併,因為這代表著圖片載入領域最普遍最重要的需求。其次我們再按需加入自己需要的各個框架之間有差異的設定項。

下面是我對於這個統一圖片載入架構的具體實現,大家可以僅作參考。

介面定義

public interface ImageLoaderStrategy{
    void showImage(View v,  String url, ImageLoaderOptions options);
    void showImage(View v, int drawable,ImageLoaderOptions options);
}複製程式碼

設定項定義

public class ImageLoaderOptions {
    //你可以把三個圖片載入框架所有的共同或相似設定項搬過來,現在僅僅用以下幾種作為範例演示。
    private int placeHolder=-1; //當沒有成功載入的時候顯示的圖片
    private ImageReSize size=null; //重新設定容器寬高
    private int errorDrawable=-1;  //載入錯誤的時候顯示的drawable
    private boolean isCrossFade=false; //是否漸變平滑的顯示圖片
    private  boolean isSkipMemoryCache = false; //是否跳過記憶體快取
    private   ViewPropertyAnimation.Animator animator = null; // 圖片載入動畫


    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實現該介面的方式:

public class GlideImageLoaderStrategy implements ImageLoaderStrategy {

    @Override
    public void showImage(View v, String url, ImageLoaderOptions options) {
        if (v instanceof ImageView) {
            //將型別轉換為ImageView
            ImageView imageView= (ImageView) v;
            //裝配基本的引數
            DrawableTypeRequest dtr = Glide.with(imageView.getContext()).load(url);
            //裝配附加引數
            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);
        }
    }
    //這個方法用來裝載由外部設定的引數
    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的介面實現類依照Glide。

下面就是最後一步,實現整個圖片載入架構的管理類,用於對外提供圖片載入服務和圖片載入框架的替換

public class ImageLoaderStrategyManager implements ImageLoaderStrategy {
    private static final ImageLoaderStrategyManager INSTANCE = new ImageLoaderStrategyManager();
    private ImageLoaderStrategy imageLoader;
    private ImageLoaderStrategyManager(){
        //預設使用Glide
        imageLoader=new GlideImageLoaderStrategy();
    }
    public static ImageLoaderStrategyManager getInstance(){
        return INSTANCE;
    }
    //可實時替換圖片載入框架
  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);
    }

}複製程式碼

至此,整個圖片載入架構都已經設計完畢了,我們也可以基本實現了對圖片載入模組的控制。

這個小的圖片載入架構是不是已經很完美了呢?其實也不是,由於Fresco的特殊,當我們切換到Fresco,或者從Fresco切換到其他載入框架的時候,我們可能仍然需要到處去修改xml檔案的圖片容器節點(ImageView/DraweeView),因為Fresco使用的時自家的元件。不過我也考慮過一種解決方案,那就是把圖片容器(ImageView/DraweeView)節點放在一個單獨的xml檔案中,使用merge的方式新增到佈局檔案中,並在程式碼層面使統一用View 來獲取圖片容器(ImageView/DraweeView)的例項做相應操作。


專案已經上傳了github,點此獲取,求star !

後記

假如你實在理解不了控制力這個概念,也可以理解為打造高內聚低耦合的模組

相關文章