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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.07 09:32:43