如何实现支持用户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
相关产品推荐
相关产品推荐

