優(yōu)雅地實現(xiàn)Android主流圖片加載框架封裝,可無侵入切換框架

前言

項目開發(fā)中,往往會隨著需求的改變而切換到其它圖片加載框架上去。如果最初代碼設(shè)計的耦合度太高,那么恭喜你,成功入坑了。至今無法忘卻整個項目一行行去復(fù)制粘貼被支配的恐懼。:)

2.gif

那么是否存在一種方式 能夠一勞永逸地解決這個痛點呢?下面我們來分析一下圖片加載框架面對的現(xiàn)狀和解決思路。

問題現(xiàn)狀

一個優(yōu)秀的框架一般在代碼設(shè)計的時候已經(jīng)封裝很不錯了,對于開發(fā)者而言框架的使用也是很方便,但是為什么說我們往往還要去做這方面的框架封裝呢?原因很簡單,實際項目開發(fā)中,我們不得不面對著日新月異的需求變化,想要在這個變化中最大程度的實現(xiàn)代碼的可擴(kuò)展性和變通性(當(dāng)然還可以偷懶),不能因為牽一發(fā)而動全身,同時要將框架適配到實際項目,框架的再封裝設(shè)計顯得尤為重要。
不多廢話,我們可以開始今天的圖片封裝之路了。

20170610145545.gif

設(shè)計思路

圖片框架的封裝主要需要滿足以下三點:

  • 低耦合,方便將來的代碼擴(kuò)展。至少要支持目前市場上使用率最高的圖片框架Fresco、Glide、Picasso三者之間的切換
  • 滿足項目中各種需求
  • 調(diào)用方便

談到圖片封裝,最先想到的是把一些常用的功能點作為參數(shù)傳入到方法內(nèi),然后調(diào)用圖片加載框架實現(xiàn)我們圖片的加載工作。比如說像下面這樣

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);

}

然后分別寫對應(yīng)的ImageLoader實現(xiàn)類FrescoImageLoader、GlideImageLoader、PicassoImageLoader,最后采用策略的設(shè)計模式實現(xiàn)代碼的切換。那么這種方式實際效果如何呢?實際開發(fā)中很明顯的一個 問題就是,對于每一個需要的參數(shù)都需要進(jìn)行對應(yīng)的封裝,就不止上面所提到的兩個方法,我們需要封裝大量的方法去滿足實際的項目需要,而且每個框架的很多屬性不一致,如果切換圖片框架的話,還是需要大量的切換成本的。
于是我們想到了下面的這種思路

public interface ILoaderProxy {

    void loadImage(LoaderOptions options);

    /**
     * 清理內(nèi)存緩存
     */
    void clearMemoryCache();

    /**
     * 清理磁盤緩存
     */
    void clearDiskCache();
}

提取各個框架通用的View,path/file文件路徑,通過LoaderOptions解決大量不同參數(shù)傳入的問題。這里需要說明的是,LoaderOptions中采用控件View,而不是ImageView,主要考慮到Fresco圖片框架采用了DraweeView,這里保留了設(shè)計的擴(kuò)展性。而圖片參數(shù)類LoaderOptions采用了Builder設(shè)計模式:


/**
 * Created by JohnsonFan on 2017/7/13.
 * 該類為圖片加載框架的通用屬性封裝,不能耦合任何一方的框架
 */
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; //旋轉(zhuǎn)角度.注意:picasso針對三星等本地圖片,默認(rèn)旋轉(zhuǎn)回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   度數(shù)
     * @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;
    }

}

當(dāng)然了,如果覺得有項目中需要可以以LoderOptions為基類繼續(xù)擴(kuò)展LoderOptions,不過現(xiàn)在這樣在LoaderOptions上自行擴(kuò)展基本上可以滿足所有日常需要了?,F(xiàn)在解決了代碼設(shè)計的方向,那么接下來 我們要采取策略的方式實現(xiàn)圖片框架的解耦。


import android.view.View;

import com.squareup.picasso.Callback;

import java.io.File;

/**
 * 圖片管理類,提供對外接口。
 * 靜態(tài)代理模式,開發(fā)者只需要關(guān)心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) {
                    //若切換其它圖片加載框架,可以實現(xiàn)一鍵替換
                    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();
    }
}

最后我們開始圖片加載框架的具體實現(xiàn)方式,這里我實現(xiàn)了Picasso圖片加載,開發(fā)者可以根據(jù)此例自行擴(kuò)展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()";
        }
    }

}

好了,到了這里,關(guān)于圖片框架的封裝已經(jīng)全部完成。而且該圖片框架的封裝已經(jīng)成功應(yīng)用到公司項目上,目前反饋良好。如有問題,歡迎交流指教!


20160926192639.gif

如果感興趣的話,歡迎在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中全局設(shè)置
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);
圖片加載效果圖.jpg
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,226評論 25 708
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,433評論 4 61
  • 思念像春天的雨 淅淅瀝瀝 瀝瀝淅淅 濕了地 潮了心 思念像夏日的清泉 汩汩地 擋不住 溢滿了 心花開了 思念像秋天...
    青月清夢閱讀 395評論 4 2
  • 雨稀稀瀝瀝的下著,不是一天,兩天,或許三天,四天,或許一周。 那是江南的黃霉季節(jié),沒有夏季的頃盆大雨酣暢淋漓的像北...
    麻辣姬絲閱讀 767評論 0 4
  • 一 docker的應(yīng)用場景 RD對QA說:“誒?這個程序在我的環(huán)境里是好使的呀?”,QA怒視之!~這個場景真實出現(xiàn)...
    skywalker閱讀 1,820評論 1 9

友情鏈接更多精彩內(nèi)容