6、图片组件:Image 与 Icon 的使用、加载网络图片(Coil 库)、图片裁剪与缩放

图片,是 UI 里最绕不开的元素。不管是用户头像、商品图、还是装饰图标,你都得跟它打交道。Compose 里处理图片的方式跟传统 View 系统差别挺大,但用熟了你会发现——嗯,其实更清爽。

这一节,我带你过一遍 Image、Icon 的基本用法,再聊聊怎么用 Coil 加载网络图片,最后说说裁剪和缩放那些坑。都是实战里高频用到的,别跳过。

Compose 图片组件知识体系 Image 组件 · 加载资源/文件/网络 · painter 参数 · contentScale 缩放 · contentDescription 无障碍 Icon 组件 · 内置图标(Icons.Default) · 自定义图标(ImageVector) · tint 着色 · 尺寸与点击区域 Coil 网络图片 · rememberAsyncImagePainter · 占位图/错误图 · 缓存策略 · 图片变换 裁剪与缩放(ContentScale) Crop / Fit / Inside / FillBounds / FillWidth / FillHeight 💡 核心思路:painter 提供数据源,contentScale 控制显示方式,Modifier 调整布局

6.1 Image 组件:最基础的图片加载方式

Compose 的 Image 组件,说白了就是用来展示一张图。它不像 ImageView 那样有 src 属性,而是通过 painter 参数来接收图片数据。

我个人习惯把 painter 理解成「画师」——你给他什么资源,他就画什么图。最常见的用法是加载本地 drawable 资源:

@Composable
fun LocalImageExample() {
    Image(
        painter = painterResource(id = R.drawable.ic_launcher_background),
        contentDescription = "应用图标",
        modifier = Modifier.size(120.dp)
    )
}

这里有个细节:contentDescription 不能省略。虽然你传空字符串也能编译通过,但无障碍服务会把它当成无意义的装饰图。我在项目里见过因为漏掉这个被无障碍测试打回来的情况,所以建议养成好习惯。

除了 drawable,你还可以加载文件、Bitmap、甚至是矢量图。比如从文件读取:

@Composable
fun FileImageExample(filePath: String) {
    val bitmap = remember(filePath) {
        BitmapFactory.decodeFile(filePath)
    }
    Image(
        painter = rememberAsyncImagePainter(model = filePath),
        contentDescription = "本地图片",
        modifier = Modifier.fillMaxWidth()
    )
}
小提示:如果图片是固定的资源文件,用 painterResource 就够了。如果是动态路径或网络图片,建议用 Coil 的 rememberAsyncImagePainter,后面会细说。

6.2 Icon 组件:轻量级图标解决方案

Icon 跟 Image 很像,但它专门用来展示小尺寸图标。你想想看,如果每个图标都用 Image 加载一张 PNG,那包体积得多大?Compose 内置了 Material Icons,直接通过 Icons.Default 就能用:

@Composable
fun IconExample() {
    Icon(
        imageVector = Icons.Default.Home,
        contentDescription = "首页",
        tint = Color.Blue,
        modifier = Modifier.size(24.dp)
    )
}

tint 参数是 Icon 独有的——它可以给图标着色。我在项目中经常用这个特性做主题切换,比如白天用黑色图标,夜间模式改成白色,一行代码搞定。

如果你需要自定义图标,可以用 ImageVector 自己画路径,或者导入 SVG 转成 ImageVector。不过说实话,大部分场景下用内置图标就够了。

注意:Icon 默认的点击区域可能偏小。如果你需要让图标可点击,建议用 Modifier.clickable 配合 Modifier.padding 扩大触摸区域,否则用户可能点不准。

6.3 加载网络图片:Coil 库实战

网络图片加载,是移动开发里最频繁的需求之一。Compose 官方推荐用 Coil,因为它轻量、Kotlin 原生、而且跟 Compose 集成得特别好。

首先,你需要在 build.gradle.kts 里加依赖:

implementation("io.coil-kt:coil-compose:2.6.0")

然后就可以用了。核心 API 是 rememberAsyncImagePainter

@Composable
fun NetworkImageExample(url: String) {
    val painter = rememberAsyncImagePainter(
        model = ImageRequest.Builder(LocalContext.current)
            .data(url)
            .crossfade(true)  // 淡入效果
            .build()
    )
    Image(
        painter = painter,
        contentDescription = "网络图片",
        modifier = Modifier.fillMaxWidth()
    )
}

这里我特别想说一下 crossfade。我曾经在一个电商项目里没加这个参数,结果图片切换时闪白,用户体验很差。加上淡入效果后,视觉上平滑很多。

Coil 还支持占位图和错误图:

val painter = rememberAsyncImagePainter(
    model = url,
    placeholder = painterResource(R.drawable.placeholder),
    error = painterResource(R.drawable.error)
)

嗯,这个在列表加载时特别有用。用户看到占位图,至少知道「这里有一张图正在加载」,而不是空白一片。

核心要点:Coil 默认会做内存缓存和磁盘缓存,你不需要额外配置。但如果图片更新频繁(比如用户头像),记得设置 diskCachePolicy = CachePolicy.DISABLED 或者加时间戳参数。

6.4 图片裁剪与缩放:ContentScale 详解

图片加载进来后,怎么显示?是裁剪、拉伸、还是保持原比例?这就轮到 ContentScale 出场了。

Compose 提供了 6 种缩放模式,我整理了一张表:

模式 行为 适用场景
ContentScale.Crop 保持比例,裁剪超出部分 头像、封面图(最常用)
ContentScale.Fit 保持比例,完整显示,可能有留白 商品详情图、banner
ContentScale.Inside 等比缩小到容器内,不放大 小图标、缩略图
ContentScale.FillBounds 拉伸填满,不保持比例 极少用,除非你故意要变形效果
ContentScale.FillWidth 宽度填满,高度按比例缩放 横向滚动列表
ContentScale.FillHeight 高度填满,宽度按比例缩放 纵向滚动列表

我个人最常用的是 Crop。比如用户头像,你希望它填满圆形或方形区域,多出来的部分裁掉就好。代码示例:

Image(
    painter = rememberAsyncImagePainter(model = avatarUrl),
    contentDescription = "用户头像",
    contentScale = ContentScale.Crop,
    modifier = Modifier
        .size(48.dp)
        .clip(CircleShape)  // 裁成圆形
)

这里 clip(CircleShape)ContentScale.Crop 是绝配——先裁剪比例,再裁形状,出来的头像又圆又正。

避坑指南:我曾经在列表里用 FillBounds 加载不同尺寸的图片,结果有些图被拉伸得面目全非。后来统一改成 Crop,问题解决。记住:除非你明确需要变形,否则别用 FillBounds

6.5 实战组合:一个完整的图片展示卡片

把上面这些知识点串起来,写一个带网络图片、图标、裁剪的卡片组件:

@Composable
fun ImageCard(
    imageUrl: String,
    title: String,
    onFavoriteClick: () -> Unit
) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp),
        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
    ) {
        Column {
            // 网络图片,裁剪填满宽度
            Image(
                painter = rememberAsyncImagePainter(
                    model = ImageRequest.Builder(LocalContext.current)
                        .data(imageUrl)
                        .crossfade(true)
                        .build(),
                    placeholder = painterResource(R.drawable.placeholder),
                    error = painterResource(R.drawable.error)
                ),
                contentDescription = title,
                contentScale = ContentScale.Crop,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(180.dp)
            )
            // 标题行 + 收藏图标
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(12.dp),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Text(text = title, style = MaterialTheme.typography.titleMedium)
                IconButton(onClick = onFavoriteClick) {
                    Icon(
                        imageVector = Icons.Default.FavoriteBorder,
                        contentDescription = "收藏",
                        tint = Color.Red
                    )
                }
            }
        }
    }
}

这个组件里,Image 负责展示网络图,Icon 负责收藏按钮,ContentScale.Crop 保证图片不拉伸。你直接拿去用,稍微改改就能适配大部分卡片场景。

我的习惯:所有网络图片都加上 crossfade(true) 和占位图。虽然代码多两行,但用户体验提升很明显。尤其是弱网环境下,用户不会看到闪白或空白。

好了,图片组件这块就聊到这儿。Image 和 Icon 是基础,Coil 是网络加载的标配,ContentScale 是控制显示的关键。你写代码时多想想「用户看到的是什么」,而不是「我加载了什么」,很多坑自然就避开了。


公众号:蓝海资料掘金营,微信deep3321