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

GitHub: 統一的圖片加載架構css

前言

對於圖片加載框架,你們用到的多是Glide,Picasso或者Fresco,這基本上是主流的圖片加載框架,咱們使用它的時候,大都感受如臂使指,簡直愉快的不要不要的。可是咱們仍是發現至少有兩個問題,以Glide爲例,第一,當需求變更,你須要對圖片加載失敗時的情景添加一個單獨的佔位符,這個時候你就不得不在每個使用到Glide的地方去添加這樣的設置;第二,當你須要對項目進行重構時,或者目前的圖片加載框架沒法實現某些需求,而須要替換的時候,你可能仍是須要對原有項目大動干戈。git

你們回顧本身手頭上的代碼,不知道是否都面臨這樣的隱患?反正當我看到咱們團隊的項目代碼的時候,個人頭老是比平時大兩倍...你問我爲啥?一堆歷史遺留問題,好比最先就直接在項目中使用Glide,後來我建議說,至少稍微作點封裝,畢竟吃相不能太難看,因而才作了一層封裝,卻依然經不起新需求的考驗,更別提替換框架的程度了了(這可能就是爲何咱們團隊轉向了RN,由於誰都不想看過去的代碼了)。github

若是你覺得這是由於我是一個完美主義者,那麼可能沒有嘗試過一行一行粘貼複製,刪除重構的日子。設計模式

廢話講完,咱們正是開始吧api

封裝的新使命

咱們先聊聊封裝,封裝的好處你們都很熟悉,對外提供簡單接口屏蔽內部複雜,保護數據,保證安全....等,你們可能基本上都滾瓜爛熟了, 現在咱們在開發Android項目的時候封裝的主要目的卻再也不是這些了,爲何,由於咱們全部諸如okhttp,retrofit,Glide,等等框架自己就實現了完美的封裝,並達成了對外提供簡單接口屏蔽內部複雜,保護數據,保證安全等目的,若是僅僅是爲了這些目的,咱們大可沒必要在作封裝。緩存

那麼咱們封裝的新的使命是什麼呢,是爲了達成對模塊的控制,什麼意思呢?仍是以圖片加載框架爲例,假如你直接在業務代碼中使用了Glide,Picasso或者Fresco的話,也就意味着,你把圖片加載的控制權徹底交給了他們,後面你想對圖片加載流程作任何改動,你都須要一個一個去修改,那麼你就喪失了對圖片加載模塊的控制權。因此,我所說的對於模塊的控制,是你隨時可以以很小的代價修改甚至替換整個模塊。安全

這也是爲何如今各類發開框架已經把本身封裝的如此之好的狀況下,咱們依然須要對它作封裝的緣由。bash

好了,接下來,咱們就分析具體問題。架構


從封裝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 !

後記

假如你實在理解不了控制力這個概念,也能夠理解爲打造高內聚低耦合的模塊

相關文章
相關標籤/搜索