6、图片组件:Image 与 Icon 的使用、加载网络图片(Coil 库)、图片裁剪与缩放
图片,是 UI 里最绕不开的元素。不管是用户头像、商品图、还是装饰图标,你都得跟它打交道。Compose 里处理图片的方式跟传统 View 系统差别挺大,但用熟了你会发现——嗯,其实更清爽。
这一节,我带你过一遍 Image、Icon 的基本用法,再聊聊怎么用 Coil 加载网络图片,最后说说裁剪和缩放那些坑。都是实战里高频用到的,别跳过。
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。不过说实话,大部分场景下用内置图标就够了。
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)
)
嗯,这个在列表加载时特别有用。用户看到占位图,至少知道「这里有一张图正在加载」,而不是空白一片。
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 是控制显示的关键。你写代码时多想想「用户看到的是什么」,而不是「我加载了什么」,很多坑自然就避开了。