You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Compose OTP输入框快速输入时内容丢失问题排查

OTP输入框快速输入内容丢失问题的排查与修复

原代码存在的核心问题

  • 硬编码状态列表长度:remember中固定创建6个mutableStateOf(TextFieldValue),但组件参数支持自定义otpLength,当otpLength不等于6时会出现状态不匹配,同时快速输入时易引发状态索引混乱。
  • 焦点移动的异步竞态:直接在onValueChange中调用focusManager.moveFocus,Compose的状态更新是异步的,快速输入时焦点切换可能未完成就收到下一个输入事件,导致输入内容错位或丢失。
  • 多字符输入处理逻辑不严谨:当用户快速输入多个字符时,仅取最后一个字符设置到下一个输入框,未考虑连续输入的状态堆叠,容易跳过部分输入内容。
  • 按键事件拦截过度:onKeyEvent始终返回true,阻止了系统默认的按键处理逻辑,可能干扰Backspace等按键的正常行为。

修复后的代码

@Composable
@Preview
fun OtpLayout(modifier: Modifier = Modifier, otpLength: Int = 6) {
    Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = modifier.fillMaxWidth()) {
        val focusManager = LocalFocusManager.current
        // 根据otpLength动态创建状态列表,避免硬编码
        val otpStates = remember(otpLength) {
            List(otpLength) { mutableStateOf(TextFieldValue(text = "", selection = TextRange(0))) }
        }

        repeat(otpLength) { index ->
            key(index) {
                BasicTextField(
                    modifier = Modifier.onKeyEvent { event ->
                        if (event.key == Key.Backspace && event.type == KeyEventType.KeyUp) {
                            val currentValue = otpStates[index].value
                            if (currentValue.text.isEmpty() && index > 0) {
                                focusManager.moveFocus(FocusDirection.Left)
                                // 仅处理该事件时返回true,其余交给系统处理
                                return@onKeyEvent true
                            }
                        }
                        false
                    },
                    maxLines = 1,
                    value = otpStates[index].value,
                    textStyle = TextStyle(color = Color.White, fontSize = 16.sp),
                    keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
                    onValueChange = { newValue ->
                        // 仅允许数字输入
                        if (!newValue.text.isDigitsOnly()) return@onValueChange

                        val currentText = newValue.text
                        // 处理单个字符输入场景
                        if (currentText.length <= 1) {
                            otpStates[index].value = newValue
                            // 输入非空且不是最后一个框,异步移动焦点
                            if (currentText.isNotEmpty() && index < otpLength - 1) {
                                LaunchedEffect(Unit) {
                                    focusManager.moveFocus(FocusDirection.Right)
                                }
                            }
                            // 输入清空且不是第一个框,异步移动焦点
                            else if (currentText.isEmpty() && index > 0) {
                                LaunchedEffect(Unit) {
                                    focusManager.moveFocus(FocusDirection.Left)
                                }
                            }
                        } else {
                            // 处理多字符快速输入,拆分字符填充到后续框
                            val remainingChars = currentText.toList()
                            otpStates[index].value = newValue.copy(text = remainingChars.first().toString())
                            // 依次填充后续输入框
                            for (i in index + 1 until otpLength) {
                                if (i - index > remainingChars.size - 1) break
                                otpStates[i].value = TextFieldValue(
                                    text = remainingChars[i - index].toString(),
                                    selection = TextRange(1)
                                )
                            }
                            // 移动焦点到最后一个被填充的框
                            val targetIndex = min(index + remainingChars.size, otpLength - 1)
                            LaunchedEffect(Unit) {
                                repeat(targetIndex - index) {
                                    focusManager.moveFocus(FocusDirection.Right)
                                }
                            }
                        }
                    },
                    decorationBox = { innerTextField ->
                        Box(
                            contentAlignment = Alignment.Center,
                            modifier = Modifier
                                .size(width = 42.dp, height = 52.dp)
                                .border(
                                    width = 1.dp,
                                    color = PrimaryColor,
                                    shape = RoundedCornerShape(6.dp)
                                )
                                .background(color = Gray, shape = RoundedCornerShape(6.dp))
                        ) {
                            innerTextField()
                        }
                    }
                )
            }
        }
    }
}

关键改动说明

  1. 动态状态列表:根据otpLength参数创建对应长度的状态列表,确保状态与UI输入框数量完全匹配。
  2. 异步焦点处理:使用LaunchedEffect包裹焦点移动逻辑,确保状态更新完成后再执行焦点切换,解决快速输入时的异步竞态问题。
  3. 完善多字符输入处理:当用户快速输入多个字符时,自动拆分字符并填充到后续OTP输入框,同时将焦点定位到最后一个被填充的框,避免内容丢失。
  4. 合理拦截按键事件:仅在处理Backspace清空当前框并需要左移焦点时返回true,其余情况交给系统处理默认逻辑,避免干扰正常输入行为。

内容的提问来源于stack exchange,提问作者sarthak gupta

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 00:25:31