Flutter自定义OTP组件:索引越界错误与退格键处理问题
Flutter自定义OTP组件问题修复方案
问题1:索引越界错误修复
错误原因
_updateParentController方法处理父控制器文本时,未考虑父文本长度小于目标索引的情况,直接调用substring触发越界;同时_fieldControllers的监听器与_updateOtp形成循环调用,加剧了越界风险。
修复步骤
- 移除子控制器监听器:删除
initState中给_fieldControllers添加监听器的代码,避免父、子控制器循环触发。 - 安全处理字符串截取:修改
_updateParentController,确保所有截取操作不超出文本实际长度:void _updateParentController(int index) { final text = _fieldControllers[index].text; final currentText = widget.controller.text; if (index >= 0 && index < widget.length) { String newText; // 安全获取前缀,限制截取范围不超过文本长度 final prefix = index > 0 ? currentText.substring(0, index.clamp(0, currentText.length)) : ''; // 后缀截取起始索引超出长度时返回空串 final suffix = (index + 1 < currentText.length) ? currentText.substring(index + 1) : ''; if (text.isNotEmpty) { newText = prefix + text[0] + suffix; } else { newText = prefix + suffix; } // 确保新文本长度不超过OTP设定长度 if (newText.length > widget.length) { newText = newText.substring(0, widget.length); } widget.controller.text = newText; widget.controller.selection = TextSelection.fromPosition( TextPosition(offset: newText.length), ); } }
问题2:退格键行为异常修复
错误原因
原onChanged逻辑仅处理焦点切换,未针对退格操作做字段清空逻辑,导致空字段按退格时无法正确清除前一个字段内容,焦点行为不符合预期。
修复步骤
修改_buildOtpField中的onChanged回调,优化退格逻辑:
onChanged: (value) { if (value.length <= 1) { if (value.isNotEmpty) { // 输入字符后自动跳转到下一个字段 if (index < widget.length - 1) { _focusNodes[index + 1].requestFocus(); } } else { // 当前字段为空时的退格处理 if (index > 0) { final prevController = _fieldControllers[index - 1]; // 前一个字段有内容则先清空 if (prevController.text.isNotEmpty) { prevController.clear(); _updateParentController(index - 1); } // 移动焦点到前一个字段 _focusNodes[index - 1].requestFocus(); } } _updateParentController(index); } },
优化后的退格行为
- 当前字段有内容时,按下退格:清空当前字段,焦点留在当前。
- 当前字段为空时,按下退格:若不是第一个字段,清空前一个字段并移焦点过去;若为第一个字段,无操作。
完整修改后的代码
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class CustomOtpField extends StatefulWidget { final TextEditingController controller; final int length; final double size; final double borderRadius; final double borderWidth; final Color focusedBorderColor; final Color unfocusedBorderColor; final Color backgroundColor; final TextStyle textStyle; final double fieldSpacing; const CustomOtpField({ required this.controller, this.length = 6, this.size = 50.0, this.borderRadius = 4.0, this.borderWidth = 1.0, this.focusedBorderColor = Colors.teal, this.unfocusedBorderColor = Colors.grey, this.backgroundColor = Colors.white, this.textStyle = const TextStyle(fontSize: 20.0, color: Colors.black), this.fieldSpacing = 12.0, super.key, }); @override CustomOtpFieldState createState() => CustomOtpFieldState(); } class CustomOtpFieldState extends State<CustomOtpField> { late List<FocusNode> _focusNodes; late List<TextEditingController> _fieldControllers; @override void initState() { super.initState(); _focusNodes = List.generate(widget.length, (index) => FocusNode()); _fieldControllers = List.generate(widget.length, (index) => TextEditingController()); widget.controller.addListener(_updateOtp); WidgetsBinding.instance.addPostFrameCallback((_) { if (_focusNodes.isNotEmpty) { _focusNodes[0].requestFocus(); } }); } @override void dispose() { for (var node in _focusNodes) { node.dispose(); } for (var controller in _fieldControllers) { controller.dispose(); } widget.controller.removeListener(_updateOtp); super.dispose(); } void _updateParentController(int index) { final text = _fieldControllers[index].text; final currentText = widget.controller.text; if (index >= 0 && index < widget.length) { String newText; final prefix = index > 0 ? currentText.substring(0, index.clamp(0, currentText.length)) : ''; final suffix = (index + 1 < currentText.length) ? currentText.substring(index + 1) : ''; if (text.isNotEmpty) { newText = prefix + text[0] + suffix; } else { newText = prefix + suffix; } if (newText.length > widget.length) { newText = newText.substring(0, widget.length); } widget.controller.text = newText; widget.controller.selection = TextSelection.fromPosition( TextPosition(offset: newText.length), ); } } void _updateOtp() { final text = widget.controller.text; if (text.length > widget.length) { widget.controller.text = text.substring(0, widget.length); return; } if (text.length == widget.length) { _focusNodes.last.unfocus(); } else if (text.length < widget.length && text.length < _focusNodes.length) { _focusNodes[text.length].requestFocus(); } for (int i = 0; i < widget.length; i++) { if (i < text.length) { if (_fieldControllers[i].text != text[i]) { _fieldControllers[i].text = text[i]; } } else { if (_fieldControllers[i].text.isNotEmpty) { _fieldControllers[i].clear(); } } } } @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(widget.length, (index) { return Row( children: [ _buildOtpField(index), if (index < widget.length - 1) SizedBox(width: widget.fieldSpacing), ], ); }), ); } Widget _buildOtpField(int index) { return SizedBox( height: widget.size, width: widget.size, child: TextField( focusNode: _focusNodes[index], controller: _fieldControllers[index], onChanged: (value) { if (value.length <= 1) { if (value.isNotEmpty) { if (index < widget.length - 1) { _focusNodes[index + 1].requestFocus(); } } else { if (index > 0) { final prevController = _fieldControllers[index - 1]; if (prevController.text.isNotEmpty) { prevController.clear(); _updateParentController(index - 1); } _focusNodes[index - 1].requestFocus(); } } _updateParentController(index); } }, textAlign: TextAlign.center, maxLength: 1, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], decoration: InputDecoration( counterText: '', contentPadding: EdgeInsets.zero, filled: true, fillColor: widget.backgroundColor, border: OutlineInputBorder( borderRadius: BorderRadius.circular(widget.borderRadius), borderSide: BorderSide( color: _focusNodes[index].hasFocus ? widget.focusedBorderColor : widget.unfocusedBorderColor, width: widget.borderWidth, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(widget.borderRadius), borderSide: BorderSide( color: widget.focusedBorderColor, width: widget.borderWidth, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(widget.borderRadius), borderSide: BorderSide( color: widget.unfocusedBorderColor, width: widget.borderWidth, ), ), ), style: widget.textStyle, ), ); } }
内容的提问来源于stack exchange,提问作者Mirza
相关产品推荐
相关产品推荐

