TextField 与基本输入:单行输入、占位符、文本变化监听、键盘选项配置

原理概览:声明式文本输入的核心机制

TextField 在 Jetpack Compose 中是一个“受控组件”,它的状态由调用方持有并驱动。与传统的编辑框不同,Compose 的 TextField 不维护自身内部可变状态,而是通过 value: String 参数接收当前文本,并通过 onValueChange: (String) -> Unit 回调将每次用户操作后的新值通知给调用方。这种单向数据流设计保证了 UI 的确定性——你总是可以看到文本的当前值是如何从上一帧的状态演变而来的。

关键理解:TextField 的 value 和 onValueChange 必须配对使用。如果只提供 value 而不处理 onValueChange,TextField 将变成只读;如果只处理 onValueChange 而不更新 value,用户输入将无法反映到界面上。

以下是 TextField 内部状态更新流程的 SVG 图,展示了用户输入如何通过回调改变组合树中的数据源:

TextField 声明式状态更新流程 1. 用户输入 2. onValueChange 3. 更新 state → 重组 4. 界面刷新 循环 单向数据流说明 • 文本值始终存放在外部 • onValueChange 是唯一 修改入口 • 不要修改回调的参数 • 重组只在状态变化时触发

组件分类与 API 选择

Compose 提供了两种文本输入组件,适用场景不同:

组件 适用场景 关键区别
TextField Material Design 风格输入框,带下划线或填充容器 默认有边框/填充,可配置视觉样式
BasicTextField 完全自定义外观,不带任何 Material 主题依赖 无默认装饰,所有样式需手动设置

本章聚焦 TextField,因为它内置了占位符、错误提示、键盘类型等实用参数,适合大部分业务场景。

核心参数与典型用法

1. 基本单行输入 + 占位符

占位符通过 placeholder 参数设置,当输入框为空时显示。注意这是 @Composable 参数,可以渲染任意组件:

@Composable
fun SimpleInput() {
    var text by remember { mutableStateOf("") }
    TextField(
        value = text,
        onValueChange = { text = it },
        placeholder = { Text("请输入用户名") },
        modifier = Modifier.fillMaxWidth()
    )
}
常见错误:如果 remember 的 mutableStateOf 没有被 by 委托,你需要在赋值时手动调用 .value,例如 text.value = it。使用 by 委托可避免这类语法噪音。

2. 文本变化监听与过滤

在 onValueChange 中可以对输入进行实时拦截和转换,实现诸如只允许数字输入、限定长度等效果:

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

    TextField(
        value = text,
        onValueChange = { newValue ->
            // 只保留数字
            val filtered = newValue.filter { it.isDigit() }
            // 限定长度 6
            if (filtered.length <= 6) {
                text = filtered
            }
        },
        placeholder = { Text("6位验证码") },
        modifier = Modifier.fillMaxWidth()
    )
}

这种“在回调中加工”的模式非常灵活,但需要注意:

  • 不要在 onValueChange 中直接修改 newValue(它是不可变的),必须构造新字符串。
  • 过滤逻辑要足够快,避免阻塞 UI 线程——复杂的正则建议用 remember 缓存。

3. 键盘选项配置

通过 keyboardOptions 参数可以控制软键盘的类型、行为,以及操作按钮的标签。以下表格列出了常用选项:

KeyboardType 说明
KeyboardType.Text默认文本键盘
KeyboardType.AsciiASCII 字符键盘(适合英文输入)
KeyboardType.Decimal带小数点的数字键盘
KeyboardType.Email包含 @ 和 . 的键盘
KeyboardType.Number纯数字键盘
KeyboardType.Password密码模式(隐藏输入)
KeyboardType.UriURL 输入优化键盘

再加上 ImeAction 配置操作按钮(如“完成”、“搜索”、“发送”),可以实现更自然的键盘交互:

@Composable
fun KeyboardConfigDemo() {
    var phone by remember { mutableStateOf("") }

    OutlinedTextField(
        value = phone,
        onValueChange = { phone = it.filter { it.isDigit() } },
        label = { Text("手机号") },
        keyboardOptions = KeyboardOptions(
            keyboardType = KeyboardType.Number,
            imeAction = ImeAction.Done   // 键盘右下角显示"完成"
        ),
        keyboardActions = KeyboardActions(
            onDone = {
                // 收起键盘,执行提交逻辑
                // 例如:focusManager.clearFocus()
                println("提交手机号: $phone")
            }
        ),
        singleLine = true
    )
}
性能提示:对于高频输入场景(如实时搜索),建议在 onValueChange 中使用 snapshotFlow 或 debounce 来降低重组频率。但本章基础环境不需要引入额外库,只需理解回调时机即可。

4. 单行输入与多行控制

默认情况下 TextField 会根据内容自动换行。通过设置 singleLine = true 可强制单行:

TextField(
    value = text,
    onValueChange = { text = it },
    singleLine = true,       // 强制单行,输入超过宽度时水平滚动
    maxLines = 1             // 与 singleLine 等效,但 singleLine 更语义化
)

常见错误与调试

  • 错误:输入无反应 —— 检查 value 是否确实通过 onValueChange 更新了。最常发生在忘记写 mutableStateOf 或忘记 by 委托。
  • 错误:键盘弹出后遮挡输入框 —— 确保 Activity 的 android:windowSoftInputMode 设置为 adjustResize 或 adjustPan,并在 Compose 中使用 Modifier.imePadding()。
  • 调试技巧:在 onValueChange 中打印日志,确认每次输入是否触发了正确的状态更新。

完整示例:综合应用

下面是一个包含占位符、过滤、单行、支持不同键盘类型的登录表单片段:

@Composable
fun LoginForm() {
    var username by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }

    Column(modifier = Modifier.padding(16.dp)) {
        TextField(
            value = username,
            onValueChange = { username = it },
            placeholder = { Text("邮箱或手机号") },
            keyboardOptions = KeyboardOptions(
                keyboardType = KeyboardType.Email
            ),
            singleLine = true,
            modifier = Modifier.fillMaxWidth()
        )
        Spacer(modifier = Modifier.height(12.dp))
        TextField(
            value = password,
            onValueChange = { password = it },
            placeholder = { Text("密码") },
            visualTransformation = PasswordVisualTransformation(),
            keyboardOptions = KeyboardOptions(
                keyboardType = KeyboardType.Password,
                imeAction = ImeAction.Done
            ),
            keyboardActions = KeyboardActions(
                onDone = { /* 执行登录 */ }
            ),
            singleLine = true,
            modifier = Modifier.fillMaxWidth()
        )
    }
}
视觉变换:visualTransformation 参数用来改变文本的显示方式而不改变实际文本值。常用的有 PasswordVisualTransformation()(密码掩码)和 VisualTransformation.None。如果你需要自定义变换(如信用卡号分段),可以继承 VisualTransformation 接口。

状态管理对比:remember vs rememberSaveable

当 Activity 重建(如屏幕旋转)时,remember 会丢失状态;rememberSaveable 会将状态保存到 Bundle 中自动恢复。对于文本输入框,推荐使用 rememberSaveable:

var text by rememberSaveable { mutableStateOf("") }

内部实现原理:rememberSaveable 通过 Saver 对象将 String 写入 SavedStateHandle,然后从 Bundle 恢复。对于基本类型(String、Int 等)它可以直接使用;涉及复杂对象需要自定义 Saver。


本章学习了 TextField 的声明式数据流、占位符与单行控制、实时过滤监听、键盘类型与操作按钮配置,以及 remember 与 rememberSaveable 的状态持久化区别。下一阶段,你将接触 OutlinedTextField 与 DecorationBox 更深度的自定义模式。