|
1 | 1 | # ImageExt 参考Coil对Glide封装实现 |
2 | 2 |
|
3 | | -> 主要为ImageView添加扩展函数来简化常见图片加载api [](https://jitpack.io/#forJrking/ImageExt) |
| 3 | +## 前言 |
4 | 4 |
|
5 | | - |
| 5 | +`glide` 是 `Google 官方`推荐的一款图片加载库,Coil是号称[Kotlin-first的Android图片加载库](https://juejin.cn/post/6844903913007611917) ,融合了kotlin特性、android最主流的技术和趋势,本篇我们主要分享如何用`kotlin`把`glide`封装的使用起来像`coil`一样。 |
6 | 6 |
|
7 | | -## 使用方法 |
| 7 | +## 集成和API预览 |
8 | 8 |
|
9 | | -```groovy |
10 | | -allprojects { |
11 | | - repositories { |
12 | | - ... |
13 | | - maven { url 'https://www.jitpack.io' } |
14 | | - } |
| 9 | +1. ```groovy |
| 10 | + allprojects { |
| 11 | + repositories { |
| 12 | + maven { url 'https://www.jitpack.io' } |
| 13 | + } |
| 14 | + } |
| 15 | + ``` |
| 16 | + |
| 17 | +2. ```groovy |
| 18 | + dependencies { |
| 19 | + implementation 'com.github.forJrking:ImageExt:0.0.2' |
| 20 | + } |
| 21 | + ``` |
| 22 | + |
| 23 | +3. ```kotlin |
| 24 | + //配置全局占位图 错误图 非必须 |
| 25 | + ImageOptions.DrawableOptions.setDefault { |
| 26 | + placeHolderResId = R.drawable.ic_launcher_background |
| 27 | + errorResId = R.color.gray |
| 28 | + } |
| 29 | + // URL |
| 30 | + imageView.load("https://www.example.com/image.jpg") |
| 31 | + // Resource |
| 32 | + imageView.load(R.drawable.image) |
| 33 | + //回调和进度监听 |
| 34 | + imageView.load("https://www.example.com/image.jpg") { |
| 35 | + placeHolderResId = R.drawable.placeholder |
| 36 | + transformation = arrayOf(GrayscaleTransformation()) |
| 37 | + progressListener { isComplete, percentage, bytesRead, totalBytes -> |
| 38 | + //加载进度 |
| 39 | + } |
| 40 | + requestListener { |
| 41 | + onSuccess { |
| 42 | + } |
| 43 | + onFail { |
| 44 | + } |
| 45 | + } |
| 46 | + } |
| 47 | + ``` |
| 48 | + |
| 49 | +4. 其他扩展函数和效果 |
| 50 | + |
| 51 | + ```kotlin |
| 52 | + ImageView.loadImage(...) |
| 53 | + ImageView.loadProgressImage(...) |
| 54 | + ImageView.loadResizeImage(...) |
| 55 | + ImageView.loadGrayImage(...) |
| 56 | + ImageView.loadBlurImage(...) |
| 57 | + ImageView.loadBlurImage(...) |
| 58 | + ImageView.loadRoundCornerImage(...) |
| 59 | + ImageView.loadCircleImage(...) |
| 60 | + ImageView.loadBorderImage(...) |
| 61 | + ``` |
| 62 | + |
| 63 | +  |
| 64 | + |
| 65 | +## 封装实现 |
| 66 | + |
| 67 | +首先看coil的调用方式采用了`kotlin`扩展函数形式,给ImageView增加了一个`load(url)`函数,然后其他占位图等配置通过DSL方式去设置。DSL如何学习和使用后面单独说。 |
| 68 | + |
| 69 | +第一步封装如下一个函数: |
| 70 | + |
| 71 | +```kotlin |
| 72 | +/**模仿 coil DSL写法**/ |
| 73 | +fun ImageView.load(load: Any?, options: (ImageOptions.() -> Unit)? = null) { |
| 74 | + ImageLoader.loadImage(ImageOptions(load).also(options)) |
15 | 75 | } |
16 | 76 | ``` |
17 | 77 |
|
18 | | -```groovy |
19 | | -dependencies { |
20 | | - implementation 'com.github.forJrking:ImageExt:0.0.3' |
| 78 | +第二步封装配置类: |
| 79 | + |
| 80 | +```kotlin |
| 81 | +/** |
| 82 | + * 图片加载库的配置,封装原始加载配置属性,进行转换 |
| 83 | + */ |
| 84 | +class ImageOptions { |
| 85 | + /*** 加载原始资源*/ |
| 86 | + var res: Any? = null |
| 87 | + /*** 显示容器*/ |
| 88 | + var imageView: ImageView? = null |
| 89 | + /*** imageView存在的上下文或者fragment\activity*/ |
| 90 | + var context: Any? = null |
| 91 | + get() { |
| 92 | + return field ?: imageView |
| 93 | + } |
| 94 | + /*** 加载占位图资源ID,如果placeholder是0表示没有占位图*/ |
| 95 | + @DrawableRes |
| 96 | + var placeHolderResId = 0 |
| 97 | + .... 省略其动画、错误图等等他属性 |
| 98 | + var centerCrop: Boolean = false |
| 99 | + /*** 网络进度监听器*/ |
| 100 | + var onProgressListener: OnProgressListener? = null |
| 101 | + /*** 加载监听*/ |
| 102 | + var requestListener: OnImageListener? = null |
| 103 | + ....省略缓存策略和优先级等等枚举 |
21 | 104 | } |
22 | 105 | ``` |
| 106 | + |
| 107 | +第三步 策略实现,由于要使用okhttp拦截器做进度监听,通过注解方式配置glide的网络下载器。 |
| 108 | + |
23 | 109 | ```kotlin |
24 | | -//配置全局占位图 错误图 非必须 |
25 | | -ImageOptions.DrawableOptions.setDefault { |
26 | | - placeHolderResId = R.drawable.ic_launcher_background |
27 | | - errorResId = R.color.gray |
28 | | -} |
29 | | -//加载url 单独设置占位图 |
30 | | -iv_2.loadImage(url, placeHolder = R.color.blue) |
31 | | -//模糊 |
32 | | -iv_3.loadBlurImage(url) |
33 | | -//圆形 |
34 | | -iv_4.loadCircleImage(url) |
35 | | -//边框 |
36 | | -iv_5.loadBorderImage(url, borderWidth = 10, borderColor = Color.RED) |
37 | | -//黑白 |
38 | | -iv_6.loadGrayImage(url) |
39 | | -//圆角 |
40 | | -iv_7.loadRoundCornerImage(url, radius = 10, type = ImageOptions.CornerType.ALL) |
41 | | -//resize |
42 | | -iv_8.loadResizeImage(url, width = 400, height = 800) |
43 | | -//支持不需要全局占位图 单独分类管理 |
44 | | -val homeOptions = ImageOptions.DrawableOptions(placeHolderResId = R.drawable.home_holder,errorResId = R.drawable.error_holder) |
45 | | -iv_8.load(url){ |
46 | | - drawableOptions = homeOptions |
47 | | -} |
48 | | -//监听回调结果 |
49 | | -iv_9.loadImage(url4, requestListener = { |
50 | | - onSuccess { |
51 | | - Toast.makeText(application, R.string.load_success, Toast.LENGTH_LONG).show() |
52 | | - } |
53 | | - onFail { |
54 | | - Toast.makeText(application, R.string.load_failed, Toast.LENGTH_SHORT).show() |
55 | | - } |
56 | | -}) |
57 | | -//终极扩展 参数非常多必须使用可选参数方式调用 |
58 | | -iv_8.load(url1) { |
59 | | - placeHolderResId = R.color.black |
60 | | - transformation = arrayOf(GrayscaleTransformation()) |
61 | | - progressListener { isComplete, percentage, bytesRead, totalBytes -> |
62 | | - //加载进度 |
63 | | - } |
64 | | - requestListener { |
65 | | - onSuccess { |
| 110 | +/**Glide策略封装*/ |
| 111 | +object ImageLoader { |
| 112 | + fun loadImage(options: ImageOptions) { |
| 113 | + Preconditions.checkNotNull(options, "ImageConfigImpl is required") |
| 114 | + val context = options.context |
| 115 | + Preconditions.checkNotNull(context, "Context is required") |
| 116 | + Preconditions.checkNotNull(options.imageView, "ImageView is required") |
| 117 | + val requestsWith = glideRequests(context) |
| 118 | + //根据类型获取 |
| 119 | + val glideRequest = when (options.res) { |
| 120 | + is String -> requestsWith.load(options.res as String) |
| 121 | + is Bitmap -> requestsWith.load(options.res as Bitmap) |
| 122 | + is Drawable -> requestsWith.load(options.res as Drawable) |
| 123 | + is Uri -> requestsWith.load(options.res as Uri) |
| 124 | + is URL -> requestsWith.load(options.res as URL) |
| 125 | + is File -> requestsWith.load(options.res as File) |
| 126 | + is Int -> requestsWith.load(options.res as Int) |
| 127 | + is ByteArray -> requestsWith.load(options.res as ByteArray) |
| 128 | + else -> requestsWith.load(options.res) |
| 129 | + } |
| 130 | + |
| 131 | + glideRequest.apply { |
| 132 | + // 占位图、错误图 |
| 133 | + ... |
| 134 | + //缓存配置,优先级,缩略图请求 |
| 135 | + ... |
| 136 | + //动画、transformation |
| 137 | + into(GlideImageViewTarget(options.imageView, options.res)) |
| 138 | + } |
| 139 | + |
| 140 | + options.onProgressListener?.let { |
| 141 | + ProgressManager.addListener(options.res.toString(), options.onProgressListener) |
66 | 142 | } |
67 | | - onFail { |
| 143 | + } |
| 144 | + private fun glideRequests(context: Any?): GlideRequests { |
| 145 | + return when (context) { |
| 146 | + is Context -> IGlideModule.with(context) |
| 147 | + is Activity -> IGlideModule.with(context) |
| 148 | + is FragmentActivity -> IGlideModule.with(context) |
| 149 | + is Fragment -> IGlideModule.with(context) |
| 150 | + is android.app.Fragment -> IGlideModule.with(context) |
| 151 | + is View -> IGlideModule.with(context) |
| 152 | + else -> throw NullPointerException("not support") |
68 | 153 | } |
69 | 154 | } |
70 | 155 | } |
71 | | -//超长扩展函数 选用建议用上面DSL方式 |
72 | | -iv_9.loadImage(load = R.drawable.test, with = MainActivity@ this, |
73 | | - placeHolderResId = R.color.black,errorResId = R.color.blue,isAnim = false, |
74 | | - requestListener = object : OnImageListener { |
75 | | - ... |
76 | | - }, |
77 | | - onProgressListener = object : OnProgressListener { |
78 | | - ... |
79 | | - }, transformation = *arrayOf(GrayscaleTransformation()) |
80 | | -) |
81 | 156 | ``` |
82 | 157 |
|
83 | | -## 可选扩展函数和Api介绍 |
| 158 | +## Kotlin DSL |
| 159 | + |
| 160 | +DSL的编写可以用下面代码简单理解和记忆(主要参考:[如何让你的回调更具Kotlin风味 (juejin.cn)](https://juejin.cn/post/6844903769436585991)) |
| 161 | + |
| 162 | +```kotlin |
| 163 | +class DSLTest{ |
| 164 | + //普通变量 |
| 165 | + var str:String? =null |
| 166 | + //函数变量 |
| 167 | + var onSuccess: ((String?) -> Unit)? = null |
| 168 | + //调用函数变量 |
| 169 | + fun onSuccessDo() { |
| 170 | + ... |
| 171 | + onSuccess.invoke("success $str") |
| 172 | + } |
| 173 | +} |
| 174 | +//定义调用的函数 |
| 175 | +load(dslPar:(DSLTest.() -> Unit)? = null){ |
| 176 | + DSLTest().also(dslPar) |
| 177 | +} |
| 178 | +//使用 |
| 179 | +load{ |
| 180 | + str = "ACC" |
| 181 | + onSuccess{ |
| 182 | + //TODO |
| 183 | + } |
| 184 | +} |
| 185 | +``` |
| 186 | + |
| 187 | +## 与传统策略模式封装对比 |
| 188 | + |
| 189 | +- 定义策略模式基础接口 |
| 190 | + |
| 191 | +```kotlin |
| 192 | +/** 图片加载策略 接口*/ |
| 193 | +public interface BaseImageLoaderStrategy { |
| 194 | + void loadImage(load Any,ImageOptions options); |
| 195 | +} |
| 196 | +``` |
| 197 | + |
| 198 | +- 实现策略接口 |
| 199 | + |
| 200 | +```kotlin |
| 201 | +/*** 具体的加载策略, Glide 加载框架*/ |
| 202 | +public class GlideImageLoader implements BaseImageLoaderStrategy { |
| 203 | + @Override |
| 204 | + public void loadImage(Context context, ImageOptions options) { |
| 205 | + Glide.with(context).apply(...).into(options.getImgView()); |
| 206 | + } |
| 207 | +} |
| 208 | +``` |
| 209 | + |
| 210 | +- 策略调度器调用 |
| 211 | + |
| 212 | +```kotlin |
| 213 | +ImageLoader.loadImage(ImageOptions(imageView).apply{ |
| 214 | + ..... |
| 215 | +}) |
| 216 | +``` |
| 217 | + |
| 218 | +策略模式一般会封装很多接口满足日常需求,由于kotlin特性我们封装一个超长参数的方法,然后使用可选参数的方式调用,但是java就无能为力了,只能对可选参数赋值null。 |
84 | 219 |
|
85 | 220 | ```kotlin |
86 | | -ImageView.loadImage(...) |
87 | | -ImageView.loadProgressImage(...) |
88 | | -ImageView.loadResizeImage(...) |
89 | | -ImageView.loadGrayImage(...) |
90 | | -ImageView.loadBlurImage(...) |
91 | | -ImageView.loadBlurImage(...) |
92 | | -ImageView.loadRoundCornerImage(...) |
93 | | -ImageView.loadCircleImage(...) |
94 | | -ImageView.loadBorderImage(...) |
95 | | -ImageView.load(load: Any?, options: ImageOptions.() -> Unit)//DSL |
| 221 | +//可选参数示例 |
| 222 | +load(url:String,isCirle:Boolean = false, width:Int=0, height:Int = 0){ |
| 223 | + ..... |
| 224 | +} |
| 225 | +//用 参数名 = 值 使用可选参数 |
| 226 | +iv_8.load(url2, height = 800) |
96 | 227 | ``` |
97 | 228 |
|
| 229 | +策略模式封装写法和扩展函数+DSL写法对比: |
| 230 | + |
| 231 | +- 统一的接口封装,都具有可扩展性 |
| 232 | +- 都可以用kotlin特性不用定义大量接口 |
| 233 | +- 扩展函数更加方便简洁 |
| 234 | +- DSL的写法让代码更加易懂,更具kotlin风格 |
| 235 | +- 多方法接口回调,可以只选择个别方法 |
| 236 | + |
| 237 | +## 总结 |
| 238 | + |
| 239 | +- 最后要方便的使用到项目中那就打包发布jitpack仓库,项目开源地址和文档 |
| 240 | + |
| 241 | +[forJrking/ImageExt: 基于Glide封装ImageView加载图片资源的扩展函数集 (github.com)](https://github.com/forJrking/ImageExt) |
| 242 | + |
| 243 | +- 由于使用到基于okhttp的下载进度管理所以使用了 glide 的@GlideModule配置方法,这样可能会和你项目自定义配置有冲突,目前只能拉代码自己修改,然后依赖Module方式了。如有更好方式联系我改进。 |
| 244 | + |
| 245 | +- Android图片加载库常见的只有几种,其他库可以自行参考实现。Kotlin真香!!! |
| 246 | + |
| 247 | + |
| 248 | +## Api介绍 |
| 249 | + |
| 250 | + |
98 | 251 | | `load: Any?` | 加载资源 | |
99 | 252 | | ------------------------------------------------------------ | ------------------------------------------------------------ | |
100 | 253 | | `with: Any?` | Glide.with( )参数,默认用ImageView | |
@@ -123,25 +276,23 @@ ImageView.load(load: Any?, options: ImageOptions.() -> Unit)//DSL |
123 | 276 | | `requestListener: OnImageListener?` | 加载结果监听,成功和失败 | |
124 | 277 |
|
125 | 278 |
|
126 | | - |
127 | 279 | ## CircleProgressView 仿微博图片加载 |
128 | | - |
129 | 280 | 就是原封不动来自[GlideImageView](https://github.com/sunfusheng/GlideImageView) ,在布局中加入即可,有三种样式可供选择。 |
130 | 281 | ```xml |
131 | 282 | <CircleProgressView |
132 | | - android:id="@+id/progressView" |
133 | | - android:layout_width="50dp" |
134 | | - android:layout_height="50dp" |
135 | | - android:layout_centerInParent="true" |
136 | | - android:layout_margin="10dp" |
137 | | - android:progress="0" |
138 | | - android:visibility="gone" |
139 | | - app:cpv_progressNormalColor="@color/transparent10" |
140 | | - app:cpv_progressReachColor="@color/transparent90_white" |
141 | | - app:cpv_progressStyle="FillInnerArc" |
142 | | - app:cpv_progressTextColor="@color/red" |
143 | | - app:cpv_progressTextSize="13sp" |
144 | | - app:cpv_progressTextVisible="false" /> |
| 283 | + android:id="@+id/progressView" |
| 284 | + android:layout_width="50dp" |
| 285 | + android:layout_height="50dp" |
| 286 | + android:layout_centerInParent="true" |
| 287 | + android:layout_margin="10dp" |
| 288 | + android:progress="0" |
| 289 | + android:visibility="gone" |
| 290 | + app:cpv_progressNormalColor="@color/transparent10" |
| 291 | + app:cpv_progressReachColor="@color/transparent90_white" |
| 292 | + app:cpv_progressStyle="FillInnerArc" |
| 293 | + app:cpv_progressTextColor="@color/red" |
| 294 | + app:cpv_progressTextSize="13sp" |
| 295 | + app:cpv_progressTextVisible="false" /> |
145 | 296 | ``` |
146 | 297 | ## SelectImageView 仿微信图片点击响应 |
147 | 298 | 一个点击可以变为半透明 |
|
0 commit comments