第3章 输入框组件实现:OutlinedTextField定制、键盘类型设置、焦点管理、输入过滤器
支付页面中,输入框承担卡号、金额、CVV等关键数据的采集任务。Compose 的 OutlinedTextField 提供了足够的定制空间,但需要结合支付场景对键盘类型、焦点流动、输入格式做精细化控制。本章从原理到代码,逐一解锁这些能力。
3.1 核心原理
OutlinedTextField 的本质是一个围绕 BasicTextField 包装的 Material 组件,其状态持有(value / onValueChange)、视觉层(Modifier 链 + 外观参数)与行为层(键盘选项、焦点、过滤器)分离。在支付页面中,我们重点操作以下模块:
- 键盘类型:通过
KeyboardOptions(keyboardType)指定数字、电话、密码键盘。 - 焦点管理:利用
FocusRequester和FocusManager实现顺序跳转和失焦验证。 - 输入过滤器:在
onValueChange中做正则校验或字符白名单,阻止无效输入进入状态。
性能要点: 避免在
onValueChange 中执行昂贵操作(如网络请求或频繁正则编译)。使用 remember { Regex() } 缓存正则实例。
3.2 实现流程
以支付页面常见的“金额输入框”为例,按以下步骤构建:
- 创建状态:
var amount by remember { mutableStateOf("") } - 定义过滤器:允许数字和小数点,但不允许多个小数点或非数字字符。
- 配置键盘:
KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Next) - 焦点控制:声明
val focusRequester = remember { FocusRequester() },绑定到Modifier.focusRequester(focusRequester),并在LaunchedEffect中请求焦点。 - 美化外观:通过
OutlinedTextField的colors、shape、label参数对齐品牌风格。
关键代码:金额输入框完整实现
@Composable
fun AmountInput(
onAmountChanged: (String) -> Unit,
focusRequester: FocusRequester
) {
var amount by remember { mutableStateOf("") }
val filter = remember {
{ input: String ->
input.isEmpty() || Regex("^\\d*\\.?\\d{0,2}$").matches(input)
}
}
OutlinedTextField(
value = amount,
onValueChange = { newValue ->
if (filter(newValue)) {
amount = newValue
onAmountChanged(newValue)
}
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Decimal,
imeAction = ImeAction.Next
),
keyboardActions = KeyboardActions(
onNext = { /* 跳转到下一个输入框 */ }
),
singleLine = true,
label = { Text("金额") },
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester)
.focusProperties { next = focusRequester },
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline
),
shape = RoundedCornerShape(8.dp)
)
}
3.3 焦点管理深入
支付表单通常有多个输入框(卡号、有效期、CVV),需要使用 FocusRequester 链式控制跳转。核心步骤:
- 为每个输入框创建独立的
FocusRequester实例。 - 在
KeyboardActions(onNext)中调用下一个focusRequester.requestFocus()。 - 在
LaunchedEffect(Unit)中对第一个输入框自动请求焦点。 - 注意:
FocusRequester必须在Composable作用域内通过remember创建,且不能在同一层级重复使用同一实例。
3.4 输入过滤器常见模式
| 场景 | 正则/策略 | 说明 |
|---|---|---|
| 仅数字 | ^\\d*$ | 卡号、CVV |
| 数字 + 小数点后两位 | ^\\d*\\.?\\d{0,2}$ | 金额 |
| 禁止空格 | !newValue.contains(" ") | 密码、卡号分段 |
| 最大长度 | newValue.length <= maxLen | 防止超长输入 |
避免无限重组: 不要直接在
onValueChange 中创建新 Regex 对象或在 LaunchedEffect 外调用 focusRequester.requestFocus(),否则会引起重组风暴。
3.5 内联架构图
下图展示了输入框组件在支付页面中的关键数据流与控制点:
3.6 调试与常见错误
调试方法
- 使用 Layout Inspector 观察焦点颜色变化,确认
focusRequester生效。 - 在
onValueChange中打印原始输入,检查过滤器是否误杀合法字符。 - 利用
Compose Compiler Metrics检查重组次数,定位不必要的重组。
常见错误
- 键盘类型不生效:确保
keyboardOptions以参数形式传入,而不是在Modifier中设置(不生效)。 - 焦点丢失:
FocusRequester未与Modifier.focusRequester()绑定,或同一实例被多个输入框共用。 - 输入过滤器导致卡顿:在
onValueChange中创建了新的Regex对象。使用remember缓存正则。 - 小数点输入异常:国际键盘差异(逗号 vs 小数点)。统一在过滤器中替换逗号为点。
3.7 性能与稳定性优化
| 优化点 | 实现方式 | 收益 |
|---|---|---|
| 状态延迟初始化 | 使用 mutableStateOf 配合 derivedStateOf | 减少额外重组 |
| 过滤器预编译 | remember { Regex(pattern) } | 避免重复编译 |
| 焦点委托延迟 | 在 LaunchedEffect 中请求焦点,而非直接调用 | 确保布局完成 |
| 防抖动输入 | 使用 snapshotFlow 结合 debounce | 减少实时校验频率 |
核心原则: 输入框组件的稳定性取决于状态管理的 purity(纯函数性)。所有副作用(焦点请求、日志、网络)必须放到
LaunchedEffect 或 snapshotFlow 中处理。
通过本节的实现,你已经掌握了支付页面核心输入框的定制方法。下一部分将把这些输入框组合成完整的表单,并加入验证逻辑。