6、图片与图标:Image组件、加载网络图片(Coil)、Icon与IconButton、图标库使用。

好,咱们今天来聊聊图片和图标。这两个东西在App里太常见了,几乎每个界面都离不开。你想想看,一个没有图片、没有图标的App,那得多枯燥?

在Jetpack Compose里,处理图片和图标的方式跟传统View体系不太一样。我刚开始从XML切到Compose时,第一反应是:「嗯?Image怎么用?Glide还能用吗?」后来发现,Compose有自己的一套玩法,而且用起来更顺手。

6.1 Image组件:显示图片的基础

Image组件是Compose里最基础的图片显示方式。它支持从资源、文件、Bitmap等多个来源加载图片。

@Composable
fun BasicImageExample() {
    // 从资源加载
    Image(
        painter = painterResource(id = R.drawable.sample_image),
        contentDescription = "示例图片",
        modifier = Modifier
            .size(200.dp)
            .clip(CircleShape),
        contentScale = ContentScale.Crop
    )
}

这里有几个关键参数:

  • painter:图片的绘制器,决定了图片从哪里来
  • contentDescription:无障碍描述,别忽略它
  • contentScale:图片的缩放模式,类似ImageView的ScaleType
  • modifier:修饰符,可以控制大小、形状、点击事件等

contentScale有几种常用模式:

模式 说明 适用场景
ContentScale.Fit 保持宽高比,完整显示 展示完整图片
ContentScale.Crop 保持宽高比,裁剪填充 头像、封面图
ContentScale.FillBounds 拉伸填充,不保持比例 背景图(慎用)
ContentScale.Inside 保持比例,缩小到容器内 防止图片溢出
我的小建议:头像场景我一般用Crop,配合clip(CircleShape)就能做出圆形头像。记得给contentDescription传一个有意义的值,别传null,除非它纯粹是装饰性的。

6.2 加载网络图片:Coil的妙用

实际项目中,图片大多来自网络。在Compose里,最流行的方案是Coil。为什么选它?因为它是专门为Kotlin协程和Compose设计的,轻量、高效。

先加依赖:

// build.gradle.kts (Module)
implementation("io.coil-kt:coil-compose:2.6.0")

然后就可以用了:

@Composable
fun NetworkImageExample() {
    AsyncImage(
        model = "https://example.com/image.jpg",
        contentDescription = "网络图片",
        modifier = Modifier
            .fillMaxWidth()
            .height(200.dp),
        contentScale = ContentScale.Crop,
        placeholder = painterResource(R.drawable.placeholder),
        error = painterResource(R.drawable.error_image)
    )
}

AsyncImage是Coil提供的Composable组件。它自动处理了:

  • 图片下载和缓存
  • 内存和磁盘缓存策略
  • 加载中和加载失败的占位图
  • 图片的取消和复用

核心要点:Coil默认使用内存缓存和磁盘缓存。你不需要手动管理缓存,它会自动帮你处理好。我在项目中遇到过一个问题:图片更新了但App里还是旧的。后来发现是Coil的缓存策略导致的,用diskCachePolicymemoryCachePolicy可以控制。

如果你需要更精细的控制,可以用ImageRequest:

@Composable
fun AdvancedCoilExample() {
    val imageLoader = ImageLoader(context) {
        memoryCachePolicy(CachePolicy.ENABLED)
        diskCachePolicy(CachePolicy.ENABLED)
        crossfade(true)
    }

    AsyncImage(
        model = ImageRequest.Builder(context)
            .data("https://example.com/image.jpg")
            .crossfade(true)
            .size(500, 500)
            .build(),
        imageLoader = imageLoader,
        contentDescription = "高级加载示例"
    )
}
注意:Coil的ImageLoader建议在Application级别创建单例,避免重复创建。我曾经在某个项目里每个页面都new了一个ImageLoader,结果内存飙升,后来改成单例就好了。

6.3 Icon与IconButton:小而美的图标

图标在UI里扮演着「视觉语言」的角色。一个点赞图标、一个分享图标,用户一看就懂,不需要文字解释。

Compose里用Icon组件显示图标:

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

IconButton则是可点击的图标按钮:

@Composable
fun IconButtonExample() {
    IconButton(onClick = { /* 处理点击 */ }) {
        Icon(
            imageVector = Icons.Default.Favorite,
            contentDescription = "收藏",
            tint = Color.Red
        )
    }
}

这里有几个要点:

  • imageVector:矢量图标,缩放不失真
  • tint:图标着色,可以统一颜色
  • contentDescription:无障碍描述,必须提供
  • IconButton自带点击涟漪效果,不需要额外处理
我的习惯:我会把常用的图标封装成常量,比如val AppIcons.Home = Icons.Default.Home,这样在多个地方引用时保持一致,也方便后续替换图标库。

6.4 图标库使用:Material Icons与自定义

Compose默认集成了Material Icons库。你只需要在依赖里加上:

// build.gradle.kts
implementation("androidx.compose.material:material-icons-extended:1.6.0")

然后就可以使用所有Material图标了:

Icon(
    imageVector = Icons.Filled.AccountCircle,
    contentDescription = "用户头像"
)

Icon(
    imageVector = Icons.Outlined.Settings,
    contentDescription = "设置"
)

Icon(
    imageVector = Icons.Rounded.Star,
    contentDescription = "评分"
)

Material Icons分为几个风格:

风格 前缀 特点
Filled Icons.Filled 实心填充,最常用
Outlined Icons.Outlined 轮廓风格,更轻盈
Rounded Icons.Rounded 圆角风格,更柔和
Sharp Icons.Sharp 尖锐风格,更硬朗
TwoTone Icons.TwoTone 双色调,更丰富

如果你需要自定义图标,可以用ImageVector.Builder:

val CustomIcon = ImageVector.Builder(
    name = "CustomStar",
    defaultWidth = 24.dp,
    defaultHeight = 24.dp,
    viewportWidth = 24f,
    viewportHeight = 24f
).apply {
    addPath(
        pathData = PathParser().parsePathString("M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z").toNodes(),
        fill = SolidColor(Color.Black)
    )
}.build()

不过说实话,我很少自己画路径。一般用SVG转成VectorDrawable,然后通过painterResource加载。这样更省事,设计师给什么就用什么。

避坑指南:material-icons-extended库很大(约30MB+),如果你只用了少量图标,建议用Icons.Default(只包含基础图标),或者用R8/ProGuard做资源裁剪。我曾经因为没注意这个,导致APK体积暴增,后来用Icons.Default替换了大部分图标,体积降了20%。

知识体系总览

下面这张图帮你理清本章的核心脉络:

图片与图标知识体系 图片 Image组件 Coil网络加载 contentScale模式 缓存与占位图 图标 Icon组件 IconButton Material Icons 自定义图标 核心:图片用Coil,图标用Material,别忘了contentDescription

嗯,图片和图标这部分内容其实不难,但细节挺多。我个人觉得,最重要的是养成好习惯:

  • 图片一定要有占位图和错误图
  • 图标一定要有contentDescription
  • 网络图片用Coil,别自己写下载逻辑
  • 图标库按需引入,别一股脑全加进来

这些习惯能帮你避免很多线上问题。我在项目里见过太多因为图片加载失败导致界面空白的案例,也见过因为图标库太大导致APK体积爆炸的情况。说白了,这些坑都是可以提前避开的。

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