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


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

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


     私信【Android有福利领取】

    问题现状

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

    不多废话,我们可以开始今天的图片封装之路了。

    设计思路

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

    • 低耦合,方便将来的代码扩展。至少要支持目前市场上使用率最高的图片框架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 ILoaderStrategy {
    
        void loadImage(LoaderOptions options);
    
        /**
         * 清理内存缓存
         */
        void clearMemoryCache();
    
        /**
         * 清理磁盘缓存
         */
        void clearDiskCache();
    }
    

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

    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()";
            }
        }
    
    }
    

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


    如果有考虑引用该封装的话,可以采用下面的方式:

    //根目录下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);
    

     

  • 相关阅读:
    Phonon
    qt 的mysql的库
    vwmare下安装fedora
    C++标准库
    C#命名空间
    用谷歌Chrome浏览器来当手机模拟器
    Javascript实现ECMAScript 5中的map、reduce和filter函数
    页面变灰实现方案
    jQuery检查元素是否在视口内(屏幕可见区域内)
    兼容浏览器的获取指定元素(elem)的样式属性(name)的方法
  • 原文地址:https://www.cnblogs.com/876013676ch/p/10034829.html
Copyright © 2020-2023  润新知