Flutter中实现类似WhatsApp的TextField编号列表功能(Dart代码)
仿WhatsApp编号列表功能的3个Bug修复
我正在实现仿WhatsApp的编号列表功能,通过自定义TextEditingController完成,未使用任何第三方包,但目前存在3个Bug:
- Bug1:输入多位数编号(如10. )后按回车,光标位置计算错误,无法正确定位到新编号后方
- Bug2:按退格键无法正确删除自动生成的编号,退格处理逻辑的字符串截取条件错误
- Bug3:在已有内容的行中间按回车时,无法正确生成连续的编号
原测试页面代码
class TestingPage extends StatefulWidget { const TestingPage({super.key}); @override State<TestingPage> createState() => _TestingPageState(); } class _TestingPageState extends State<TestingPage> { CustomTextEditingController controller = CustomTextEditingController(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Testing"), centerTitle: true, ), body: Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 30), child: TextField( controller: controller, maxLines: 8, decoration: const InputDecoration( hintText: "Enter text here...", border: OutlineInputBorder()), ), ), ), ); } }
原自定义TextEditingController代码
import 'package:flutter/cupertino.dart'; class CustomTextEditingController extends TextEditingController { bool isBackPressed = false; String _previousText = ''; CustomTextEditingController({String text = ''}) : super(text: text) { addListener(_onTextChanged); } // Custom function to handle text changes void _onTextChanged() { String text = this.text; int cursorPosition = selection.baseOffset; if (_previousText.length > text.length) { isBackPressed = true; } else { isBackPressed = false; } _previousText = text; if (cursorPosition > 0) { // Detect Enter key press if (text[cursorPosition - 1] == '\n' && !isBackPressed) { debugPrint("inside12"); String previousLine = _getPreviousLine(text, cursorPosition); RegExp numberRegex = RegExp(r'^(\d+)\.\s'); Match? match = numberRegex.firstMatch(previousLine); if (match != null) { int currentNumber = int.parse(match.group(1)!); String newText = text.replaceRange( cursorPosition, cursorPosition, '${currentNumber + 1}. ', ); value = TextEditingValue( text: newText, selection: TextSelection.collapsed( offset: cursorPosition + currentNumber.toString().length + 2, ), ); } } // Handle backspace to clear line numbering if (text[cursorPosition - 1] == '\n' && cursorPosition > 1 && text.substring(cursorPosition - 4, cursorPosition - 1) == '. ') { debugPrint("inside11"); String newText = text.substring(0, cursorPosition - 4) + text.substring(cursorPosition); value = TextEditingValue( text: newText, selection: TextSelection.collapsed(offset: cursorPosition - 4), ); } } } @override void dispose() { // Clean up the listener when the controller is disposed removeListener(_onTextChanged); super.dispose(); } String _getPreviousLine(String text, int cursorPosition) { int lastNewline = text.lastIndexOf('\n', cursorPosition - 2); if (lastNewline == -1) { return text.substring(0, cursorPosition).trim(); } else { return text.substring(lastNewline + 1, cursorPosition - 1).trim(); } } }
修复后的CustomTextEditingController代码
import 'package:flutter/cupertino.dart'; class CustomTextEditingController extends TextEditingController { bool isBackPressed = false; String _previousText = ''; CustomTextEditingController({String text = ''}) : super(text: text) { addListener(_onTextChanged); } void _onTextChanged() { String text = this.text; int cursorPosition = selection.baseOffset; // 标记是否为退格操作 isBackPressed = _previousText.length > text.length; _previousText = text; if (cursorPosition > 0) { // 处理回车键生成编号 if (text[cursorPosition - 1] == '\n' && !isBackPressed) { String previousLine = _getPreviousLine(text, cursorPosition); RegExp numberRegex = RegExp(r'^(\d+)\.\s'); Match? match = numberRegex.firstMatch(previousLine); if (match != null) { int currentNumber = int.parse(match.group(1)!); String newNumberText = '${currentNumber + 1}. '; String newText = text.replaceRange( cursorPosition, cursorPosition, newNumberText, ); // 光标位置设为插入后的位置,用新编号的长度计算 value = TextEditingValue( text: newText, selection: TextSelection.collapsed( offset: cursorPosition + newNumberText.length, ), ); } } // 处理退格删除自动生成的编号 else if (isBackPressed) { // 检查光标是否在自动生成的编号后方(换行后的位置) if (cursorPosition >= 3 && text.substring(cursorPosition - 3, cursorPosition).endsWith('. ')) { // 尝试匹配前面的数字 RegExp numberRegex = RegExp(r'(\d+)\.\s$'); String beforeCursor = text.substring(0, cursorPosition); Match? match = numberRegex.firstMatch(beforeCursor); if (match != null) { // 删除整个编号部分 String newText = text.substring(0, cursorPosition - match.group(0)!.length) + text.substring(cursorPosition); value = TextEditingValue( text: newText, selection: TextSelection.collapsed(offset: cursorPosition - match.group(0)!.length), ); } } } } } @override void dispose() { removeListener(_onTextChanged); super.dispose(); } String _getPreviousLine(String text, int cursorPosition) { int lastNewline = text.lastIndexOf('\n', cursorPosition - 2); if (lastNewline == -1) { return text.substring(0, cursorPosition - 1).trim(); } else { return text.substring(lastNewline + 1, cursorPosition - 1).trim(); } } }
修复说明
- Bug1修复:原代码用当前编号的长度计算光标位置,适配多位数编号时逻辑错误,改为直接使用新生成的编号字符串长度计算光标位置,确保光标始终在新编号后方。
- Bug2修复:原退格逻辑的字符串截取条件错误(截取3个字符匹配2个字符的". "),改为先检查光标前内容是否以". "结尾,再匹配完整数字编号格式,动态获取编号长度进行删除,适配任意位数的编号。
- Bug3修复:调整
_getPreviousLine方法,当光标在第一行时,截取范围排除刚输入的换行符,确保正确获取上一行的完整编号内容;同时优化退格逻辑触发条件,仅在退格操作时处理编号删除,避免与回车逻辑冲突。
内容的提问来源于stack exchange,提问作者Shruti Ramnandan Sharma
相关产品推荐
相关产品推荐

