Flutter如何基于TextFormField实现类似WhatsApp的@提及功能
在Flutter的TextFormField中实现WhatsApp风格的@提及功能
当然可以搞定!我之前做过类似的需求,其实核心就是监听输入、识别@触发、弹出选择列表,最后把选中的内容以高亮样式插入到输入框里。下面我一步步给你拆解实现思路和关键代码,都是原生Flutter就能搞定的,不用依赖太多第三方库:
1. 监听输入,捕捉@触发时机
首先我们需要用TextEditingController来监听输入框的文本变化,当用户输入@的时候,触发弹出提及选择面板。同时也要处理用户输入其他内容时关闭面板的逻辑:
final TextEditingController _controller = TextEditingController(); OverlayEntry? _overlayEntry; final List<String> _mentionItems = ['Alice', 'Bob', '会议室A', '咖啡区']; // 模拟可选的提及项 @override void initState() { super.initState(); _controller.addListener(_handleTextChange); } void _handleTextChange() { final text = _controller.text; final selection = _controller.selection; // 检查光标位置:必须是光标(非选中状态),且光标前的字符是@ if (selection.start == selection.end && selection.start > 0) { final charBefore = text[selection.start - 1]; if (charBefore == '@' && _overlayEntry == null) { // 弹出提及选择面板 _showMentionPicker(); } else if (charBefore != '@' && _overlayEntry != null) { // 输入非@相关内容,关闭面板 _hideMentionPicker(); } } }
2. 用Overlay弹出提及选择面板
要实现类似WhatsApp那种紧贴输入框的选择列表,用Flutter的Overlay组件最合适,它可以在当前页面的最上层渲染内容,而且能灵活定位:
void _showMentionPicker() { // 获取输入框的位置信息,用来定位面板 final renderBox = context.findRenderObject() as RenderBox; final offset = renderBox.localToGlobal(Offset.zero); _overlayEntry = OverlayEntry( builder: (context) => Positioned( left: offset.dx, top: offset.dy - 220, // 调整这个值让面板贴在输入框上方 width: renderBox.size.width, child: Material( elevation: 4, borderRadius: BorderRadius.circular(8), child: ListView.builder( shrinkWrap: true, padding: EdgeInsets.zero, itemCount: _mentionItems.length, itemBuilder: (context, index) { final item = _mentionItems[index]; return ListTile( title: Text('@$item'), onTap: () { _insertMention(item); _hideMentionPicker(); }, ); }, ), ), ), ); Overlay.of(context).insert(_overlayEntry!); } void _hideMentionPicker() { _overlayEntry?.remove(); _overlayEntry = null; }
3. 自定义输入控制器,实现提及内容高亮
默认的TextFormField只能显示统一样式的文本,我们需要自定义TextEditingController,重写buildTextSpan方法,让@开头的提及内容显示特殊样式(比如蓝色加粗):
class MentionTextEditingController extends TextEditingController { final List<String> mentionedItems; MentionTextEditingController(this.mentionedItems); @override TextSpan buildTextSpan({required BuildContext context, TextStyle? style, required bool withComposing}) { final spans = <TextSpan>[]; final text = this.text; int currentStart = 0; // 用正则匹配所有@开头的提及内容 final mentionRegex = RegExp(r'@(\w+|\S+)'); for (final match in mentionRegex.allMatches(text)) { final mentionText = match.group(0)!; // 先添加匹配前的普通文本 if (match.start > currentStart) { spans.add(TextSpan(text: text.substring(currentStart, match.start), style: style)); } // 添加高亮的提及文本 spans.add(TextSpan( text: mentionText, style: style?.copyWith( color: Colors.blue, fontWeight: FontWeight.w600, ), )); currentStart = match.end; } // 添加剩余的普通文本 if (currentStart < text.length) { spans.add(TextSpan(text: text.substring(currentStart), style: style)); } return TextSpan(children: spans, style: style); } }
然后替换之前的普通控制器为这个自定义的:
final MentionTextEditingController _controller = MentionTextEditingController(['Alice', 'Bob', '会议室A', '咖啡区']);
4. 插入提及内容并处理光标位置
当用户选择了某个提及项后,我们需要把光标前的@替换成完整的@xxx,并且把光标移动到插入内容的后面,保证输入流畅:
void _insertMention(String item) { final currentText = _controller.text; final selection = _controller.selection; // 替换光标前的@为@xxx,后面加个空格方便继续输入 final newText = currentText.replaceRange(selection.start - 1, selection.start, '@$item '); _controller.text = newText; // 把光标移到@xxx后面的空格位置 _controller.selection = TextSelection.fromPosition( TextPosition(offset: selection.start + item.length + 1) ); }
一些额外的细节处理
- 点击输入框外部关闭面板:可以在页面上添加
GestureDetector,监听点击事件调用_hideMentionPicker() - 搜索过滤提及项:如果可选的提及项很多,可以在面板顶部加个搜索框,实时过滤
_mentionItems - 处理键盘弹出/收起:可以用
WidgetsBindingObserver监听键盘状态,调整面板的位置 - 多个@的支持:上面的正则已经能匹配多个@内容,不需要额外修改
这样一套下来,就能完美实现类似WhatsApp的@提及功能了,从触发、选择到样式渲染都和原生体验一致!
内容的提问来源于stack exchange,提问作者Bisma Frühling
相关产品推荐
相关产品推荐

