Android 新一代图片加载库 - Coil

7,506 阅读3分钟

Coil 是 Android 的新一代图片加载库,它的全名叫做 Coroutine Image Loader,即协程图片加载器,用于显示网络或本地图像资源。

特点

  • 快速:执行了多项优化,包括内存和磁盘缓存,图像降采样,自动暂停/取消请求等。
  • 轻量级:依赖于 Kotlin,协程和 Okio,并与谷歌的 R8 等代码缩减器无缝协作。
  • 易于使用:API 利用 Kotlin 的语言特性来实现简洁性和最小化的样板代码。
  • 现代化:以 Kotlin 为首要语言,并与协程,Okio,Ktor 和 OkHttp 等现代库实现互操作。

加载图片

先引入依赖

implementation(libs.coil)

最简单的加载方法就是使用这个扩展函数了

inline fun ImageView.load(
    data: Any?,
    imageLoader: ImageLoader = context.imageLoader,
    builder: ImageRequest.Builder.() -> Unit = {}
): Disposable {
    val request = ImageRequest.Builder(context)
        .data(data)
        .target(this)
        .apply(builder)
        .build()
    return imageLoader.enqueue(request)
}

使用扩展函数来加载本地或网络中的图片

// 加载网络图片
binding.imageView.load("https://img2.huashi6.com/images/resource/2020/07/12/h82924904p0.jpg")
// 加载资源图片
binding.imageView.load(R.drawable.girl)
// 加载文件中的图片
val file = File(requireContext().getExternalFilesDir(null), "saved_image.jpg")
binding.imageView.load(file.absolutePath)

支持设置占位图,裁剪变换,生命周期关联等

binding.imageView.load("https://img2.huashi6.com/images/resource/2020/07/12/h82924904p0.jpg") {
    crossfade(true) //渐进渐出
    crossfade(1000) //渐进渐出时间
    placeholder(R.mipmap.sym_def_app_icon) //加载占位图
    error(R.mipmap.sym_def_app_icon)  //加载失败占位图
    allowHardware(true) //硬件加速
    allowRgb565(true) //支持565格式
    lifecycle(lifecycle) //生命周期关联
    transformations(CircleCropTransformation()) //圆形裁剪变换
}

变为圆角矩形

binding.imageView.load("https://img2.huashi6.com/images/resource/2020/07/12/h82924904p0.jpg") {
    lifecycle(lifecycle)
    transformations(RoundedCornersTransformation(20f))
}

可以创建自定义的图片加载器,为其添加一些日志拦截器等。

class LoggingInterceptor : Interceptor {

    companion object {
        private const val TAG = "LoggingInterceptor"
    }

    override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
        val url = chain.request.data.toString()
        val width = chain.size.width.toString()
        val height = chain.size.height.toString()
        Log.i(TAG, "url: $url, width: $width, height: $height")
        return chain.proceed(chain.request)
    }
}
class MyApplication : Application(), ImageLoaderFactory {

    override fun newImageLoader() =
        ImageLoader.Builder(this.applicationContext).components { add(LoggingInterceptor()) }
            .build()
}

替换 Okhttp 实例

val okHttpClient = OkHttpClient.Builder()
    .retryOnConnectionFailure(true)
    .connectTimeout(30, TimeUnit.SECONDS)
    .writeTimeout(20, TimeUnit.SECONDS)
    .readTimeout(20, TimeUnit.SECONDS)
    .build()
val imageLoader = ImageLoader.Builder(requireContext()).okHttpClient {
    okHttpClient
}.build()
Coil.setImageLoader(imageLoader)
binding.imageView.load("https://img2.huashi6.com/images/resource/2020/07/12/h82924904p0.jpg")

加载 gif

添加依赖

implementation(libs.coil.gif)

按照官方的做法,设置 ImageLoader。

val imageLoader = ImageLoader.Builder(requireContext())
    .components {
        if (SDK_INT >= 28) {
            add(ImageDecoderDecoder.Factory())
        } else {
            add(GifDecoder.Factory())
        }
    }.build()
Coil.setImageLoader(imageLoader)
binding.imageView.load(GIF_URL)

下载监听

可以监听下载过程

binding.imageView.load(IMAGE_URL) {
    listener(
        onStart = {
            Log.i(TAG, "onStart")
        },
        onError = { request, throwable ->
            Log.i(TAG, "onError")
        },
        onSuccess = { request, result ->
            Log.i(TAG, "onSuccess")
        },
        onCancel = { request ->
            Log.i(TAG, "onCancel")
        }
    )
}

取消下载

val disposable = binding.imageView.load(IMAGE_URL)
disposable.dispose()

对 Jetpack Compose 的支持

引入依赖:

implementation(libs.coil.compose)

使用 AsyncImage

@Composable
@NonRestartableComposable
fun AsyncImage(
    model: Any?,
    contentDescription: String?,
    modifier: Modifier = Modifier,
    transform: (State) -> State = DefaultTransform,
    onState: ((State) -> Unit)? = null,
    alignment: Alignment = Alignment.Center,
    contentScale: ContentScale = ContentScale.Fit,
    alpha: Float = DefaultAlpha,
    colorFilter: ColorFilter? = null,
    filterQuality: FilterQuality = DefaultFilterQuality,
    clipToBounds: Boolean = true,
    modelEqualityDelegate: EqualityDelegate = DefaultModelEqualityDelegate,
)

比如显示一张网络图片,就可以这样干。

@Composable
fun DisplayPicture() {
    AsyncImage(
        model = "https://img2.huashi6.com/images/resource/2020/07/12/h82924904p0.jpg",
        contentDescription = null
    )
}

支持设置占位图,过程监听,裁剪等

@Composable
fun DisplayPicture() {
    AsyncImage(
        modifier = Modifier
            .clip(CircleShape)
            .size(200.dp),
        onSuccess = {
            Log.i(TAG, "onSuccess")
        },
        onError = {
            Log.i(TAG, "onError")
        },
        onLoading = {
            Log.i(TAG, "onLoading")
        },
        model = ImageRequest.Builder(LocalContext.current)
            .data("https://img2.huashi6.com/images/resource/2020/07/12/h82924904p0.jpg")
            .crossfade(true)
            .placeholder(R.drawable.default_image)
            .error(R.drawable.default_image)
            .build(),
        contentScale = ContentScale.Crop,
        contentDescription = null
    )
}

这里介绍一下这个 ContentScale,它是用来指定图片如何适应其容器大小的,有以下几个值:

  • ContentScale.FillBounds:图片会被拉伸或压缩以完全填充其容器的宽度和高度,这可能会导致图片的宽高比失真。
  • ContentScale.Fit:图片会保持其原始宽高比,并尽可能大地缩放以适应容器,同时确保图片的任一边都不会超出容器的边界,这可能会导致容器的某些部分未被图片覆盖。
  • ContentScale.Crop:图片会被裁剪以完全覆盖其容器的宽度和高度,同时保持图片的宽高比,这通常用于需要确保整个容器都被图片覆盖的场景,但可能会丢失图片的一部分内容。
  • ContentScale.FillWidth:图片会保持其原始宽高比,并调整其高度以完全填充容器的宽度,这可能会导致图片的高度超出容器的高度,从而被裁剪或需要额外的布局处理。
  • ContentScale.FillHeight:图片会保持其原始宽高比,并调整其宽度以完全填充容器的高度,这可能会导致图片的宽度超出容器的宽度,从而需要相应的处理。
  • ContentScale.Inside:图片会保持其原始宽高比,并缩放以确保完全位于容器内部,同时其任一边都不会超出容器的边界。
  • ContentScale.None:图片将以其原始尺寸显示,不会进行任何缩放或裁剪。