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

Flutter项目中实现文本选中弹出快捷菜单示例请求

Flutter 选中文本弹出快捷笔记菜单实现示例

核心思路

要么扩展系统默认的文本选择工具栏,添加自定义"添加笔记"按钮;要么通过Overlay组件实现完全自定义的悬浮菜单,监听文本选择事件触发菜单展示。


方案一:扩展系统默认文本选择工具栏

复用系统原生工具栏样式,仅新增自定义操作按钮,兼容性好、实现简单。

代码示例

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('文本选中专栏笔记示例')),
        body: const Padding(
          padding: EdgeInsets.all(16.0),
          child: CustomSelectableText(
            '选中文本试试,会弹出添加笔记的选项哦!这是一段测试文本,用于演示文本选择后的快捷笔记功能。',
          ),
        ),
      ),
    );
  }
}

class CustomSelectableText extends StatefulWidget {
  final String text;
  const CustomSelectableText(this.text, {super.key});

  @override
  State<CustomSelectableText> createState() => _CustomSelectableTextState();
}

class _CustomSelectableTextState extends State<CustomSelectableText> {
  @override
  Widget build(BuildContext context) {
    return SelectableText(
      widget.text,
      toolbarOptions: const ToolbarOptions(
        copy: true,
        selectAll: true,
        cut: false,
        paste: false,
      ),
      // 自定义文本选择工具栏
      contextMenuBuilder: (context, editableTextState) {
        final selection = editableTextState.textEditingValue.selection;
        // 仅当选中有效文本时展示自定义工具栏
        if (selection.isValid && selection.baseOffset != selection.extentOffset) {
          final selectedText = widget.text.substring(selection.baseOffset, selection.extentOffset);
          return AdaptiveTextSelectionToolbar(
            anchors: editableTextState.contextMenuAnchors,
            children: [
              // 保留系统默认按钮
              TextSelectionToolbarCopyButton(
                onCopy: () {
                  editableTextState.handleCopy();
                  Navigator.pop(context);
                },
              ),
              TextSelectionToolbarSelectAllButton(
                onSelectAll: () {
                  editableTextState.handleSelectAll();
                  Navigator.pop(context);
                },
              ),
              // 自定义添加笔记按钮
              TextSelectionToolbarTextButton(
                onPressed: () {
                  Navigator.pop(context);
                  _showNoteDialog(selectedText);
                },
                child: const Text('添加笔记'),
              ),
            ],
          );
        }
        return const SizedBox.shrink();
      },
    );
  }

  // 弹出笔记编辑弹窗
  void _showNoteDialog(String selectedText) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('快速笔记'),
        content: TextField(
          controller: TextEditingController(text: selectedText),
          maxLines: 5,
          decoration: const InputDecoration(hintText: '编辑你的笔记...'),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          TextButton(
            onPressed: () {
              // 此处添加笔记保存逻辑(如存入本地存储/后端接口)
              Navigator.pop(context);
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('笔记已保存')),
              );
            },
            child: const Text('保存'),
          ),
        ],
      ),
    );
  }
}

说明

  • contextMenuBuilder是Flutter 3.3+新增API,用于自定义文本选择工具栏,适配iOS/Android原生样式
  • 通过editableTextState获取选中文本,传递给编辑弹窗
  • 保留系统默认操作,降低用户学习成本

方案二:自定义悬浮菜单(Overlay实现)

完全自定义菜单样式与位置,适合需要个性化设计的场景。

代码示例

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('自定义悬浮笔记菜单')),
        body: const Padding(
          padding: EdgeInsets.all(16.0),
          child: CustomTextWithMenu(),
        ),
      ),
    );
  }
}

class CustomTextWithMenu extends StatefulWidget {
  const CustomTextWithMenu({super.key});

  @override
  State<CustomTextWithMenu> createState() => _CustomTextWithMenuState();
}

class _CustomTextWithMenuState extends State<CustomTextWithMenu> {
  OverlayEntry? _overlayEntry;
  String _selectedText = '';
  final GlobalKey _textKey = GlobalKey();

  // 展示自定义悬浮菜单
  void _showMenu(Rect globalRect) {
    _overlayEntry = OverlayEntry(
      builder: (context) => Positioned(
        top: globalRect.top - 60,
        left: globalRect.left,
        child: Material(
          elevation: 6,
          borderRadius: BorderRadius.circular(8),
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              TextButton.icon(
                onPressed: () {
                  _overlayEntry?.remove();
                  _showNoteDialog(_selectedText);
                },
                icon: const Icon(Icons.note_add, size: 18),
                label: const Text('添加笔记'),
              ),
              const VerticalDivider(width: 1, thickness: 16),
              TextButton.icon(
                onPressed: () {
                  _overlayEntry?.remove();
                  Clipboard.setData(ClipboardData(text: _selectedText));
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('已复制')),
                  );
                },
                icon: const Icon(Icons.copy, size: 18),
                label: const Text('复制'),
              ),
            ],
          ),
        ),
      ),
    );
    Overlay.of(context).insert(_overlayEntry!);
  }

  // 隐藏菜单
  void _hideMenu() {
    _overlayEntry?.remove();
    _overlayEntry = null;
  }

  // 笔记编辑弹窗
  void _showNoteDialog(String text) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('快速笔记'),
        content: TextField(
          controller: TextEditingController(text: text),
          maxLines: 5,
        ),
        actions: [
          TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消')),
          TextButton(
            onPressed: () {
              Navigator.pop(context);
              ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('笔记已保存')));
            },
            child: const Text('保存'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return SelectableText(
      '选中文本后,会弹出自定义的悬浮菜单,点击添加笔记即可保存内容。这是一段用于测试的长文本,确保能覆盖不同的选择场景。',
      key: _textKey,
      onSelectionChanged: (selection, cause) {
        // 仅处理长按/拖拽选中事件
        if (cause == SelectionChangedCause.longPress || cause == SelectionChangedCause.drag) {
          if (selection.isValid && selection.baseOffset != selection.extentOffset) {
            _selectedText = selection.textInside(TextEditingValue(text: '选中文本后,会弹出自定义的悬浮菜单,点击添加笔记即可保存内容。这是一段用于测试的长文本,确保能覆盖不同的选择场景。'))!;
            // 获取选中区域的全局坐标
            final renderBox = _textKey.currentContext?.findRenderObject() as RenderBox;
            final localRect = renderBox.getLocalRectForCaret(selection.extent);
            final globalRect = renderBox.localToGlobal(Offset.zero) & localRect.size;
            _hideMenu();
            _showMenu(globalRect);
          } else {
            _hideMenu();
          }
        }
      },
    );
  }

  @override
  void dispose() {
    _hideMenu();
    super.dispose();
  }
}

说明

  • 通过onSelectionChanged监听文本选择状态,获取选中内容与位置
  • 使用OverlayEntry在屏幕顶层绘制自定义菜单,位置跟随选中区域调整
  • 可自由修改菜单的样式、布局、动画效果

注意事项

  • 方案一的contextMenuBuilder需Flutter 3.3+版本支持,低版本可使用第三方包兼容
  • 笔记保存逻辑需自行实现,可结合shared_preferences、Hive等本地存储方案
  • 需处理文本选择取消、页面跳转等场景,及时隐藏菜单

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 18:46:11