4、常用组件:Text、Button、Image、TextField 的使用与自定义,Icon 与图标系统。
好,咱们今天来聊聊 Jetpack Compose 里最常用、也最基础的几个组件。说实话,不管多复杂的界面,拆开来看,无非就是文字、按钮、图片、输入框和图标的排列组合。我刚开始用 Compose 的时候,觉得这玩意儿跟 XML 写 View 完全是两码事,但用顺手了之后,真香。
这一节,我会带着你把这些组件的核心用法过一遍,顺便聊聊我踩过的坑。嗯,咱们开始吧。
4.1 Text:不只是显示文字
Text 是 Compose 里最基础的组件,没有之一。你想想看,哪个 App 没有文字?但它的用法其实比你想的要灵活得多。
基本用法
@Composable
fun Greeting(name: String) {
Text(text = "你好,$name")
}
就这么简单。但实际项目中,我们往往需要调整样式。我个人习惯把常用的文字样式封装成一个主题,而不是到处写硬编码。
自定义样式
Text(
text = "这是一段重要提示",
color = Color.Red,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
maxLines 和 overflow,否则文字可能会把界面撑爆。我在项目中就遇到过用户输入超长文本导致布局错乱的情况,后来统一加了这两个属性,世界清净了。
富文本与 Span
有时候一段文字里需要不同的样式,比如「点击这里注册」,其中「注册」要加粗变色。这时候可以用 AnnotatedString:
val annotatedString = buildAnnotatedString {
append("点击")
withStyle(SpanStyle(color = Color.Blue, fontWeight = FontWeight.Bold)) {
append("注册")
}
append("开始使用")
}
Text(text = annotatedString)
4.2 Button:点击的学问
Button 在 Compose 里也分好几种,最常用的是 Button 和 OutlinedButton。说白了,一个带背景色,一个只有边框。
基础点击
Button(onClick = { /* 处理点击 */ }) {
Text("提交")
}
注意,Button 的内容是通过 content 槽位传入的,所以你可以放任何 Composable,不只是文字。比如放一个图标加文字:
Button(onClick = { /* 登录 */ }) {
Icon(Icons.Default.Person, contentDescription = "用户")
Spacer(modifier = Modifier.width(8.dp))
Text("登录")
}
Image,结果图片把按钮撑得变形了。后来发现,给图片加上 Modifier.size(24.dp) 就好了。记住,Button 本身不会限制子组件的大小,你得自己控制。
自定义形状与颜色
Button(
onClick = { /* 删除 */ },
colors = ButtonDefaults.buttonColors(
containerColor = Color.Red,
contentColor = Color.White
),
shape = RoundedCornerShape(8.dp)
) {
Text("删除")
}
4.3 Image:图片加载与占位
Compose 里的 Image 组件,说白了就是用来显示图片的。但要注意,它本身不负责网络加载,你需要配合 Coil 或 Glide 的 Compose 扩展来用。
显示本地资源
Image(
painter = painterResource(id = R.drawable.ic_launcher),
contentDescription = "应用图标",
modifier = Modifier.size(64.dp)
)
显示网络图片(使用 Coil)
AsyncImage(
model = "https://example.com/image.jpg",
contentDescription = "网络图片",
placeholder = painterResource(R.drawable.placeholder),
error = painterResource(R.drawable.error)
)
嗯,这里要提一句,contentDescription 是给无障碍功能用的,别偷懒不写。我见过很多 App 图片没有描述,屏幕阅读器用户根本不知道那是什么。
缩放模式
ContentScale 控制图片如何适应容器。常用的有:
ContentScale.Fit— 保持比例,完整显示ContentScale.Crop— 裁剪填充,不留空白ContentScale.FillBounds— 拉伸填充,可能变形
Crop,封面图用 Fit,产品图用 FillBounds 要谨慎,容易把商品拉变形。
4.4 TextField:输入框的进化
TextField 在 Compose 里分两种:TextField(Material 2 风格)和 OutlinedTextField(带边框)。我个人更推荐 OutlinedTextField,视觉上更清晰。
基本输入
var text by remember { mutableStateOf("") }
OutlinedTextField(
value = text,
onValueChange = { text = it },
label = { Text("用户名") },
placeholder = { Text("请输入用户名") }
)
注意,value 和 onValueChange 是必须的,Compose 是单向数据流,你得自己管理状态。
输入验证与键盘类型
OutlinedTextField(
value = text,
onValueChange = { if (it.length <= 11) text = it },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
visualTransformation = PasswordVisualTransformation(),
isError = text.length < 11 && text.isNotEmpty(),
supportingText = {
if (text.isNotEmpty() && text.length < 11) {
Text("手机号必须为11位", color = MaterialTheme.colorScheme.error)
}
}
)
visualTransformation 可以实现密码遮罩、信用卡号格式化等效果。我曾经做过一个银行卡号输入框,每4位自动加空格,就是用 VisualTransformation 实现的。
4.5 Icon 与图标系统
Compose 自带了一套 Material Icons,分为 Icons.Default、Icons.Filled、Icons.Outlined 等风格。用法很简单:
Icon(
imageVector = Icons.Default.Home,
contentDescription = "首页",
tint = Color.Gray
)
如果你需要自定义图标,可以用 ImageVector 或者直接放一个 Image 组件加载 SVG。我个人习惯把常用的图标封装成一个 sealed class,方便统一管理。
图标与文字组合
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.Star, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.width(4.dp))
Text("4.5")
}
这里 contentDescription 设为 null 表示这个图标是装饰性的,屏幕阅读器会忽略它。嗯,这个细节很多人不注意。
知识体系总览
下面这张图,是我梳理的本章核心组件关系。你看一眼,心里就有谱了。
自定义组件:组合的艺术
Compose 的核心思想就是「组合优于继承」。你不需要创建一个自定义 View,只需要把现有组件拼起来。比如,一个带图标的输入框:
@Composable
fun IconTextField(
value: String,
onValueChange: (String) -> Unit,
icon: ImageVector,
label: String
) {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
label = { Text(label) },
leadingIcon = { Icon(icon, contentDescription = null) },
singleLine = true
)
}
你看,就这么几行代码,一个可复用的组件就出来了。我在项目中经常这样封装,把重复的 UI 模式抽象成函数,代码量直接减半。