优雅地实现Android主流图片加载框架封装,可无侵入切换框架

前言

项目开发中,往往会随着需求的改变而切换到其它图片加载框架上去。如果最初代码设计的耦合度太高,那么恭喜你,成功入坑了。至今无法忘却整个项目一行行去复制粘贴被支配的恐惧。:)

2.gif

那么是否存在一种方式 能够一劳永逸地解决这个痛点呢?下面我们来分析一下图片加载框架面对的现状和解决思路。

问题现状

一个优秀的框架一般在代码设计的时候已经封装很不错了,对于开发者而言框架的使用也是很方便,但是为什么说我们往往还要去做这方面的框架封装呢?原因很简单,实际项目开发中,我们不得不面对着日新月异的需求变化,想要在这个变化中最大程度的实现代码的可扩展性和变通性(当然还可以偷懒),不能因为牵一发而动全身,同时要将框架适配到实际项目,框架的再封装设计显得尤为重要。
不多废话,我们可以开始今天的图片封装之路了。

20170610145545.gif

设计思路

图片框架的封装主要需要满足以下三点:

  • 低耦合,方便将来的代码扩展。至少要支持目前市场上使用率最高的图片框架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 ILoaderProxy {

    void loadImage(LoaderOptions options);

    /**
     * 清理内存缓存
     */
    void clearMemoryCache();

    /**
     * 清理磁盘缓存
     */
    void clearDiskCache();
}

提取各个框架通用的View,path/file文件路径,通过LoaderOptions解决大量不同参数传入的问题。这里需要说明的是,LoaderOptions中采用控件View,而不是ImageView,主要考虑到Fresco图片框架采用了DraweeView,这里保留了设计的扩展性。而图片参数类LoaderOptions采用了Builder设计模式:


/**
 * 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; //旋转角度.注意: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()";
        }
    }

}

好了,到了这里,关于图片框架的封装已经全部完成。而且该图片框架的封装已经成功应用到公司项目上,目前反馈良好。如有问题,欢迎交流指教!


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中全局设置
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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,595评论 4 59
  • 思念像春天的雨 淅淅沥沥 沥沥淅淅 湿了地 潮了心 思念像夏日的清泉 汩汩地 挡不住 溢满了 心花开了 思念像秋天...
    青月清梦阅读 307评论 4 2
  • 雨稀稀沥沥的下着,不是一天,两天,或许三天,四天,或许一周。 那是江南的黄霉季节,没有夏季的顷盆大雨酣畅淋漓的像北...
    麻辣姬丝阅读 635评论 0 4
  • 一 docker的应用场景 RD对QA说:“诶?这个程序在我的环境里是好使的呀?”,QA怒视之!~这个场景真实出现...
    skywalker阅读 1,720评论 1 9