4. 数字输入框设计:不可见输入框、密码掩码样式、光标隐藏、监听输入变化
数字输入框是支付键盘的数据交互核心。在防录屏场景中,输入框不能直接显示明文数字,必须同时满足:不可被录屏软件捕获、视觉上显示密码掩码、隐藏光标消除位置泄漏以及实时监听输入变化用于后续校验。本章通过四个设计要点,构建一个安全的数字输入组件。
4.1 核心原理
传统的 TextField 或 OutlinedTextField 在录屏时会将输入内容和光标位置暴露在屏幕录制流中。防录屏输入框需要完成以下转换:
| 设计要求 | 原理 | 录屏暴露风险 |
|---|---|---|
| 不可见输入框 | 使用透明绘制或零尺寸输入框,内容不被录屏捕获 | 高 |
| 密码掩码样式 | 将每个数字替换为"●"或"*",视觉隐藏真实内容 | 中 |
| 光标隐藏 | 设置透明光标或禁止显示光标 | 中 |
| 监听输入变化 | 通过回调或状态管理实时获取输入值 | 低 |
知识点: 在Compose中,
BasicTextField 提供了更底层的控制能力,允许开发者完全自定义视觉表现,是实现防录屏输入框的首选基础组件。
4.2 实现流程
以下步骤指导你构建一个完整的防录屏数字输入框:
- 创建基础输入状态:使用
mutableStateOf管理输入字符串。 - 配置不可见输入框:设置
BasicTextField的modifier、textStyle为透明,确保视觉层不显示内容。 - 实现密码掩码样式:通过
visualTransformation将每个字符替换为掩码符号。 - 隐藏光标:设置
cursorBrush为透明,或者通过Modifier.pointerInput阻止光标显示。 - 监听输入变化:在
onValueChange回调中处理输入值。 - 提取数字并限制长度:过滤非数字字符,并设置最大输入长度。
最佳实践: 将输入框自身设为
alpha(0f) 或 Modifier.graphicsLayer { alpha = 0f },使其完全透明,但功能保持正常。这样录屏软件无法捕获任何像素信息。
4.3 核心代码实现
完整的数字输入框组件代码如下:
@Composable
fun SecureDigitInput(
onInputChanged: (String) -> Unit,
modifier: Modifier = Modifier
) {
var input by remember { mutableStateOf("") }
val maxLength = 6 // 示例:6位支付密码
// 过滤非数字字符
val filteredInput by derivedStateOf {
input.filter { it.isDigit() }.take(maxLength)
}
Box(
modifier = modifier.fillMaxWidth().height(56.dp),
contentAlignment = Alignment.Center
) {
// 不可见输入框层
BasicTextField(
value = filteredInput,
onValueChange = { newValue ->
input = newValue
onInputChanged(filteredInput)
},
modifier = Modifier
.fillMaxSize()
.graphicsLayer { alpha = 0f }, // 关键:完全透明
textStyle = TextStyle(
color = Color.Transparent,
fontSize = 1.sp // 极小字号确保零像素
),
cursorBrush = SolidColor(Color.Transparent), // 隐藏光标
visualTransformation = PasswordVisualTransformation('●'),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
singleLine = true
)
// 视觉掩码显示层
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.Center
) {
filteredInput.forEach { _ ->
Text(
text = "●",
fontSize = 32.sp,
color = Color.Black,
modifier = Modifier.padding(horizontal = 8.dp)
)
}
}
}
}
常见错误: 如果直接使用
TextField 代替 BasicTextField,可能无法完全控制光标和透明度,导致录屏软件仍能捕获到输入内容。务必使用 BasicTextField 以获得底层控制权。
4.4 调试与验证
验证输入框的防录屏效果:
- 录屏测试:在真机上启动录屏,输入若干数字,回放录屏视频检查是否出现数字或光标痕迹。
- 光标检测:截屏或录屏中应无光标闪烁痕迹。可通过设置
cursorBrush = SolidColor(Color.Transparent)彻底隐藏。 - 输入监听日志:在
onInputChanged中添加临时日志输出,确保每次输入变化都正确传递。
// 调试日志示例
onInputChanged = { value ->
Log.d("SecureInput", "当前输入: $value")
}
4.5 常见错误
| 错误表现 | 原因 | 解决方案 |
|---|---|---|
| 录屏仍捕获到数字 | 输入框未完全透明,或使用了 TextField |
改用 BasicTextField 并设置 alpha = 0f |
| 光标仍然可见 | cursorBrush 未正确设置为透明 |
使用 SolidColor(Color.Transparent) |
| 用户输入无效 | 输入过滤导致输入顺序错乱 | 确保 onValueChange 内只更新合法字符 |
| 视觉掩码与输入不同步 | 未使用 derivedStateOf 或 snapshotFlow |
使用 derivedStateOf 保持同步 |
4.6 内联 SVG:数字输入框核心设计模式
4.7 性能与稳定性优化
为了确保输入框在高频输入和复杂场景下的稳定性,请遵循以下优化建议:
- 避免不必要的重组:使用
derivedStateOf只让掩码层在输入变化时重组,而不是每次 UI 帧都重绘。 - 限制输入长度:在过滤器中直接
.take(maxLength),避免状态膨胀。 - 使用
snapshotFlow处理副作用:如需发送输入事件到 ViewModel,使用LaunchedEffect配合snapshotFlow监听变化。 - 透明度层的性能权衡:
graphicsLayer { alpha = 0f }会触发硬件加速,适用于大多数设备。若性能受限,可改用Modifier.alpha(0f)。
进阶: 如果你需要输入框在不可见状态下仍然支持触摸事件穿透测试,可以添加
Modifier.clickable 在透明层上方处理点击,然后通过 FocusRequester 手动请求焦点给 BasicTextField。