Flutter TextField中如何一次性删除整个WidgetSpan?
问题:自定义WidgetSpan在TextField中无法一次性删除,只能逐字符删除
在使用自定义TextEditingController渲染WidgetSpan时,组件能正常显示,但删除时会逐个删除字符,无法一次性删掉整个WidgetSpan对应的<cmd>...</cmd>文本块,同时还存在输入新文本时光标位置异常的问题。
解决思路
- 重写
TextEditingController的delete方法,拦截系统删除操作,判断光标位置是否落在WidgetSpan对应的文本范围内,若是则一次性删除整个匹配块 - 修复文本修改逻辑,直接更新控制器的
value而非返回字符串,确保UI状态同步 - 处理光标定位,删除后将光标自动定位到原块的起始位置,输入时保持光标在文本末尾
- 支持文本中存在多个
WidgetSpan的场景
修改后的完整代码
import 'package:flutter/material.dart'; // 定义常量,避免硬编码 const String SPLIT_CMD_START = '<cmd>'; const String SPLIT_CMD_END = '</cmd>'; const String SPLIT_CMD_MID = '|'; class WidgetSpanTextEditingController extends TextEditingController { WidgetSpanTextEditingController({String? text}) : super.fromValue(text == null ? TextEditingValue.empty : TextEditingValue(text: text)); @override TextSpan buildTextSpan({ required BuildContext context, TextStyle? style, required bool withComposing, }) { // 匹配所有<cmd>...</cmd>格式的文本块 final matches = RegExp(r'<cmd>(.*?)</cmd>').allMatches(text).toList(); if (matches.isEmpty) { return TextSpan(text: text, style: style); } final children = <InlineSpan>[]; int currentOffset = 0; for (final match in matches) { // 添加匹配块之前的普通文本 if (match.start > currentOffset) { children.add(TextSpan( text: text.substring(currentOffset, match.start), style: style, )); } // 提取标签内的内容,分割显示文本和真实命令 final content = text.substring( match.start + SPLIT_CMD_START.length, match.end - SPLIT_CMD_END.length, ); final parts = content.trim().split(SPLIT_CMD_MID); final displayText = parts.isNotEmpty ? parts[0].trim() : ''; // 添加自定义WidgetSpan children.add(WidgetSpan( child: GestureDetector( child: Padding( padding: const EdgeInsets.only(right: 5.0, top: 2.0, bottom: 2.0), child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(5.0)), child: Container( padding: const EdgeInsets.all(5.0), color: Colors.orange, child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(displayText), const SizedBox(width: 5.0), InkWell( child: const Icon(Icons.close, size: 15.0), onTap: () => _removeMatch(match), ), ], ), ), ), ), ), )); currentOffset = match.end; } // 添加最后一个匹配块之后的普通文本 if (currentOffset < text.length) { children.add(TextSpan( text: text.substring(currentOffset), style: style, )); } return TextSpan(children: children, style: style); } @override void delete(int start, int end) { // 检查是否有匹配块包含当前删除范围 final matches = RegExp(r'<cmd>(.*?)</cmd>').allMatches(text).toList(); for (final match in matches) { // 判断删除操作是否触及匹配块 if ((start >= match.start && start < match.end) || (end > match.start && end <= match.end) || (start <= match.start && end >= match.end)) { // 一次性删除整个匹配块 super.delete(match.start, match.end); // 设置光标到匹配块的起始位置 selection = TextSelection.collapsed(offset: match.start); return; } } // 无匹配块时执行默认删除逻辑 super.delete(start, end); } // 通过匹配对象删除对应的文本块 void _removeMatch(Match match) { value = value.copyWith( text: text.replaceRange(match.start, match.end, ''), selection: TextSelection.collapsed(offset: match.start), ); } }
关键修改说明
- 修复正则匹配错误:原代码中错误使用转义后的
<cmd>作为匹配规则,改为直接匹配<cmd>标签,确保能正确识别目标文本块 - 重写
delete方法:拦截系统删除操作,判断删除位置是否在WidgetSpan对应的文本范围内,实现一次性删除整个块 - 支持多WidgetSpan场景:原代码仅处理最后一个匹配块,现在遍历所有匹配块,批量构建文本和WidgetSpan
- 优化状态同步:删除操作直接修改控制器的
value属性,确保UI实时更新,同时正确设置光标位置 - 修复光标异常:删除后光标自动定位到原块起始位置,输入新内容时沿用系统默认逻辑,保持光标在文本末尾
内容的提问来源于stack exchange,提问作者Nicholas Jela
相关产品推荐
相关产品推荐

