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

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();
    }
  }
}

修复说明

  1. Bug1修复:原代码用当前编号的长度计算光标位置,适配多位数编号时逻辑错误,改为直接使用新生成的编号字符串长度计算光标位置,确保光标始终在新编号后方。
  2. Bug2修复:原退格逻辑的字符串截取条件错误(截取3个字符匹配2个字符的". "),改为先检查光标前内容是否以". "结尾,再匹配完整数字编号格式,动态获取编号长度进行删除,适配任意位数的编号。
  3. Bug3修复:调整_getPreviousLine方法,当光标在第一行时,截取范围排除刚输入的换行符,确保正确获取上一行的完整编号内容;同时优化退格逻辑触发条件,仅在退格操作时处理编号删除,避免与回车逻辑冲突。

内容的提问来源于stack exchange,提问作者Shruti Ramnandan Sharma

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 09:20:55