Compose 图片加载新姿势 — Coil 精通

书接上回,上一篇文章把 Coil 的基础用法铺陈清楚,就像降龙十八掌先把稳当劲儿练到位 ------ 毕竟技术学习最怕夹生饭。

这一回,我们就顺着这股稳当劲儿往前迈一步,专门拆解 Coil 的高级玩法。

ImageRequest

如果我们不满足于普通的在线加载图片,可以使用 ImageRequest 自定义加载。

在之前的文章中,简单的加载是:

Kotlin 复制代码
AsyncImage(
    modifier = Modifier.size(400.dp).background(Color.Black),
    model = PIC_URL,
    contentScale = ContentScale.Crop,
    contentDescription = "ballon"
)

如果换成 ImageRequest,则是:

Kotlin 复制代码
AsyncImage(
    modifier = Modifier.size(400.dp).background(Color.Black),
    model = ImageRequest
        .Builder(LocalContext.current)
        .data(PIC_URL)
        .build(),
    contentScale = ContentScale.Crop,
    contentDescription = "ballon"
)

查看 ImageRequest 的文档,会发现有很多接口,我们不会一一讲述,这里我们只讲解几个比较重要的接口。

渐入

ImageRequest 支持图片的渐入特效:

Kotlin 复制代码
AsyncImage(
    modifier = Modifier.size(400.dp).background(Color.Black),
    model = ImageRequest
        .Builder(LocalContext.current)
        .data(PIC_URL)
        .crossfade(600)
        .build(),
    contentScale = ContentScale.Crop,
    contentDescription = "ballon"
)

使用 crossfade 函数来时间渐变,该函数的参数是渐变时间------这里我们给了 600 毫秒。

你也可以直接使用 crossfade(true) 来渐入,该方法默认 200 毫秒的渐变时间。

优化内存

通常我们不会直接加载原图,毕竟太大了。ImageRequest 支持设定图片的显示像素大小:

ini 复制代码
AsyncImage(
    modifier = Modifier.size(400.dp).background(Color.Black),
    model = ImageRequest
        .Builder(LocalContext.current)
        .data(PIC_URL)
        .size(200)
        .crossfade(true)
        .build(),
    contentScale = ContentScale.Crop,
    contentDescription = "ballon"
)

使用 size 设定图片的像素大小,为了对比,这里我们只给了 200 像素:

不难看出,已经非常模糊了,证明尺寸起作用了。

ImageLoader

ImageLoader 是执行 ImageRequest 的服务对象。它们负责处理缓存、数据获取、图像解码、请求管理、内存管理等工作。

我们可以在应用程序中创建一个单一的 ImageLoader 并全局共享使用。

Kotlin 复制代码
SingletonImageLoader.setSafe {
    ImageLoader.Builder(context)
        .build()
}

我们只需要执行上述代码,就可以设置全局的 ImageLoader

通常在 Android 中的做法是使用 Application 进行初始化,这里我们展示一下使用 startup 库进行初始设置:

Kotlin 复制代码
class CoilInitializer : Initializer<SingletonImageLoader> {
    override fun create(context: Context): SingletonImageLoader {
        SingletonImageLoader.setSafe {
            ImageLoader.Builder(context)
                .logger(DebugLogger()) // 设置一个日志,可选项
                .build()
        }
        return SingletonImageLoader
    }

    override fun dependencies(): MutableList<Class<out Initializer<*>>> {
        return mutableListOf()
    }
}

别忘了在 Manifest 中注册:

xml 复制代码
<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    
    <meta-data
        android:name="com.momentum.ui.core.startup.CoilInitializer"
        android:value="androidx.startup" />
</provider>

我们可以通过 ImageLoader 定制内存缓存以及磁盘缓存:

Kotlin 复制代码
ImageLoader.Builder(context)
    .memoryCache {
        MemoryCache.Builder()
            .maxSizeBytes(20 * 1024 * 1024) // 20MB 的内存缓存
            .build()
    }
    .diskCache {
        DiskCache.Builder()
            .directory(context.cacheDir.toOkioPath()) // 定制磁盘缓存的路径
            .maxSizeBytes(200 * 1024 * 1024) // 200MB 的磁盘缓存
            .build()
    }
    .logger(DebugLogger())
    .build()

如果你有清除缓存的需求,Coil 依然满足你!

Kotlin 复制代码
SingletonImageLoader.get(context).memoryCache?.clear() // 清除内存缓存
SingletonImageLoader.get(context).diskCache?.clear() // 清除磁盘缓存

rememberAsyncImagePainter

Coil 内部,AsyncImage 使用 rememberAsyncImagePainter 来加载图像资源。

如果你需要的是 Painter 而不是一个可组合函数,那么可以使用 rememberAsyncImagePainter 来加载图像:

Kotlin 复制代码
val painter = rememberAsyncImagePainter(
    model = ImageRequest.Builder(LocalPlatformContext.current)
        .data(PIC_URL)
        .build(),
)

Image(
    modifier = Modifier.size(400.dp).background(Color.Black),
    painter = painter,
    contentScale = ContentScale.Crop,
    contentDescription = "ballon",
)

这段代码和使用普通的 AsyncImage 的效果是一样的。

函数 rememberAsyncImagePainter 的第一个参数,就是提供一个 ImageRequestImageRequest 的用法上面已经讲述过了。

如果你只想获得一个 Painter,或者想提前缓存这张图片,rememberAsyncImagePainter 就非常适合。

当然,如果只是这样,rememberAsyncImagePainter 意义或许不大,rememberAsyncImagePainter 真正厉害的地方是构建更加复杂页面逻辑:

Kotlin 复制代码
val painter = rememberAsyncImagePainter(
    model = ImageRequest.Builder(LocalPlatformContext.current).data(PIC_URL).build(),
)

val state by painter.state.collectAsState() // 获取状态

when (state) {

    AsyncImagePainter.State.Empty -> {} // empty 不管

    is AsyncImagePainter.State.Error -> { // 发生错误时展示一张 200dp 大小的图片
        Box(
            modifier = Modifier
                .size(400.dp)
        ) {
            Image(painter = painterResource(R.drawable.icn_failed), contentDescription = null, Modifier.size(200.dp))
        }
    }
    is AsyncImagePainter.State.Loading -> { // 加载过程中展示文字
        Box(
            modifier = Modifier
                .size(400.dp)
        ) {
            Text("加载中,请稍等", modifier = Modifier.align(Alignment.Center), fontSize = 48.sp)
        }
    }
    is AsyncImagePainter.State.Success -> { // 成功就展示图片
        Image(
            modifier = Modifier
                .size(400.dp),
            painter = painter,
            contentScale = ContentScale.Crop,
            contentDescription = "ballon",
        )
    }
}

rememberAsyncImagePainter 会返回一个 painter 对象,通过 painter.state 可以获取当前的加载状态。

如果一切顺利会是下面这个效果:

当然,如果请求有问题,会显示错误 UI:

如果你对错误处理特别在意,那么就需要特别关注 painter.state

当发生错误的时候,painter.state 会返回 AsyncImagePainter.State.Error,它的实现是这样的:

Kotlin 复制代码
data class Error(
    override val painter: Painter?,
    val result: ErrorResult, // 错误信息
) : State

result 表示当前的错误信息,深挖 ErrorResult

Kotlin 复制代码
class ErrorResult(
    /**
     * The error drawable.
     */
    override val image: Image?,

    /**
     * The request that was executed to create this result.
     */
    override val request: ImageRequest,

    /**
     * The error that failed the request.
     */
    val throwable: Throwable,
)

我们可以通过 throwable 来处理错误信息,这里稍微举一下例子:

Kotlin 复制代码
when (val cur = state) {

    //...

    is AsyncImagePainter.State.Error -> {

        Box(
            modifier = Modifier.size(400.dp).background(Color.LightGray)
        ) {
            val text by remember {
                derivedStateOf {
                    when (val error = cur.result.throwable) {
                    
                        is UnknownHostException -> { // 未知主机错误,可能是 url 不对,也有可能是解析问题
                            "未知主机"
                        }

                        is HttpException -> { // Http 错误
                            "Http 错误:${error.response.code}"
                        }

                        else -> { // 其他错误
                            "未知错误"
                        }
                    }

                }
            }

            Text(text, modifier = Modifier.align(Alignment.Center), fontSize = 48.sp)
        }
    }
    
    //...
}

我调试了几种错误显示效果,仅供参考:

SubcomposeAsyncImage

SubcomposeAsyncImageAsyncImage 的一个变体,它使用子组合为 AsyncImagePainter 的状态提供插槽,而不是使用 Painters

说人话就是 SubcomposeAsyncImage 通过 UI 去定义占位图,而 AsyncImag 只能使用图片作为占位图。

从灵活性上来讲,SubcomposeAsyncImage 会更加灵活,让你能够自定义很多效果。

不过先前已经掌握了 rememberAsyncImagePainter,现在再看 SubcomposeAsyncImage,有点索然无味。

Kotlin 复制代码
SubcomposeAsyncImage(
    modifier = Modifier
                .size(400.dp),
    model = PIC_URL,
    contentDescription = "ballon"
) {
    val state by painter.state.collectAsState()
    when(state) {
        AsyncImagePainter.State.Empty -> TODO()
        is AsyncImagePainter.State.Error -> TODO()
        is AsyncImagePainter.State.Loading -> TODO()
        is AsyncImagePainter.State.Success -> TODO()
    }
}

熟悉吗?

如果你不想每次都处理 state,可以使用 SubcomposeAsyncImage 的另一个重载版本:

Kotlin 复制代码
SubcomposeAsyncImage(
    modifier = Modifier.size(400.dp).drawBackground(Color.Black),
    model = PIC_URL,
    contentDescription = "ballon",
    contentScale = ContentScale.Crop,
    loading = {

        Box(Modifier.fillMaxSize()) {
            val moving = rememberInfiniteTransition()
            val yTrans by moving.animateFloat(-200f,200f, animationSpec = infiniteRepeatable(animation = tween(400), repeatMode = RepeatMode.Reverse))
            Spacer(
                modifier = Modifier
                    .align(Alignment.Center)
                    .size(40.dp)
                    .offset {
                        IntOffset(x = 0, y = yTrans.toInt())
                    }
                    .clip(CircleShape)
                    .background(Color.Blue)
            )
        }
    },

    error = {
        Box(
            modifier = Modifier.fillMaxSize().background(Color.LightGray)
        ) {
            Text("我错了")
        }
    }
)

SubcomposeAsyncImage 会提供 loadingsuccess 以及 error 让开发者自定义 UI。上述代码我们自定义了 loadingerror,我们看下 loading 的效果:


几乎所有的 AsyncImage 用法已经介绍完毕了,我相信之前的讲解已经能够满足 88% 的开发情况了。

当然,Coil 的强大还不止于此。Coil 还支持加载一些别的类型的资源。


加载其他资源

SVG

默认情况下,Coil 无法加载 SVG 图片的,不过,我们可以添加一行依赖,让 Coil 支持:

Kotlin 复制代码
val coilBom = platform("io.coil-kt.coil3:coil-bom:3.3.0")
implementation(coilBom)
implementation("io.coil-kt.coil3:coil-compose")
implementation("io.coil-kt.coil3:coil-network-okhttp")
implementation("io.coil-kt.coil3:coil-svg") // 添加 SVG 的支持

我们会用到一张 SVG 图片------可爱的柴犬:

此时,Coil 就有了加载 SVG 的能力了:

Kotlin 复制代码
AsyncImage(
    modifier = Modifier
        .size(400.dp)
        .background(Color.Black),
    model = PIC_SVG_URL, // 加载 SVG
    contentScale = ContentScale.Fit,
    contentDescription = "ballon"
)

GIF

默认情况下,Coil 无法完美加载 GIF,会停留在第一帧:

引入依赖之后,就可以正常加载了:

Kotlin 复制代码
//...
implementation("io.coil-kt.coil3:coil-gif")
//...
Kotlin 复制代码
AsyncImage(
    modifier = Modifier
        .size(400.dp)
        .background(Color.Black),
    model = PIC_GIF_URL,
    contentScale = ContentScale.Fit,
    contentDescription = "ballon"
)

Video

Coil 也支持加载视频的预览帧,并且支持的非常棒!

首先,添加依赖:

Kotlin 复制代码
//...
implementation("io.coil-kt.coil3:coil-video")
//...

然后,通过 ImageLoader 设置解码库:

Kotlin 复制代码
ImageLoader.Builder(context)
    .components {
        add(VideoFrameDecoder.Factory())
    }
    .build()

好的,准备工作已经完全做好,现在我们尝试一下加载视频的预览:

Kotlin 复制代码
AsyncImage(
    modifier = Modifier
        .size(400.dp)
        .background(Color.Black),
    model = ImageRequest
        .Builder(LocalContext.current)
        .data(VID_URL)
        .videoFrameIndex(100) // 加载第 100 帧
        .build(),
    contentScale = ContentScale.Inside,
    contentDescription = "video"
)

Coil 支持多种方式加载预览帧:

  • videoFrameIndex,加载第 n 帧,如上述代码用到的那样,我们加载了第 100 帧作为预览图。
  • videoFramePercent,记载基于视频总时长的百分比帧。
  • videoFrameMillis,指定视频中提取帧的时间,也就是加载视频第 n 毫秒的帧。

如果你在测试的时候发现一片漆黑,很有可能不是代码出了问题,而是视频的第一帧就是黑色的,大多数电影预告片的第一帧都是黑色的。

本地资源

如果想要加载本地资源,例如 R.drawable.bkg_naruto,你不需要添加任何其他依赖:

Kotlin 复制代码
AsyncImage(
    model = R.drawable.bkg_naruto,
    contentDescription = null,
)

是的,就这么简单!


实际上,Coil 可以完美替代 Compose 中的原生控件 Image

总结

本文承接 Coil 基础用法,介绍其高级功能与拓展应用。

如果你想在 Compose 中找到一款合适的图片加载组件,那么 Coil 一定是你的不二选择。

同时,Coil 还支持跨平台,在桌面端,网页端,依然可以使用 Coil 加载图片,这让 Coil 成为了 Compose 中图片加载的最佳组件。

相关推荐
MiniCode5 小时前
EllipsizeEndTextview末尾省略自定义View
android·java·前端
得物技术6 小时前
R8疑难杂症分析实战:外联优化设计缺陷引起的崩溃|得物技术
android·性能优化·编译器
webbin6 小时前
Glide GifDrawable加载流程
android·glide
帅得不敢出门6 小时前
Android Framework打电话禁止播放运营商视频彩铃
android·java·framework·音视频
非凡ghost6 小时前
Solid Explorer文件管理器:功能强大的安卓文件管理器及网盘文件管理器
android·学习·软件需求
独自破碎E6 小时前
得物25年春招-安卓部分编程题
android·java·开发语言
用户2018792831676 小时前
StickerHeaderRecyclerView的一种简易实现
android
张风捷特烈8 小时前
FlutterUnit 3.3.0 | 全组件、全属性、鸿蒙支持来袭
android·前端·flutter