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

如何实现支持用户ID、重名提及与编辑的Flutter文本输入框

Flutter 支持完整提及功能的文本输入框实现方案

核心思路

将显示文本与底层提及数据分离:显示层展示用户名,底层存储用户唯一ID及提及内容在文本中的位置信息。这种设计既能解决同名用户区分问题,也能在用户名变更时保持消息的准确性,同时支持编辑时自动维护提及的完整性。

数据模型设计

1. 提及数据类

用于存储单个提及的核心信息:

class Mention {
  final String userId; // 用户唯一ID,区分同名用户
  final String displayName; // 显示用的用户名
  int start; // 提及内容在文本中的起始下标
  int end; // 提及内容在文本中的结束下标

  Mention({
    required this.userId,
    required this.displayName,
    required this.start,
    required this.end,
  });
}

2. 消息内容类

用于统一存储消息的显示文本和所有提及数据,方便持久化和编辑加载:

class MessageContent {
  final String plainText; // 消息的纯文本内容
  final List<Mention> mentions; // 消息中所有提及的列表

  MessageContent({
    required this.plainText,
    required this.mentions,
  });
}

自定义 TextEditingController 实现

核心逻辑是维护提及数据与文本的位置同步,处理插入、删除、编辑操作时的偏移调整:

class MentionTextEditingController extends TextEditingController {
  List<Mention> mentions = [];

  // 插入提及用户到当前光标位置
  void insertMention(Mention mention) {
    final cursorPos = selection.baseOffset;
    // 在光标位置插入用户名文本
    final newText = text.replaceRange(cursorPos, cursorPos, mention.displayName);
    // 更新提及的实际位置
    final updatedMention = Mention(
      userId: mention.userId,
      displayName: mention.displayName,
      start: cursorPos,
      end: cursorPos + mention.displayName.length,
    );
    mentions.add(updatedMention);
    // 更新文本内容与光标位置
    value = value.copyWith(
      text: newText,
      selection: TextSelection.collapsed(offset: cursorPos + mention.displayName.length),
      composing: TextRange.empty,
    );
    // 调整插入位置之后的所有提及的偏移量
    _adjustMentionOffsets(cursorPos, mention.displayName.length);
  }

  // 文本变化时自动调整提及的位置偏移
  @override
  void notifyListeners() {
    final oldTextLength = previousText.length;
    final newTextLength = text.length;
    final diff = newTextLength - oldTextLength;
    if (diff != 0) {
      final cursorPos = selection.baseOffset;
      _adjustMentionOffsets(cursorPos, diff);
      _cleanInvalidMentions(); // 清理被删除的无效提及
    }
    super.notifyListeners();
  }

  // 调整提及的位置偏移
  void _adjustMentionOffsets(int position, int delta) {
    mentions = mentions.map((mention) {
      if (mention.start >= position) {
        return Mention(
          userId: mention.userId,
          displayName: mention.displayName,
          start: mention.start + delta,
          end: mention.end + delta,
        );
      }
      return mention;
    }).toList();
  }

  // 清理无效的提及(比如被完全删除的提及)
  void _cleanInvalidMentions() {
    mentions.removeWhere((mention) {
      return mention.start >= text.length || mention.end > text.length || mention.start >= mention.end;
    });
  }

  // 从保存的 MessageContent 加载消息(编辑场景)
  void loadMessageContent(MessageContent content) {
    text = content.plainText;
    mentions = List.from(content.mentions);
  }

  // 导出 MessageContent 用于持久化存储
  MessageContent exportMessageContent() {
    return MessageContent(
      plainText: text,
      mentions: List.from(mentions),
    );
  }
}

UI 交互与样式处理

1. 提及高亮显示

通过 buildTextSpan 自定义文本样式,将提及部分高亮:

TextField(
  controller: _mentionController,
  decoration: InputDecoration(hintText: "输入消息,@触发提及"),
  style: TextStyle(fontSize: 16),
  buildTextSpan: (context, baseStyle) {
    final spans = <TextSpan>[];
    int currentIndex = 0;
    // 按起始位置排序提及,避免拼接顺序混乱
    final sortedMentions = _mentionController.mentions.toList()
      ..sort((a, b) => a.start.compareTo(b.start));

    for (final mention in sortedMentions) {
      // 添加提及之前的普通文本
      if (currentIndex < mention.start) {
        spans.add(TextSpan(
          text: _mentionController.text.substring(currentIndex, mention.start),
          style: baseStyle,
        ));
      }
      // 添加高亮的提及文本
      spans.add(TextSpan(
        text: mention.displayName,
        style: baseStyle.copyWith(color: Colors.blue, fontWeight: FontWeight.bold),
      ));
      currentIndex = mention.end;
    }
    // 添加剩余的普通文本
    if (currentIndex < _mentionController.text.length) {
      spans.add(TextSpan(
        text: _mentionController.text.substring(currentIndex),
        style: baseStyle,
      ));
    }
    return TextSpan(style: baseStyle, children: spans);
  },
)

2. @触发用户选择

监听文本输入,当输入@时弹出用户选择列表:

@override
void initState() {
  super.initState();
  _mentionController.addListener(() {
    final text = _mentionController.text;
    final cursorPos = _mentionController.selection.baseOffset;
    // 检测光标前是否输入了@
    if (cursorPos > 0 && text[cursorPos - 1] == '@') {
      _showUserSelector();
    }
  });
}

void _showUserSelector() {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: Text("选择提及用户"),
      content: ListView.builder(
        itemCount: _userList.length, // _userList 为你的用户数据源
        itemBuilder: (context, index) {
          final user = _userList[index];
          return ListTile(
            title: Text(user.displayName),
            onTap: () {
              _mentionController.insertMention(Mention(
                userId: user.id,
                displayName: user.displayName,
                start: 0, // 插入时会自动修正位置
                end: 0,
              ));
              Navigator.pop(context);
            },
          );
        },
      ),
    ),
  );
}

编辑场景支持

当需要编辑已保存的消息时,直接从持久化存储中取出MessageContent,调用控制器的loadMessageContent方法即可加载完整的文本和提及数据,编辑过程中控制器会自动维护提及的位置和完整性。

关键优势

  • 同名用户区分:通过用户唯一ID识别,不受显示名影响
  • 编辑完整性:自动调整提及位置,支持消息任意位置编辑
  • 数据持久化:存储用户ID而非显示名,避免用户名变更导致消息失效
  • 多次提及支持:同一用户可被多次提及,各自维护独立的位置信息

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 07:37:49