Glide用Kotlin应该这样封装(一)

4,340 阅读6分钟

Kotlin应该这样写系列:

SharedPreferences用Kotlin应该这样写

Glide用Kotlin应该这样封装(二)

前言

Glide 是 Google 官方推荐的一款图片加载库,但是大家为了项目后期的维护都会对Glide进行封装使用。最常见的就是通过策略模式简单封装,可以很好地支持切换Glide 、 Picasso 等。让我们回顾下策略模式实现,分析其优劣,再借鉴 Coil 的优点打造一个Glide封装。

策略模式探讨

  • 写一个接口,统一方法名,也可以有多个方法,可以自己根据需求添加

    /** 图片加载策略  接口*/
    public interface BaseImageLoaderStrategy {
        void loadImage(Context context,ImageOptions options);
    }
    
  • 实现具体的加载策略

    /*** 具体的加载策略, Glide 加载框架*/
    public class GlideImageLoader implements BaseImageLoaderStrategy {
        @Override
        public void loadImage(Context context, ImageOptions options) {
            Glide.with(context)
                    .load(options.getUrl())
                    .placeholder(options.getPlaceHolder())
                    .into(options.getImgView());
        }
    }
    
  • ImageOptions Build模式封装

    /**
     * 设置具体的参数,参数过多时候代码整洁
     */
    public class ImageOptions {
    
        private String url; //网络的 url
        private int placeHolder; //当没有成功加载的时候显示的图片
        private ImageView imgView; //ImageView 的实例
    
        private ImageOptions(Builder builder) {
            this.url = builder.url;
            this.placeHolder = builder.placeHolder;
            this.imgView = builder.imgView;
        }
    
        public String getUrl() {
            return url;
        }
    
        public int getPlaceHolder() {
            return placeHolder;
        }
    
        public ImageView getImgView() {
            return imgView;
        }
    
        public static class Builder{
            private String url; 
            private int placeHolder; 
            private ImageView imgView;
     
            public Builder() {
                this.url = "";
                this.placeHolder = R.mipmap.ic_launcher;
                this.imgView = null;
            }
    
            public Builder url(String url) {
                this.url = url;
                return this;
            }
    
            public Builder placeHolder(int placeHolder) {
                this.placeHolder = placeHolder;
                return this;
            }
    
            public Builder imgView(ImageView imgView) {
                this.imgView = imgView;
                return this;
            }
    
            public ImageOptions bulid(){
                return new ImageOptions(this);
            }
        }
    }
    
  • 调度策略

    **
     * 图片加载框架 策略模式
     * 设计为单例模式,并且暴露一个方法,可以设置加载模式,使用哪种图片加载框架。
     */
    public class ImageLoadreUtils {
    
        private static ImageLoadreUtils mInstance;
    
        private BaseImageLoaderStrategy imageLoaderStrategy;
    
        private ImageLoadreUtils() {
            //默认使用 Glide 加载模式
            imageLoaderStrategy = new GlideImageLoader();
        }
    
        public static ImageLoadreUtils getInstance() {
            if (mInstance == null) {
                synchronized (ImageLoadreUtils.class) {
                    if (mInstance == null) {
                        mInstance = new ImageLoadreUtils();
                        return mInstance;
                    }
                }
            }
            return mInstance;
        }
    
        /*** 设置使用的图片加载框架*/
        public void setImageLoaderStrategy(BaseImageLoaderStrategy imageLoaderStrategy){
            this.imageLoaderStrategy = imageLoaderStrategy;
        }
    
        /*** 加载图片*/
        public void loadImage(Context context, ImageLoader imageLoader){
            imageLoaderStrategy.loadImage(context,imageLoader);
        }
    
  • 优缺点分析

    优点:1.可以根据需求替换不同的图片框架加载策略;2.Glide的同一API调用,有需求变化时可以快速变换;3.提升代码阅读和扩展性

    缺点:要根据需求变更或增加策略接口方法,导致策略实现需要不断维护更新

Coil 真漂亮

kotlin 的出现,由于其扩展函数的特真香,新的图片框架coil出现,来看看她的使用

// URL
imageView.load("https://www.example.com/image.jpg")
//多参数
imageView.load("https://www.example.com/image.jpg") {
    crossfade(true)
    placeholder(R.drawable.image)
    transformations(CircleCropTransformation())
}

简洁的API使用让人眼前一亮。那有什么办法让Glide也可以像coil一样呢?

Glide封装

  • 继承AppGlideModule添加OkHttp进度监听器管理等

    //ProgressManager负责网络图片进度管理 具体实现可以百度
    @GlideModule(glideName = "IGlideModule")
    class GlideModule : AppGlideModule() {
        override fun applyOptions(context: Context, builder: GlideBuilder) {
        }
    
        override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
            registry.replace(GlideUrl::class.java, InputStream::class.java, OkHttpUrlLoader.Factory(ProgressManager.okHttpClient))
        }
    
        override fun isManifestParsingEnabled(): Boolean {
            return false
        }
    }
    
  • 封装Glide

    /**Glide封装*/
    object GlideImageLoader {
    
        @JvmStatic
        fun loadImage(options: ImageOptions) {
            Preconditions.checkNotNull(options, "ImageConfigImpl is required")
            val context = options.context
            Preconditions.checkNotNull(context, "Context is required")
            Preconditions.checkNotNull(options.imageView, "ImageView is required")
            val requestsWith = glideRequests(context)
            //根据类型获取
            val glideRequest = when (options.res) {
                is String -> requestsWith.load(options.res as String)
                is Bitmap -> requestsWith.load(options.res as Bitmap)
                is Drawable -> requestsWith.load(options.res as Drawable)
                is Uri -> requestsWith.load(options.res as Uri)
                is URL -> requestsWith.load(options.res as URL)
                is File -> requestsWith.load(options.res as File)
                is Int -> requestsWith.load(options.res as Int)
                is ByteArray -> requestsWith.load(options.res as ByteArray)
                else -> requestsWith.load(options.res)
            }
    
            glideRequest.apply {
                // 占位图、错误图
                ...
                //缓存配置,优先级,缩略图请求
                ...
                //动画、transformation
                into(GlideImageViewTarget(options.imageView, options.res))
            }
    
            options.onProgressListener?.let {
                ProgressManager.addListener(options.res.toString(), options.onProgressListener)
            }
        }
    
        private fun glideRequests(context: Any?): GlideRequests {
            return when (context) {
                is Context -> IGlideModule.with(context)
                is Activity -> IGlideModule.with(context)
                is FragmentActivity -> IGlideModule.with(context)
                is Fragment -> IGlideModule.with(context)
                is android.app.Fragment -> IGlideModule.with(context)
                is View -> IGlideModule.with(context)
                else -> throw NullPointerException("not support")
            }
        }
    
        /*** 清除本地缓存*/
        suspend fun clearDiskCache(context: Context) = withContext(Dispatchers.IO) {
            Glide.get(context).clearDiskCache()
        }
    
        /*** 清除内存缓存*/
        @JvmStatic
        fun clearMemory(context: Context) {
            Glide.get(context).clearMemory();
        }
    
        /*** 取消图片加载*/
        @JvmStatic
        fun clearImage(context: Context, imageView: ImageView?) {
            Glide.get(context).requestManagerRetriever[context].clear(imageView!!)
        }
    
        /*** 预加载*/
        @JvmStatic
        fun preloadImage(context: Any?, url: String?) {
            glideRequests(context).load(url).preload()
        }
    
        /*** 暂停加载*/
        @JvmStatic
        fun pauseRequests(context: Any?) {
            glideRequests(context).pauseRequests()
        }
    
        /**下载*/
        suspend fun downloadImage(context: Context, imgUrl: String?): File? = withContext(Dispatchers.IO) {
            ...
        }
    }
    
    /**
     * 图片加载库的配置,封装原始加载配置属性,进行转换
     */
    class ImageOptions {
        /*** 加载原始资源*/
        var res: Any? = null
    
        /*** 显示容器*/
        var imageView: ImageView? = null
    
        /*** imageView存在的上下文或者fragment\activity*/
        var context: Any? = null
            get() {
                return field ?: imageView
            }
    
        /*** 加载占位图资源ID,如果placeholder是0表示没有占位图*/
        @DrawableRes
        var placeHolderResId = 0
    		.... 省略其动画、错误图等等他属性
        var centerCrop: Boolean = false
        /*** 网络进度监听器*/
        var onProgressListener: OnProgressListener? = null
        /*** 加载监听*/
        var requestListener: OnImageListener? = null
    	  ....省略缓存策略和优先级等等枚举
    }
    

ImageView增加扩展函数

  • 新建ImageExt.kt,增加常用加载方法和一个拥有所有配置参数的方法

    private const val placeHolderImageView = 0
    /**
     * 加载本地图片
     */
    @JvmOverloads
    fun ImageView.loadImage(@RawRes @DrawableRes drawableId: Int, @RawRes @DrawableRes errorId: Int = drawableId) {
        this.loadImage(load = drawableId, placeHolderResId = drawableId, errorResId = errorId)
    }
    
    @JvmOverloads
    fun ImageView.loadImage(url: String?, @RawRes @DrawableRes placeHolder: Int = placeHolderImageView, @RawRes @DrawableRes errorId: Int = placeHolder, loadListener: OnImageListener? = null) {
        this.loadImage(load = url, placeHolderResId = placeHolder, errorResId = errorId,
                requestListener = loadListener)
    }
    
    @JvmOverloads
    fun ImageView.loadProgressImage(url: String?, @RawRes @DrawableRes placeHolder: Int = placeHolderImageView, progressListener: OnProgressListener? = null) {
        this.loadImage(load = url, placeHolderResId = placeHolder, errorResId = placeHolder,
                onProgressListener = progressListener)
    }
    
    @JvmOverloads
    fun ImageView.loadResizeImage(url: String?, width: Int, height: Int, @RawRes @DrawableRes placeHolder: Int = placeHolderImageView) {
        this.loadImage(load = url, placeHolderResId = placeHolder, errorResId = placeHolder,
                size = ImageOptions.OverrideSize(width, height))
    }
    
    @JvmOverloads
    fun ImageView.loadGrayImage(url: String?, @DrawableRes placeHolder: Int = placeHolderImageView) {
        this.loadImage(load = url, placeHolderResId = placeHolder, errorResId = placeHolder, isGray = true)
    }
    
    @JvmOverloads
    fun ImageView.loadBlurImage(url: String?, radius: Int = 25, sampling: Int = 4, @DrawableRes placeHolder: Int = placeHolderImageView) {
        this.loadImage(load = url, placeHolderResId = placeHolder, errorResId = placeHolder,
                isBlur = true, blurRadius = radius, blurSampling = sampling)
    }
    
    @JvmOverloads
    fun ImageView.loadRoundCornerImage(url: String?, radius: Int = 0, type: ImageOptions.CornerType = ImageOptions.CornerType.ALL, @DrawableRes placeHolder: Int = placeHolderImageView) {
        this.loadImage(load = url, placeHolderResId = placeHolder, errorResId = placeHolder,
                isRoundedCorners = radius > 0, roundRadius = radius, cornerType = type)
    }
    
    @JvmOverloads
    fun ImageView.loadCircleImage(url: String?, borderWidth: Int = 0, @ColorInt borderColor: Int = 0, @DrawableRes placeHolder: Int = placeHolderImageView) {
        this.loadImage(load = url, placeHolderResId = placeHolder, errorResId = placeHolder,
                isCircle = true, borderWidth = borderWidth, borderColor = borderColor)
    }
    
    @JvmOverloads
    fun ImageView.loadBorderImage(url: String?, borderWidth: Int = 0, @ColorInt borderColor: Int = 0, @DrawableRes placeHolder: Int = placeHolderImageView) {
        this.loadImage(load = url, placeHolderResId = placeHolder, errorResId = placeHolder,
                borderWidth = borderWidth, borderColor = borderColor)
    }
    
    @JvmOverloads
    fun ImageView.loadImage(load: Any?, with: Any? = this,
            //占位图 错误图
                            @DrawableRes placeHolderResId: Int = placeHolderImageView, placeHolderDrawable: Drawable? = null,
                            @DrawableRes errorResId: Int = placeHolderImageView, errorDrawable: Drawable? = null,
                            @DrawableRes fallbackResId: Int = placeHolderImageView, fallbackDrawable: Drawable? = null,
            //缓存策略等
                            skipMemoryCache: Boolean = false,
                            diskCacheStrategy: ImageOptions.DiskCache = ImageOptions.DiskCache.AUTOMATIC,
            //优先级
                            priority: ImageOptions.LoadPriority = ImageOptions.LoadPriority.NORMAL,
            //缩略图
                            thumbnail: Float = 0f, thumbnailUrl: Any? = null,
                            size: ImageOptions.OverrideSize? = null,
            //gif或者动画
                            isAnim: Boolean = true,
                            isCrossFade: Boolean = false,
                            isCircle: Boolean = false,
                            isGray: Boolean = false,
                            isFitCenter: Boolean = false,
                            centerCrop: Boolean = false,
            //输出图像像素格式
                            format: Bitmap.Config? = null,
            //边框 一组一起
                            borderWidth: Int = 0, borderColor: Int = 0,
            //模糊处理 一组一起使用
                            isBlur: Boolean = false, blurRadius: Int = 25, blurSampling: Int = 4,
            //圆角 一组一起使用
                            isRoundedCorners: Boolean = false, roundRadius: Int = 0, cornerType: ImageOptions.CornerType = ImageOptions.CornerType.ALL,
            //自定义转换器
                            vararg transformation: Transformation<Bitmap>,
            //进度监听,请求回调监听
                            onProgressListener: OnProgressListener? = null, requestListener: OnImageListener? = null) {
        GlideImageLoader.loadImage(ImageOptions().also {
            it.res = load
            it.imageView = this
            it.context = with
            it.placeHolderResId = placeHolderResId
            it.placeHolderDrawable = placeHolderDrawable
            it.errorResId = errorResId
            it.errorDrawable = errorDrawable
            it.fallbackResId = fallbackResId
            it.fallbackDrawable = fallbackDrawable
            it.isCrossFade = isCrossFade
            it.skipMemoryCache = skipMemoryCache
            it.isAnim = isAnim
            it.diskCacheStrategy = diskCacheStrategy
            it.priority = priority
            it.thumbnail = thumbnail
            it.thumbnailUrl = thumbnailUrl
            it.size = size
            it.isCircle = isCircle
            it.isGray = isGray
            it.centerCrop = centerCrop
            it.isFitCenter = isFitCenter
            it.format = format
            it.borderWidth = borderWidth
            it.borderColor = borderColor
            it.isBlur = isBlur
            it.blurRadius = blurRadius
            it.blurSampling = blurSampling
            it.isRoundedCorners = isRoundedCorners
            it.roundRadius = roundRadius
            it.cornerType = cornerType
            it.transformation = transformation
            it.onProgressListener = onProgressListener
            it.requestListener = requestListener
        })
    }
    
  • 调用效果,由于不同需求要用到Glide的api组合,扩展函数提供了参数最多的一个loadImage函数,调用这个函数要用可选参数的方式。示例如下:

    //url
    imageView.loadImage(url, placeHolder = R.color.blue)
    //特效
    iv_3.loadBlurImage(url4)
    iv_4.loadCircleImage(url1)
    iv_5.loadBorderImage(url1, borderWidth = 10, borderColor = Color.RED)
    iv_6.loadGrayImage(url1)
    iv_7.loadRoundCornerImage(url1, radius = 10, type = ImageOptions.CornerType.ALL)
    iv_8.loadResizeImage(url2, width = 400, height = 800)
    //进度条
    iv_0.loadProgressImage(url1, progressListener = object : OnProgressListener {
        override fun onProgress(isComplete: Boolean, percentage: Int, bytesRead: Long, totalBytes: Long) {
            // 跟踪进度
            Log.d("TAG", "onProgress: $percentage")
        }
    })
    //成功失败回调
    iv_2.loadImage(url4, loadListener = object : OnImageListener {
        override fun onSuccess(drawable: Drawable?) {
            Toast.makeText(application, R.string.load_success, Toast.LENGTH_LONG).show()
        }
        override fun onFail(msg: String?) {
            Toast.makeText(application, R.string.load_failed, Toast.LENGTH_LONG).show()
        }
    })
    //其他特殊需求 使用可选参数,loadImage(xxx= Xxxx,yyy = Yyy)这样使用
    iv_9.loadImage(load = R.drawable.test, with = MainActivity@ this, placeHolderResId = R.color.black,
            requestListener = object : OnImageListener {
                override fun onSuccess(drawable: Drawable?) {
                }
                override fun onFail(msg: String?) {
                }
            },
            onProgressListener = object : OnProgressListener {
                override fun onProgress(isComplete: Boolean, percentage: Int, bytesRead: Long, totalBytes: Long) {
                }
            }, transformation = *arrayOf(GrayscaleTransformation(),CircleWithBorderTransformation(borderWidth = 0, borderColor = 0)))
    

总结

  • 最后要方便的使用到项目中那就打包发布jitpack仓库,项目开源地址和文档

    forJrking/ImageExt: 基于Glide封装ImageView加载图片资源的扩展函数集 (github.com)

  • 由于使用到基于okhttp的进度管理所以使用了 Glide 的@GlideModule配置方法,这样可能会和你项目自定义配置有冲突,目前只能拉代码自己修改,然后依赖Module方式了。如有更好方式联系我改进。

  • Android图片加载库常见的只有几种,其他库可以自行参考实现。Kotlin真香!!!