4. 数字输入框设计:不可见输入框、密码掩码样式、光标隐藏、监听输入变化

数字输入框是支付键盘的数据交互核心。在防录屏场景中,输入框不能直接显示明文数字,必须同时满足:不可被录屏软件捕获、视觉上显示密码掩码、隐藏光标消除位置泄漏以及实时监听输入变化用于后续校验。本章通过四个设计要点,构建一个安全的数字输入组件。

4.1 核心原理

传统的 TextField 或 OutlinedTextField 在录屏时会将输入内容和光标位置暴露在屏幕录制流中。防录屏输入框需要完成以下转换:

设计要求 原理 录屏暴露风险
不可见输入框 使用透明绘制或零尺寸输入框,内容不被录屏捕获 高
密码掩码样式 将每个数字替换为"●"或"*",视觉隐藏真实内容 中
光标隐藏 设置透明光标或禁止显示光标 中
监听输入变化 通过回调或状态管理实时获取输入值 低
知识点: 在Compose中,BasicTextField 提供了更底层的控制能力,允许开发者完全自定义视觉表现,是实现防录屏输入框的首选基础组件。

4.2 实现流程

以下步骤指导你构建一个完整的防录屏数字输入框:

  1. 创建基础输入状态:使用 mutableStateOf 管理输入字符串。
  2. 配置不可见输入框:设置 BasicTextField 的 modifier、textStyle 为透明,确保视觉层不显示内容。
  3. 实现密码掩码样式:通过 visualTransformation 将每个字符替换为掩码符号。
  4. 隐藏光标:设置 cursorBrush 为透明,或者通过 Modifier.pointerInput 阻止光标显示。
  5. 监听输入变化:在 onValueChange 回调中处理输入值。
  6. 提取数字并限制长度:过滤非数字字符,并设置最大输入长度。
最佳实践: 将输入框自身设为 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:数字输入框核心设计模式

数字输入框核心设计模式 不可见输入框 BasicTextField alpha=0f 密码掩码样式 visualTransformation "●" 替换 光标隐藏 cursorBrush Color.Transparent 监听输入变化 onValueChange 回调通知 输入过滤 digit 过滤 长度限制 输入流:用户输入 → 不可见捕获 → 过滤 → 掩码显示 + 输入回调

4.7 性能与稳定性优化

为了确保输入框在高频输入和复杂场景下的稳定性,请遵循以下优化建议:

  • 避免不必要的重组:使用 derivedStateOf 只让掩码层在输入变化时重组,而不是每次 UI 帧都重绘。
  • 限制输入长度:在过滤器中直接 .take(maxLength),避免状态膨胀。
  • 使用 snapshotFlow 处理副作用:如需发送输入事件到 ViewModel,使用 LaunchedEffect 配合 snapshotFlow 监听变化。
  • 透明度层的性能权衡:graphicsLayer { alpha = 0f } 会触发硬件加速,适用于大多数设备。若性能受限,可改用 Modifier.alpha(0f)。
进阶: 如果你需要输入框在不可见状态下仍然支持触摸事件穿透测试,可以添加 Modifier.clickable 在透明层上方处理点击,然后通过 FocusRequester 手动请求焦点给 BasicTextField。