Compose 常用组件
Material3 Text / Button / TextField / Image / Icon / Card / Slider / Checkbox / RadioButton / Switch / AlertDialog / Snackbar 等核心组件详解
学习目标
- 熟练使用 Text 的样式 / 颜色 / 截断 / maxLines
- 掌握 Button / OutlinedButton / TextButton 的取舍
- 用 TextField 实现表单输入,配置键盘类型与 visualTransformation
- 用 Card / Icon / Image / Slider / 开关类组件拼业务卡片
- 用 AlertDialog 与 Snackbar(SnackbarHostState)实现弹窗与提示
学习目标
布局是骨架,组件是血肉。Material3 在 Compose 中以 @Composable 函数形式提供了一系列组件,每个都是“一个状态 + 一段 UI”的封装。本章逐个拆解 Android 日常最常用的 13 个组件。
读完本章你将能够:
- 用
Text配TextStyle/overflow/maxLines控制文本呈现; - 在
Button/OutlinedButton/TextButton中按视觉重要性挑选; - 用
TextField/OutlinedTextField写表单输入,掌握键盘类型、visualTransformation与错误状态; - 用
Image/Icon/Card拼出列表项卡片; - 用
Slider/Checkbox/RadioButton/Switch处理用户输入; - 用
AlertDialog弹模态确认框,用Snackbar配SnackbarHostState显示临时提示。
本章假设你已配置好 Compose BOM(参考第 44 章)。所有 import 路径以
androidx.compose.material3.*为主。
Text
1. 基本使用
Text(text = "Hello Compose")
2. 样式
Text(
text = "标题",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.primary,
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.SansSerif,
letterSpacing = 0.5.sp,
textAlign = TextAlign.Center,
textDecoration = TextDecoration.Underline,
)
style 是预设模板(M3 提供了 headline / title / body / label 多个层级),其余参数会覆盖 style 中的对应字段。
3. 截断与行数
Text(
text = "这是一段很长的描述文本,超过两行就会被截断…",
maxLines = 2,
overflow = TextOverflow.Ellipsis, // 超出部分显示…
softWrap = true, // 自动换行(默认 true)
)
overflow 可选 Clip / Ellipsis / Visible。配合 maxLines 处理过长文案。
4. 富文本 AnnotatedString
val annotated = buildAnnotatedString {
append("同意 ")
pushStringAnnotation(tag = "URL", annotation = "https://example.com")
withStyle(SpanStyle(color = Color.Blue, textDecoration = TextDecoration.Underline)) {
append("服务条款")
}
pop()
append("。")
}
Text(text = annotated)
ClickableText 还能让链接部分可点击,获取对应 annotation。
5. 多行间距
Text(
text = "...",
style = LocalTextStyle.current.copy(lineHeight = 28.sp),
)
Button 系列
Button(onClick = { /* 提交 */ }) {
Text("提交")
}
参数:
| 参数 | 含义 |
|---|---|
onClick |
必填,点击回调(普通 lambda) |
enabled |
是否可点(false 时变灰) |
modifier |
布局 |
shape |
圆角,默认 ButtonShape |
colors |
通过 ButtonDefaults.buttonColors() 自定义颜色 |
contentPadding |
内边距 |
content |
子组件(一般包一个 Text + 可选 Icon) |
三种 Button 的取舍:
| 组件 | 用途 | 视觉 |
|---|---|---|
Button |
主要操作(提交、确认) | 填充背景 |
OutlinedButton |
次要操作(取消、重置) | 描边无填充 |
TextButton |
文本类操作(“查看更多”) | 仅文本 |
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = { /* 提交 */ }) { Text("提交") }
OutlinedButton(onClick = { /* 取消 */ }) { Text("取消") }
TextButton(onClick = { /* 跳转 */ }) { Text("查看更多") }
}
带图标的按钮:
Button(onClick = { }) {
Icon(Icons.Default.Save, contentDescription = null)
Spacer(Modifier.width(4.dp))
Text("保存")
}
TextField 与 OutlinedTextField
1. 受控式 API
Compose 的输入框是受控组件:状态在外部,UI 通过 value + onValueChange 反映状态。
@Composable
fun NameField() {
var name by remember { mutableStateOf("") }
TextField(
value = name,
onValueChange = { name = it },
label = { Text("用户名") },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
}
坑:忘记把
onValueChange写回状态,输入框不会响应任何字符。
2. 键盘选项
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.ImeAction
TextField(
value = phone,
onValueChange = { phone = it },
label = { Text("手机号") },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Phone,
imeAction = ImeAction.Send,
autoCorrect = false,
capitalization = KeyboardCapitalization.None,
),
)
KeyboardType 选项:Text / Ascii / Number / Phone / Uri / Email / Password / NumberPassword。
ImeAction 选项:Default / Done / Go / Next / Previous / Search / Send。
3. 密码视觉转换
var password by remember { mutableStateOf("") }
var visible by remember { mutableStateOf(false) }
TextField(
value = password,
onValueChange = { password = it },
label = { Text("密码") },
singleLine = true,
visualTransformation =
if (visible) VisualTransformation.None
else PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
trailingIcon = {
IconButton(onClick = { visible = !visible }) {
Icon(
imageVector = if (visible) Icons.Default.VisibilityOff else Icons.Default.Visibility,
contentDescription = if (visible) "隐藏密码" else "显示密码",
)
}
},
)
4. 错误状态
val isError = name.length !in 3..20
TextField(
value = name,
onValueChange = { name = it },
isError = isError,
label = { Text("用户名") },
supportingText = {
if (isError) Text("长度需 3-20") else Text("建议 3-20 字符")
},
)
5. OutlinedTextField
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("邮箱") },
)
行为与 TextField 一致,只是视觉是描边而不是填充背景,常用于表单批量输入。
Image 与 Icon
1. Image
Image(
painter = painterResource(R.drawable.avatar),
contentDescription = "用户头像",
contentScale = ContentScale.Crop,
modifier = Modifier
.size(80.dp)
.clip(CircleShape),
)
参数:
| 参数 | 含义 |
|---|---|
painter / imageBitmap / vector |
资源 / 位图 / 矢量 |
contentDescription |
无障碍描述,纯装饰图像填 null |
contentScale |
Crop / Fit / FillBounds / Inside |
alignment |
在自身 bounds 内的对齐 |
colorFilter / alpha |
颜色滤镜与透明度 |
加载网络图片用 Coil:
implementation("io.coil-kt:coil-compose:2.7.0")
AsyncImage(
model = "https://example.com/a.jpg",
contentDescription = "网络图",
contentScale = ContentScale.Crop,
modifier = Modifier.size(120.dp).clip(RoundedCornerShape(8.dp)),
)
2. Icon
Icon(
imageVector = Icons.Default.Settings,
contentDescription = "设置",
tint = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.size(24.dp),
)
Icons.Default.* 来自 androidx.compose.material:material-icons-extended(可选依赖)提供全套 Material 图标。tint 决定图标颜色。
装饰性图标(按钮内文字已说明用途)的
contentDescription应填null,避免 TalkBack 重复念。
Card
Card(
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
modifier = Modifier.fillMaxWidth(),
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("标题", style = MaterialTheme.typography.titleLarge)
Text("副标题", style = MaterialTheme.typography.bodyMedium)
}
}
Card 是带阴影 / 形状 / 背景的容器。M3 推荐使用 ElevatedCard / OutlinedCard 两个变体:
| 变体 | 视觉 | 场景 |
|---|---|---|
Card |
阴影 + 填充背景 | 通用 |
ElevatedCard |
较强阴影 | 重要内容 |
OutlinedCard |
描边 | 列表项 |
加点击:
Card(onClick = { /* 打开详情 */ }) {
Text("可点")
}
M3 起 Card 直接支持 onClick,省去 Modifier.clickable。
Slider
@Composable
fun VolumeSlider() {
var volume by remember { mutableStateOf(50f) }
Slider(
value = volume,
onValueChange = { volume = it },
valueRange = 0f..100f,
steps = 0, // 0 = 连续;N = 分 N+1 段
onValueChangeFinished = { println("最终:$volume") },
modifier = Modifier.fillMaxWidth(),
)
}
steps 用于“分段滑块”(如 0 / 25 / 50 / 75 / 100):
Slider(
value = progress,
onValueChange = { },
valueRange = 0f..100f,
steps = 3, // 4 个停顿点
)
开关类组件
1. Checkbox
var checked by remember { mutableStateOf(false) }
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = checked, onCheckedChange = { checked = it })
Text("同意条款")
}
2. RadioButton
val options = listOf("男", "女", "保密")
var selected by remember { mutableStateOf(options.first()) }
Column {
options.forEach { label ->
Row(verticalAlignment = Alignment.CenterVertically) {
RadioButton(
selected = selected == label,
onClick = { selected = label },
)
Text(label)
}
}
}
3. Switch
var dark by remember { mutableStateOf(false) }
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text("深色模式")
Switch(checked = dark, onCheckedChange = { dark = it })
}
三者 API 一致:checked 状态 + onCheckedChange 回调。
AlertDialog
@Composable
fun ConfirmDialog(show: Boolean, onConfirm: () -> Unit, onDismiss: () -> Unit) {
if (show) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("删除") },
text = { Text("确认删除这条数据?此操作不可恢复。") },
confirmButton = {
TextButton(onClick = onConfirm) { Text("删除") }
},
dismissButton = {
TextButton(onClick = onDismiss) { Text("取消") }
},
)
}
}
要点:
- 用
if (show)控制显示,不是写“show ? Dialog : null”的 Java 风格 if 表达式; onDismissRequest在用户点外部 / 返回键时触发;- 内部用
Dialog槽可自定义任意内容。
更复杂的对话框用 Dialog(onDismissRequest = { }) { /* 自定义 Composable */ }。
Snackbar
1. SnackbarHostState
@Composable
fun SnackBarDemo() {
val host = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
Scaffold(
snackbarHost = { SnackbarHost(hostState = host) },
) { padding ->
Column(modifier = Modifier.padding(padding)) {
Button(onClick = {
scope.launch {
val result = host.showSnackbar(
message = "已保存",
actionLabel = "撤销",
duration = SnackbarDuration.Short,
)
if (result == SnackbarResult.ActionPerformed) {
// 撤销逻辑
}
}
}) { Text("保存") }
}
}
}
要点:
SnackbarHostState是状态持有者,showSnackbar是 suspend 函数,必须在协程中调用;SnackbarHost(hostState)渲染 UI,通常放Scaffold.snackbarHost槽;duration选项Short(约 1.5s)/Long(约 3s)/Indefinite(手动 dismiss);actionLabel配合返回SnackbarResult.ActionPerformed处理点击动作(如“撤销”)。
2. 与 navigation 联动
val navController = rememberNavController()
val snackbarHost = remember { SnackbarHostState() }
LaunchedEffect(Unit) {
navController.currentBackStackEntryAsFlow().collect { entry ->
entry?.savedStateHandle?.get<String>("snackbar")?.let { msg ->
snackbarHost.showSnackbar(msg)
}
}
}
页面间传递消息常用 “navigate + savedStateHandle + Snackbar” 组合。
综合示例:设置卡片
@Composable
fun SettingsCard(
darkMode: Boolean,
volume: Float,
onDarkModeChange: (Boolean) -> Unit,
onVolumeChange: (Float) -> Unit,
onClearCache: () -> Unit,
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("设置", style = MaterialTheme.typography.titleLarge)
Spacer(Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text("深色模式")
Switch(checked = darkMode, onCheckedChange = onDarkModeChange)
}
Spacer(Modifier.height(8.dp))
Column {
Text("音量:${volume.toInt()}")
Slider(
value = volume,
onValueChange = onVolumeChange,
valueRange = 0f..100f,
)
}
Spacer(Modifier.height(12.dp))
OutlinedButton(
onClick = onClearCache,
modifier = Modifier.fillMaxWidth(),
) { Text("清除缓存") }
}
}
}
@Preview(showBackground = true)
@Composable
fun SettingsCardPreview() {
MaterialTheme {
SettingsCard(
darkMode = false,
volume = 60f,
onDarkModeChange = {},
onVolumeChange = {},
onClearCache = {},
)
}
}
常见坑与最佳实践
Text不写maxLines:长文本会撑破容器或溢出,列表项尤其明显。TextField忘记onValueChange:输入框看起来“无法输入”,因为状态没回流。务必把it写回状态。Button子级放非Text没加Spacer:图标和文字紧贴。按钮内建议Icon + Spacer + Text三件套。Image不写contentScale:默认Fit,头像常被拉变形。圆形头像用clip(CircleShape)+Crop。Icon不写tint:默认是LocalContentColor,在非 M3 上下文中可能不可见,建议显式tint。Card.onClickvsModifier.clickable:M3 起用Card(onClick = ...),自带涟漪与高亮,比Modifier.clickable更协调。Switch/Checkbox状态提升:组件内部不存状态,每次重组用外部传入。把状态用remember { mutableStateOf(...) }放上层。AlertDialog不处理onDismissRequest:用户按返回键没反应。空实现也至少要onDismiss = {}。showSnackbar在协程外调用:showSnackbar是 suspend,必须在rememberCoroutineScope().launch { }中调用。SnackbarHost不放 Scaffold:直接渲染会盖住内容。放Scaffold.snackbarHost才会有自动避让。
章节小结
Text用style+maxLines+overflow = Ellipsis控制样式与截断;富文本用buildAnnotatedString;Button/OutlinedButton/TextButton按视觉重要性挑选,按钮内子级常用Icon + Spacer + Text;TextField/OutlinedTextField是受控组件,必填value+onValueChange;用keyboardOptions控制键盘类型与imeAction,用visualTransformation隐藏密码;Image配contentScale+clip;Icon配tint;网络图用 Coil 的AsyncImage;Card/ElevatedCard/OutlinedCard是带阴影容器,M3 起支持onClick槽;Slider/Checkbox/RadioButton/Switch状态完全由外部持有,组件只负责渲染与回调;AlertDialog用if (show) { ... }控制显示;Snackbar通过SnackbarHostState+showSnackbarsuspend 调用,渲染放Scaffold.snackbarHost。
下一章预告
下一章我们进入 Compose 列表:LazyColumn / LazyRow 的 items / item / contentPadding、LazyVerticalGrid 的 cells、key 参数的性能意义、LazyListState 控制滚动、stickyHeader、animateItemPlacement、嵌套滚动与无障碍 contentDescription,最后给出列表性能优化清单。