Android Compose 01_基础显示组件

基础显示组件 - Text、Button、TextField、FAB

前言

欢迎来到 Compose 学习的第一站!

如果说 XML 布局像砖块盖房子,每一块都要精心打磨;那么 Compose 就像乐高积木,拿起就能拼,拼完就能用。

今天要讲的四个组件——Text、Button、TextField、FloatingActionButton(FAB),就是 Compose 中最基础的那几块积木。无论你要盖什么"房子",这几块积木都少不了。

67tool-2026-02-04 10_13_47

它们就像你手机里的App一样无处不在:

  • Text:显示文字,就像朋友圈的文字内容
  • Button:用户点击的按钮,就像"发布"按钮
  • TextField:输入框,就像评论框、搜索框
  • FAB:浮动操作按钮,就像新建邮件的"+"按钮

让我们一个一个来认识它们!


1. Text 组件 - 文字显示的万花筒

Text 是 Compose 中最简单的组件,也是最常用的组件。毕竟,没有文字的 App 就像没有文字的书,很难传达信息。

1.1 实战示例一:基础文本样式

让我们先看看 Text 组件的各种样式:

// 基础文本样式
Card(modifier = Modifier.fillMaxWidth()) {
    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        Text(
            text = "基础文本样式",
            style = MaterialTheme.typography.titleMedium,
            fontWeight = FontWeight.Bold
        )

        // 使用预设样式
        Text(text = "默认文本样式 (bodyLarge)")
        Text(
            text = "标题样式 (headlineSmall)",
            style = MaterialTheme.typography.headlineSmall
        )
        Text(
            text = "副标题样式 (titleMedium)",
            style = MaterialTheme.typography.titleMedium
        )
        Text(
            text = "标签样式 (labelMedium)",
            style = MaterialTheme.typography.labelMedium
        )
    }
}

代码讲解:

  1. Card 容器:用 Card 把相关内容包在一起,Card 会自动添加圆角和阴影,让内容有层次感(先忽略下章介绍)
  2. Column 布局:Column 让多个 Text 垂直排列,spacedBy(8.dp) 表示每个 Text 之间间隔 8dp(先忽略下章介绍)
  3. 预设样式:可以看到,我们使用了 MaterialTheme.typography.xxx 来获取预设样式,这样可以保证整个 App 的文字风格统一
  4. fontWeight.Bold:给标题加粗,让层级更清晰

运行效果:

你注意到没有?第一个 Text 没有设置 style,它使用的是默认的 bodyLarge 样式。而后面几个 Text 分别使用了不同的预设样式,大小和粗细都有明显区别。

1.2 Text 常用 API 详解

API 参数 作用 使用场景 注意事项
text: String 要显示的文本内容 显示任何文字 必需参数,支持字符串模板如 "计数: $count"
style: TextStyle 文本样式 统一文字风格 推荐使用 MaterialTheme.typography.xxx 而非硬编码
color: Color 文字颜色 强调或区分文字 优先使用主题颜色,如 MaterialTheme.colorScheme.primary
fontSize: TextUnit 字体大小 自定义文字尺寸 使用 sp 单位以支持系统字体缩放,如 18.sp
fontWeight: FontWeight 字体粗细 强调重要内容 常用值:Bold(粗体)、Medium(中等)、Normal(正常)
textAlign: TextAlign 文本对齐 居中/两端对齐等 需配合 Modifier.fillMaxWidth() 使用才生效
maxLines: Int 最大行数 限制显示行数 配合 overflow 使用,超出则截断
overflow: TextOverflow 溢出处理 文本过长时的处理 Clip(直接裁剪)、Ellipsis(显示省略号...)
lineHeight: TextUnit 行高 多行文本的行间距 提升长文本可读性,如 24.sp
modifier: Modifier 修饰符 控制尺寸、位置等 Modifier.padding(16.dp)

1.3 Material Theme 预设样式

Material 3 预定义了一套字体样式,强烈推荐使用它们而不是硬编码:

样式 用途 示例场景
displayLarge/Medium/Small 超大标题 启动页欢迎语
headlineLarge/Medium/Small 大标题 页面主标题
titleLarge/Medium/Small 中等标题 卡片标题、列表项标题
bodyLarge/Medium/Small 正文 文章内容、描述文字
labelLarge/Medium/Small 标签 按钮文字、输入框标签

使用预设样式的好处:

  • 自动适配深色模式
  • 统一的设计风格
  • 支持无障碍功能
  • 维护更方便

1.4 实战示例二:文本溢出处理

当文本内容过长时,我们需要控制它的显示方式:

// 文本溢出处理
Card(modifier = Modifier.fillMaxWidth()) {
    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        Text(
            text = "文本溢出处理",
            style = MaterialTheme.typography.titleMedium,
            fontWeight = FontWeight.Bold
        )

        // 方式一:直接裁剪
        Text(
            text = "这是一段很长的文本,如果超出容器宽度会如何显示?",
            modifier = Modifier.width(200.dp),
            maxLines = 1,
            overflow = TextOverflow.Clip
        )
        // 方式二:显示省略号
        Text(
            text = "这是一段很长的文本,使用省略号处理溢出...",
            modifier = Modifier.width(200.dp),
            maxLines = 1,
            overflow = TextOverflow.Ellipsis
        )
        // 方式三:两行后省略
        Text(
            text = "这是一段很长的文本,如果超出容器宽度会如何显示?",
            modifier = Modifier.width(200.dp),
            maxLines = 2,
            overflow = TextOverflow.Ellipsis
        )
    }
}

代码讲解:

这里演示了三种处理长文本的方式:

  1. TextOverflow.Clip:直接裁断超出的部分,文字会突然中断
  2. TextOverflow.Ellipsis + maxLines = 1:单行显示,超出部分用省略号(...)代替,最常用
  3. TextOverflow.Ellipsis + maxLines = 2:最多显示两行,第二行超出时用省略号

小技巧Modifier.width(200.dp) 限制容器宽度是为了演示溢出效果,实际使用时可以根据需求调整。

运行效果:

你可以清楚地看到三种处理方式的区别:第一种直接截断很突兀,第二种用省略号最友好,第三种可以显示更多内容。

1.5 实战示例三:动态文本

Text 也可以显示动态内容,比如计数器:

// 动态文本
Card(modifier = Modifier.fillMaxWidth()) {
    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        Text(
            text = "动态文本",
            style = MaterialTheme.typography.titleMedium,
            fontWeight = FontWeight.Bold
        )

        var counter by remember { mutableIntStateOf(0) }

        Text(
            text = "点击次数: $counter",
            fontSize = 18.sp,
            fontWeight = FontWeight.Medium
        )

        Button(
            onClick = { counter++ },
            modifier = Modifier.align(Alignment.CenterHorizontally)
        ) {
            Text("增加计数")
        }
    }
}

代码讲解:

  1. remember { mutableIntStateOf(0) }:这是 Compose 的状态管理,counter 会记住当前的计数值
  2. $counter:Kotlin 的字符串模板,直接把变量的值嵌入到字符串中
  3. onClick = { counter++ }:每次点击按钮,counter 就会加 1,Text 会自动更新显示

重点理解:在 Compose 中,你不需要手动去更新 UI,只需要改变状态,UI 会自动重组(Recompose)来反映新的状态。这就是 Compose 的核心思想——数据驱动 UI

运行效果:

PixPin_2026-02-04_10-19-57

每次点击按钮,你会看到数字立即更新,这就是 Compose 响应式编程的魅力!


2. Button 组件 - 用户交互的主角

Button 是用户最常点击的组件,Material 3 提供了多种按钮样式,让你可以根据重要程度选择合适的按钮。

2.1 实战示例一:按钮家族大合影

让我们看看各种按钮的样式:

@Composable
fun ButtonDemo() {
    var clickCount by remember { mutableIntStateOf(0) }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
            .verticalScroll(rememberScrollState()),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        Text(
            text = "Button 组件演示",
            style = MaterialTheme.typography.headlineMedium,
            fontWeight = FontWeight.Bold
        )

        Text(
            text = "点击次数: $clickCount",
            style = MaterialTheme.typography.bodyLarge,
            modifier = Modifier.align(Alignment.CenterHorizontally)
        )

        // 基础按钮
        Card(modifier = Modifier.fillMaxWidth()) {
            Column(
                modifier = Modifier.padding(16.dp),
                verticalArrangement = Arrangement.spacedBy(12.dp)
            ) {
                Text(
                    text = "基础按钮",
                    style = MaterialTheme.typography.titleMedium,
                    fontWeight = FontWeight.Bold
                )

                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.spacedBy(8.dp)
                ) {
                    Button(
                        onClick = { clickCount++ },
                        modifier = Modifier.weight(1f)
                    ) {
                        Text("主要按钮")
                    }

                    OutlinedButton(
                        onClick = { clickCount++ },
                        modifier = Modifier.weight(1f)
                    ) {
                        Text("轮廓按钮")
                    }
                }

                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.spacedBy(8.dp)
                ) {
                    TextButton(
                        onClick = { clickCount++ },
                        modifier = Modifier.weight(1f)
                    ) {
                        Text("文本按钮")
                    }

                    ElevatedButton(
                        onClick = { clickCount++ },
                        modifier = Modifier.weight(1f)
                    ) {
                        Text("凸起按钮")
                    }
                }
            }
        }
    }
}

代码讲解:

  1. clickCount 状态:用一个变量记录点击次数,所有按钮都共享这个状态
  2. verticalScroll:让整个 Column 可以滚动,防止内容超出屏幕
  3. Modifier.weight(1f):让两个按钮平分宽度,各占 50%
  4. Arrangement.spacedBy(8.dp):Row 中两个按钮之间间隔 8dp

运行效果:

你可以清楚地看到四种按钮的区别:

  • Button:蓝色填充背景,最显眼
  • OutlinedButton:有边框但透明,次一级
  • TextButton:只有文字,最低调
  • ElevatedButton:有阴影的填充按钮,介于 Button 和 OutlinedButton 之间

2.2 实战示例二:带图标的按钮

按钮不仅可以放文字,还可以放图标:

// 带图标的按钮
Card(modifier = Modifier.fillMaxWidth()) {
    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        Text(
            text = "带图标的按钮",
            style = MaterialTheme.typography.titleMedium,
            fontWeight = FontWeight.Bold
        )

        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            Button(
                onClick = { clickCount++ },
                modifier = Modifier.weight(1f)
            ) {
                Icon(
                    imageVector = Icons.Default.Add,
                    contentDescription = "添加",
                    modifier = Modifier.size(18.dp)
                )
                Spacer(modifier = Modifier.width(4.dp))
                Text("添加")
            }

            OutlinedButton(
                onClick = { clickCount++ },
                modifier = Modifier.weight(1f)
            ) {
                Icon(
                    imageVector = Icons.Default.Edit,
                    contentDescription = "编辑",
                    modifier = Modifier.size(18.dp)
                )
                Spacer(modifier = Modifier.width(4.dp))
                Text("编辑")
            }
        }
    }
}

代码讲解:

  1. Icons.Default.Add:Material Design 提供的内置图标,无需自己画
  2. contentDescription:图标的内容描述,用于无障碍访问,比如屏幕阅读器会读出"添加按钮"
  3. Modifier.size(18.dp):控制图标大小,18dp 是按钮内图标的推荐尺寸
  4. Spacer:在图标和文字之间加一点间距,避免太挤

运行效果:

带图标的按钮更直观,用户一眼就知道这个按钮是干什么的。比如"添加"按钮带一个"+"图标,比纯文字"添加"更容易理解。

2.3 实战示例三:按钮状态控制

有时候我们需要根据条件控制按钮是否可用:

// 不同状态的按钮
Card(modifier = Modifier.fillMaxWidth()) {
    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        Text(
            text = "不同状态的按钮",
            style = MaterialTheme.typography.titleMedium,
            fontWeight = FontWeight.Bold
        )

        Button(
            onClick = { clickCount++ },
            enabled = true,
            modifier = Modifier.fillMaxWidth()
        ) {
            Text("启用状态")
        }

        Button(
            onClick = { /* 禁用状态下无法点击 */ },
            enabled = false,
            modifier = Modifier.fillMaxWidth()
        ) {
            Text("禁用状态")
        }
    }
}

代码讲解:

  1. enabled = true:按钮正常可用,点击会触发 onClick
  2. enabled = false:按钮禁用状态,颜色变灰,点击无效

常见使用场景

  • 表单未填写完成时,提交按钮禁用
  • 正在加载数据时,防止重复点击
  • 用户权限不足时,某些操作不可用

运行效果:

你可以看到,禁用状态的按钮颜色变灰,给用户明确的反馈——这个按钮现在不能点。

2.4 Button 常用 API 详解

API 参数 作用 使用场景 注意事项
onClick: () -> Unit 点击回调 处理用户点击事件 必需参数,点击时触发
enabled: Boolean 是否启用 控制按钮可点击性 false 时按钮变灰且无法点击
colors: ButtonColors 按钮颜色 自定义按钮颜色 使用 ButtonDefaults.buttonColors() 创建
contentPadding: PaddingValues 内容内边距 调整按钮内部间距 影响最小触摸区域,建议保留默认值
modifier: Modifier 修饰符 控制尺寸、位置等 Modifier.fillMaxWidth()
content: @Composable () -> Unit 按钮内容 图标、文字等 必需参数,通常放 Text 和 Icon

2.5 Button 家族成员

Material 3 提供了 5 种按钮,各有各的适用场景:

按钮类型 视觉特点 重要程度 使用场景
Button 填充背景色 ⭐⭐⭐⭐⭐ 主要操作:提交、确认、购买
OutlinedButton 有边框、透明背景 ⭐⭐⭐ 次要操作:取消、上一步
TextButton 只有文字、无背景 ⭐⭐ 低优先级:"了解更多"、"跳过"
ElevatedButton 凸起的填充按钮 ⭐⭐⭐⭐ 需要突出的操作
IconButton 只有图标 ⭐⭐⭐ 图标操作:收藏、分享、设置

按钮选择指南:

  • 一个页面只有一个主操作 → 用 Button
  • 取消、返回等次要操作 → 用 OutlinedButton
  • "了解更多"类低优先级操作 → 用 TextButton
  • 只需要图标的操作 → 用 IconButton

3. TextField 组件 - 文本输入的百宝箱

TextField 是用户输入文本的地方,从搜索框到密码框,从评论框到邮箱输入,都离不开它。

3.1 TextField 常用 API 详解

API 参数 作用 使用场景 注意事项
value: String 输入框的当前内容 显示用户输入的文本 必需参数,需配合状态管理使用
onValueChange: (String) -> Unit 值变化回调 更新输入框状态 必需参数,每次输入都会触发
label: @Composable () -> Unit 浮动标签 说明输入框用途 聚焦时浮动到顶部,如 Text("邮箱")
placeholder: @Composable () -> Unit 占位符 提示用户输入内容 聚焦且有内容时消失,如 Text("请输入邮箱")
leadingIcon: @Composable () -> Unit 前置图标 增强输入框可识别性 如搜索图标、邮箱图标
trailingIcon: @Composable () -> Unit 后置图标 附加操作按钮 如清除按钮、密码显示切换
visualTransformation: VisualTransformation 内容转换 改变显示内容 PasswordVisualTransformation() 用于密码隐藏
keyboardOptions: KeyboardOptions 键盘选项 优化输入体验 KeyboardType.EmailImeAction.Next
isError: Boolean 错误状态 表单验证提示 true 时显示红色边框和错误样式
supportingText: @Composable () -> Unit 辅助文本 错误提示/字符计数 显示在输入框下方

TextField 类型选择:

类型 外观 使用场景 推荐程度
TextField 填充背景 表单中唯一的输入框 ⭐⭐⭐
OutlinedTextField 有边框 表单中有多个输入框 ⭐⭐⭐⭐⭐

推荐使用 OutlinedTextField,因为边框能让用户清楚看到输入区域。

3.2 实战示例一:基础输入框

让我们从最简单的输入框开始:

@Composable
fun TextFieldDemo() {
    var text by remember { mutableStateOf("") }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
            .verticalScroll(rememberScrollState()),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        Text(
            text = "TextField 组件演示",
            style = MaterialTheme.typography.headlineMedium,
            fontWeight = FontWeight.Bold
        )

        // 基础输入框
        Card(modifier = Modifier.fillMaxWidth()) {
            Column(
                modifier = Modifier.padding(16.dp),
                verticalArrangement = Arrangement.spacedBy(12.dp)
            ) {
                Text(
                    text = "基础输入框",
                    style = MaterialTheme.typography.titleMedium,
                    fontWeight = FontWeight.Bold
                )

                OutlinedTextField(
                    value = text,
                    onValueChange = { text = it },
                    label = { Text("基础输入框") },
                    placeholder = { Text("请输入文本") },
                    modifier = Modifier.fillMaxWidth()
                )
            }
        }
    }
}

代码讲解:

  1. value = text:显示输入框的当前内容,这个 text 是一个状态变量
  2. onValueChange = { text = it }:用户每次输入,都会触发这个回调,更新 text 状态
  3. label:浮动标签,当输入框获得焦点或有内容时,标签会浮动到顶部
  4. placeholder:占位符,提示用户该输入什么,内容为空时显示

运行效果:

当你点击输入框时,label 会自动浮动到顶部,这就是 Material Design 的浮动标签效果,非常优雅。

3.3 实战示例二:密码输入框

密码输入框需要隐藏输入内容,通常还有一个"显示/隐藏"按钮:

// 密码输入框
var password by remember { mutableStateOf("") }
var passwordVisible by remember { mutableStateOf(false) }

Card(modifier = Modifier.fillMaxWidth()) {
    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        Text(
            text = "密码输入框",
            style = MaterialTheme.typography.titleMedium,
            fontWeight = FontWeight.Bold
        )

        OutlinedTextField(
            value = password,
            onValueChange = { password = it },
            label = { Text("密码") },
            visualTransformation = if (passwordVisible)
                VisualTransformation.None
            else
                PasswordVisualTransformation(),
            trailingIcon = {
                IconButton(onClick = { passwordVisible = !passwordVisible }) {
                    Icon(
                        imageVector = if (passwordVisible) Icons.Default.Visibility
                                      else Icons.Default.VisibilityOff,
                        contentDescription = if (passwordVisible) "隐藏" else "显示"
                    )
                }
            },
            modifier = Modifier.fillMaxWidth()
        )
    }
}

代码讲解:

  1. passwordVisible 状态:记录密码是否可见
  2. visualTransformation:控制密码的显示方式
    • PasswordVisualTransformation():密码显示为 ••••
    • VisualTransformation.None:正常显示密码
  3. trailingIcon:在输入框右侧添加一个按钮
  4. 眼睛图标切换:根据 passwordVisible 状态切换显示 VisibilityVisibilityOff 图标

运行效果:

点击眼睛图标,密码会在明文和密文之间切换。这是一个非常常见且实用的功能,提升了用户体验。

3.4 实战示例三:带验证的邮箱输入框

表单验证是输入框的重要功能:

// 邮箱输入框(带图标)
var email by remember { mutableStateOf("") }
var isError by remember { mutableStateOf(false) }

Card(modifier = Modifier.fillMaxWidth()) {
    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        Text(
            text = "邮箱",
            style = MaterialTheme.typography.titleMedium,
            fontWeight = FontWeight.Bold
        )

        // 简单的邮箱验证
        val isValidEmail = email.contains("@") && email.contains(".")

        OutlinedTextField(
            value = email,
            onValueChange = {
                email = it
                isError = email.isNotEmpty() && !isValidEmail
            },
            label = { Text("邮箱") },
            leadingIcon = {
                Icon(Icons.Default.Email, contentDescription = "邮箱")
            },
            keyboardOptions = KeyboardOptions(
                keyboardType = KeyboardType.Email,
                imeAction = ImeAction.Next
            ),
            modifier = Modifier.fillMaxWidth()
        )
    }
}

代码讲解:

  1. 邮箱验证逻辑:简单检查是否包含 "@" 和 "."(实际项目中应该用更严格的正则表达式)
  2. leadingIcon:在输入框左侧添加邮箱图标,让用户一眼就知道这是邮箱输入框
  3. keyboardOptions
    • KeyboardType.Email:自动弹出邮箱键盘(带有 @ 符号)
    • ImeAction.Next:键盘的回车键显示为"下一步"

运行效果:


4. FloatingActionButton 组件 - 浮动的魔法按钮

FAB(FloatingActionButton)是 Material Design 的标志性组件,它是一个浮在界面上的圆形按钮,通常用于最重要的操作

4.1 FAB 常用 API 详解

API 参数 作用 使用场景 注意事项
onClick: () -> Unit 点击回调 处理点击事件 必需参数
containerColor: Color 背景颜色 区分操作类型 默认使用主题的 primary 颜色
modifier: Modifier 修饰符 控制尺寸、位置 通常配合 Scaffold 使用
content: @Composable () -> Unit 按钮内容 通常是 Icon 必需参数

FAB 尺寸选择:

类型 尺寸 使用场景
SmallFloatingActionButton 紧凑界面
FloatingActionButton 标准 通用场景
LargeFloatingActionButton 需要强调时
ExtendedFloatingActionButton 扩展 带文字的按钮

4.2 实战示例:FAB

FAB 通常是显示添加某个东西,如视频、图片等等。

@Composable
fun FABDemo() {

    var fabCount by remember { mutableStateOf(0) }
    Text(
          text = "FBA点击次数 $fabCount",
          style = MaterialTheme.typography.titleMedium,
          fontWeight = FontWeight.Bold
     )

    Card(
        modifier = Modifier.fillMaxWidth()
    ) {
        Column(
            modifier = Modifier.padding(16.dp),
            verticalArrangement = Arrangement.spacedBy(12.dp)
        ) {
            Text(
                text = "基础FAB",
                style = MaterialTheme.typography.titleMedium,
                fontWeight = FontWeight.Bold
            )

            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceEvenly,
                verticalAlignment = Alignment.CenterVertically
            ) {
                // 标准 FAB (Primary)
                FloatingActionButton(
                    onClick = { fabCount++ }
                ) {
                    Icon(
                        imageVector = Icons.Default.Add,
                        contentDescription = "添加"
                    )
                }

                // 次要颜色 FAB (Secondary)
                FloatingActionButton(
                    onClick = { fabCount++ },
                    containerColor = MaterialTheme.colorScheme.secondary
                ) {
                    Icon(
                        imageVector = Icons.Default.Edit,
                        contentDescription = "编辑"
                    )
                }

                // 错误色/警告色 FAB (Error)
                FloatingActionButton(
                    onClick = { fabCount++ },
                    containerColor = MaterialTheme.colorScheme.error
                ) {
                    Icon(
                        imageVector = Icons.Default.Delete,
                        contentDescription = "删除"
                    )
                }
            }
        }
    }
}

运行效果:

每次点击右下角的 "+" 按钮,就会添加一个新项目。FAB 浮在内容之上,阴影让它看起来真的"浮"起来了。

FAB 使用场景推荐:

场景 FAB 图标 操作
邮件列表 Icons.Default.Email 写邮件
聊天列表 Icons.Default.Add 新建聊天
待办事项 Icons.Default.Check 添加任务
相册 Icons.Default.Camera 拍照
编辑器 Icons.Default.Save 保存

5. 综合实战 - 完整的登录表单

学了这么多,让我们来个综合实战:做一个漂亮的登录表单!

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginForm() {
    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }
    var passwordVisible by remember { mutableStateOf(false) }
    var agreedToTerms by remember { mutableStateOf(false) }
    var showError by remember { mutableStateOf(false) }
    var isLoading by remember { mutableStateOf(false) }

    val isFormValid = email.isNotBlank() && password.length >= 6 && agreedToTerms

    Box(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        contentAlignment = Alignment.Center
    ) {
        Card(
            modifier = Modifier.fillMaxWidth(),
            elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
        ) {
            Column(
                modifier = Modifier.padding(24.dp),
                verticalArrangement = Arrangement.spacedBy(20.dp)
            ) {
                // 标题
                Text(
                    text = "欢迎回来",
                    style = MaterialTheme.typography.headlineMedium,
                    fontWeight = FontWeight.Bold,
                    modifier = Modifier.align(Alignment.CenterHorizontally)
                )

                // 邮箱输入
                OutlinedTextField(
                    value = email,
                    onValueChange = {
                        email = it
                        showError = false
                    },
                    label = { Text("邮箱地址") },
                    leadingIcon = {
                        Icon(Icons.Default.Email, contentDescription = "邮箱")
                    },
                    isError = showError && email.isBlank(),
                    supportingText = {
                        if (showError && email.isBlank()) {
                            Text(
                                text = "请输入邮箱地址",
                                color = MaterialTheme.colorScheme.error
                            )
                        }
                    },
                    keyboardOptions = KeyboardOptions(
                        keyboardType = KeyboardType.Email,
                        imeAction = ImeAction.Next
                    ),
                    modifier = Modifier.fillMaxWidth()
                )

                // 密码输入
                OutlinedTextField(
                    value = password,
                    onValueChange = {
                        password = it
                        showError = false
                    },
                    label = { Text("密码") },
                    leadingIcon = {
                        Icon(Icons.Default.Lock, contentDescription = "密码")
                    },
                    trailingIcon = {
                        IconButton(onClick = { passwordVisible = !passwordVisible }) {
                            Icon(
                                imageVector = if (passwordVisible) Icons.Default.Visibility
                                              else Icons.Default.VisibilityOff,
                                contentDescription = if (passwordVisible) "隐藏" else "显示"
                            )
                        }
                    },
                    visualTransformation = if (passwordVisible) VisualTransformation.None
                                          else PasswordVisualTransformation(),
                    isError = showError && password.length < 6,
                    supportingText = {
                        if (showError && password.length < 6) {
                            Text(
                                text = "密码至少需要6位字符",
                                color = MaterialTheme.colorScheme.error
                            )
                        }
                    },
                    keyboardOptions = KeyboardOptions(
                        keyboardType = KeyboardType.Password,
                        imeAction = ImeAction.Done
                    ),
                    modifier = Modifier.fillMaxWidth()
                )

                // 同意条款
                Row(
                    modifier = Modifier.fillMaxWidth(),
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    Checkbox(
                        checked = agreedToTerms,
                        onCheckedChange = { agreedToTerms = it }
                    )
                    Spacer(modifier = Modifier.width(8.dp))
                    Text("我同意用户协议")
                }

                // 登录按钮
                Button(
                    onClick = {
                        if (isFormValid) {
                            isLoading = true
                            // 模拟登录请求
                        } else {
                            showError = true
                        }
                    },
                    enabled = !isLoading && isFormValid,
                    modifier = Modifier.fillMaxWidth()
                ) {
                    if (isLoading) {
                        CircularProgressIndicator(
                            modifier = Modifier.size(20.dp),
                            strokeWidth = 2.dp
                        )
                    } else {
                        Text("登录")
                    }
                }

                // 注册链接
                Row(
                    modifier = Modifier.align(Alignment.CenterHorizontally)
                ) {
                    Text("还没有账号?")
                    TextButton(onClick = { }) {
                        Text("立即注册")
                    }
                }
            }
        }
    }
}

代码讲解:

这个登录表单综合运用了今天学的所有组件:

  1. 状态管理:用 6 个状态变量管理表单的各个部分

    • emailpassword:输入内容
    • passwordVisible:密码显示状态
    • agreedToTerms:是否同意条款
    • showError:是否显示错误
    • isLoading:是否正在加载
  2. 表单验证isFormValid 综合判断表单是否有效

    • 邮箱不能为空
    • 密码至少 6 位
    • 必须同意条款
  3. 用户体验优化

    • 输入框有前置图标,清晰标识用途
    • 密码框有显示/隐藏切换
    • 错误时实时显示错误提示
    • 登录按钮只在表单有效时可点击
    • 登录中显示加载动画
  4. 布局技巧

    • 用 Box + contentAlignment = Alignment.Center 让卡片居中
    • Card 添加阴影让它有层次感
    • verticalArrangement = Arrangement.spacedBy(20.dp) 统一间距

运行效果:

PixPin_2026-02-04_13-55-08

这是一个完整可用的登录表单!你可以尝试:

  • 输入错误的邮箱格式,会看到错误提示
  • 密码少于 6 位,会提示密码长度不足
  • 不勾选同意条款,登录按钮不可点击
  • 填写完整后点击登录,按钮会显示加载动画

6. 最佳实践 & 常见坑

6.1 Text 最佳实践

✅ 推荐 ❌ 不推荐
使用 MaterialTheme.typography.xxx 硬编码 fontSize = 18.sp
设置 maxLines 防止溢出 让文字无限延伸
提取字符串到资源文件 直接写死中文
buildAnnotatedString 做富文本 用多个 Text 拼接

6.2 Button 最佳实践

建议 说明
一屏一个主按钮 不要放太多主要按钮,用户会困惑
按钮文字用动词 "登录"、"保存",不要用 "登录按钮"
及时反馈 点击后立即给反馈(加载动画、Toast 等)
保持最小触摸区域 至少 48x48 dp,确保可点击

6.3 TextField 最佳实践

建议 说明
提供清晰的 label 让用户知道该输什么
设置正确的键盘类型 电话号用 Phone,邮箱用 Email
实时验证 不要等提交才报错,输入时就可以提示
添加 contentDescription 方便无障碍访问
使用 OutlinedTextField 多个输入框时更清晰

6.4 FAB 最佳实践

✅ 正确使用 ❌ 错误使用
主要操作(新建、保存) 次要操作(取消、返回)
放在屏幕右下角 放在其他位置
每屏最多一个 FAB 放多个 FAB

6.5 常见坑

坑 1:Modifier 顺序很重要

// ❌ 错误:背景会被 padding 裁剪
Modifier.background(Color.Red).padding(16.dp)

// ✅ 正确:padding 在背景之前
Modifier.padding(16.dp).background(Color.Red)

记住:Modifier 链是从右到左应用的!先应用 padding,再应用 background。

坑 2:记得处理状态

// ❌ 错误:没有更新状态
OutlinedTextField(
    value = text,  // text 永远不变
    onValueChange = { }  // 空实现
)

// ✅ 正确:更新状态
var text by remember { mutableStateOf("") }
OutlinedTextField(
    value = text,
    onValueChange = { text = it }  // 更新状态
)

坑 3:忘记设置键盘类型

// ❌ 忘记设置键盘类型,默认键盘不好用
OutlinedTextField(
    value = phone,
    onValueChange = { phone = it },
    label = { Text("手机号") }
)

// ✅ 设置键盘类型,体验更好
OutlinedTextField(
    value = phone,
    onValueChange = { phone = it },
    label = { Text("手机号") },
    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone)
)

7. 总结

今天我们学习了四个最基础的显示组件:

组件 用途 关键点
Text 显示文字 用 Material Theme 样式,注意溢出处理
Button 用户交互 根据重要程度选择类型,一屏一个主按钮
TextField 文本输入 用 OutlinedTextField,记得验证和设置键盘类型
FAB 主要操作 悬浮在右下角,一屏一个,用于最重要的操作

这些组件就像盖房子的砖块,虽然简单,但无处不在。掌握它们,你就踏出了 Compose 开发的第一步!

你学到了什么?

  • Text 可以显示各种样式的文字,记得用 Material Theme 预设样式
  • Button 有多种类型,根据操作的重要程度选择
  • TextField 需要配合状态管理使用,记得做表单验证
  • FAB 用于最重要的操作,通常配合 Scaffold 使用

下篇预告

基础组件学会了,但怎么把它们组织起来呢?

下一篇,我们将学习布局和容器组件:Card、Surface、Scaffold...它们会把你的组件排列得整整齐齐,让界面既美观又实用。

敬请期待!


本文代码来自 Compose Material 3 组件演示项目,欢迎 Star 收藏!

上一篇Android Compose 00 - 系列导读

posted @ 2026-02-05 09:30  hooong  阅读(3)  评论(0)    收藏  举报