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 中图片加载的最佳组件。

相关推荐
柿蒂1 小时前
聊聊SliverPersistentHeader优先消费滑动的设计
android·flutter
假装多好1233 小时前
android三方调试几个常用命令
android·1024程序员节·三方,gms
侧耳4293 小时前
android11禁止安装apk
android·java·1024程序员节
JohnnyDeng944 小时前
ArkTs-Android 与 ArkTS (HarmonyOS) 存储目录全面对比
android·harmonyos·arkts·1024程序员节
2501_915918414 小时前
iOS 26 查看电池容量与健康状态 多工具组合的工程实践
android·ios·小程序·https·uni-app·iphone·webview
limingade5 小时前
手机摄像头如何识别体检的色盲检查图的数字和图案(下)
android·1024程序员节·色盲检查图·手机摄像头识别色盲图案·android识别色盲检测卡·色盲色弱检测卡
文火冰糖的硅基工坊5 小时前
[嵌入式系统-150]:智能机器人(具身智能)内部的嵌入式系统以及各自的功能、硬件架构、操作系统、软件架构
android·linux·算法·ubuntu·机器人·硬件架构
2501_915909066 小时前
iOS 架构设计全解析 从MVC到MVVM与使用 开心上架 跨平台发布 免Mac
android·ios·小程序·https·uni-app·iphone·webview
明道源码7 小时前
Android Studio 创建 Android 模拟器
android·ide·android studio
明道源码7 小时前
Android Studio 使用教程
android·ide·android studio