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

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

关键修改说明

  1. 修复正则匹配错误:原代码中错误使用转义后的&lt;cmd&gt;作为匹配规则,改为直接匹配<cmd>标签,确保能正确识别目标文本块
  2. 重写delete方法:拦截系统删除操作,判断删除位置是否在WidgetSpan对应的文本范围内,实现一次性删除整个块
  3. 支持多WidgetSpan场景:原代码仅处理最后一个匹配块,现在遍历所有匹配块,批量构建文本和WidgetSpan
  4. 优化状态同步:删除操作直接修改控制器的value属性,确保UI实时更新,同时正确设置光标位置
  5. 修复光标异常:删除后光标自动定位到原块起始位置,输入新内容时沿用系统默认逻辑,保持光标在文本末尾

内容的提问来源于stack exchange,提问作者Nicholas Jela

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 20:22:16