概述

#

上下文菜单(或文本选择工具栏)是指在 Flutter 中长按或右键单击文本时显示的菜单,其中包含诸如剪切复制粘贴全选等选项。以前,只能通过 ToolbarOptionsTextSelectionControls 对其进行有限的自定义。现在,它们已通过小部件(widget)实现组合,就像 Flutter 中的其他一切一样,并且特定的配置参数已被弃用。

背景

#

以前,可以通过 TextSelectionControls 禁用上下文菜单中的按钮,但除此之外的任何自定义都需要复制和编辑框架中数百行的自定义类。现在,所有这些都已被一个简单的构建器函数 contextMenuBuilder 取代,该函数允许使用任何 Flutter 小部件作为上下文菜单。

变更说明

#

上下文菜单现在是从 contextMenuBuilder 参数构建的,该参数已添加到所有文本编辑和文本选择小部件中。如果未提供此参数,Flutter 会默认将其设置为一个为给定平台构建正确上下文菜单的选项。所有这些默认小部件都已公开供用户重用。自定义上下文菜单现在包括使用 contextMenuBuilder 返回您想要的任何小部件,可能包括重用内置的上下文菜单小部件。

下面是一个示例,演示了如何在选中电子邮件地址时将“发送电子邮件”按钮添加到默认上下文菜单中。完整代码可在 GitHub 上的 samples 仓库中找到,文件名为 email_button_page.dart

dart
TextField(
  contextMenuBuilder: (context, editableTextState) {
    final TextEditingValue value = editableTextState.textEditingValue;
    final List<ContextMenuButtonItem> buttonItems =
        editableTextState.contextMenuButtonItems;
    if (isValidEmail(value.selection.textInside(value.text))) {
      buttonItems.insert(
          0,
          ContextMenuButtonItem(
            label: 'Send email',
            onPressed: () {
              ContextMenuController.removeAny();
              Navigator.of(context).push(_showDialog(context));
            },
          ));
    }
    return AdaptiveTextSelectionToolbar.buttonItems(
      anchors: editableTextState.contextMenuAnchors,
      buttonItems: buttonItems,
    );
  },
)

GitHub 上的 samples 仓库中提供了大量不同自定义上下文菜单的示例,地址为 samples repo

所有相关的弃用功能都已标记了弃用警告“请改用 contextMenuBuilder。”

迁移指南

#

通常,对上下文菜单的任何先前更改(现已弃用)现在都需要使用相关文本编辑或文本选择小部件上的 contextMenuBuilder 参数(例如,TextField 上的)。返回一个内置的上下文菜单小部件,如 AdaptiveTextSelectionToolbar,即可使用 Flutter 的内置上下文菜单;或者返回您自己的小部件来实现完全自定义。

为了迁移到 contextMenuBuilder,已弃用以下参数和类。

该类以前用于显式启用或禁用上下文菜单中的特定按钮。在此更改之前,您可能像这样将其传递给 TextField 或其他小部件:

dart
// Deprecated.
TextField(
  toolbarOptions: ToolbarOptions(
    copy: true,
  ),
)

现在,您可以通过调整传递给 AdaptiveTextSelectionToolbarbuttonItems 来实现相同效果。例如,您可以确保剪切按钮永远不会出现,而其他按钮则像往常一样出现:

dart
TextField(
  contextMenuBuilder: (context, editableTextState) {
    final List<ContextMenuButtonItem> buttonItems =
        editableTextState.contextMenuButtonItems;
    buttonItems.removeWhere((ContextMenuButtonItem buttonItem) {
      return buttonItem.type == ContextMenuButtonType.cut;
    });
    return AdaptiveTextSelectionToolbar.buttonItems(
      anchors: editableTextState.contextMenuAnchors,
      buttonItems: buttonItems,
    );
  },
)

或者,您可以确保剪切按钮独占且始终出现:

dart
TextField(
  contextMenuBuilder: (context, editableTextState) {
    return AdaptiveTextSelectionToolbar.buttonItems(
      anchors: editableTextState.contextMenuAnchors,
      buttonItems: <ContextMenuButtonItem>[
        ContextMenuButtonItem(
          onPressed: () {
            editableTextState.cutSelection(SelectionChangedCause.toolbar);
          },
          type: ContextMenuButtonType.cut,
        ),
      ],
    );
  },
)

TextSelectionControls.canCut 和其他按钮布尔值

#

这些布尔值之前与 ToolbarOptions.cut 等具有相同效果,即启用和禁用特定按钮。在此更改之前,您可能通过重写 TextSelectionControls 并像这样设置这些布尔值来隐藏和显示按钮:

dart
// Deprecated.
class _MyMaterialTextSelectionControls extends MaterialTextSelectionControls {
  @override
  bool canCut() => false,
}

有关如何使用 contextMenuBuilder 实现类似效果,请参阅上一节关于 ToolbarOptions 的内容。

TextSelectionControls.handleCut 和其他按钮回调

#

这些函数允许修改按下按钮时调用的回调。在此更改之前,您可能通过重写这些处理程序方法来修改上下文菜单按钮的回调,如下所示:

dart
// Deprecated.
class _MyMaterialTextSelectionControls extends MaterialTextSelectionControls {
  @override
  bool handleCut() {
    // My custom cut implementation here.
  },
}

使用 contextMenuBuilder 仍然可以实现这一点,包括在自定义处理程序中调用原始按钮的操作,方法是使用诸如 AdaptiveTextSelectionToolbar.buttonItems 等工具栏小部件。

此示例演示了如何修改复制按钮,使其在执行常规复制逻辑的同时显示一个对话框。

dart
TextField(
  contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
    final List<ContextMenuButtonItem> buttonItems =
        editableTextState.contextMenuButtonItems;
    final int copyButtonIndex = buttonItems.indexWhere(
      (ContextMenuButtonItem buttonItem) {
        return buttonItem.type == ContextMenuButtonType.copy;
      },
    );
    if (copyButtonIndex >= 0) {
      final ContextMenuButtonItem copyButtonItem =
          buttonItems[copyButtonIndex];
      buttonItems[copyButtonIndex] = copyButtonItem.copyWith(
        onPressed: () {
          copyButtonItem.onPressed();
          Navigator.of(context).push(
            DialogRoute<void>(
              context: context,
              builder: (BuildContext context) =>
                const AlertDialog(
                  title: Text('Copied, but also showed this dialog.'),
                ),
            );
          )
        },
      );
    }
    return AdaptiveTextSelectionToolbar.buttonItems(
      anchors: editableTextState.contextMenuAnchors,
      buttonItems: buttonItems,
    );
  },
)

在 GitHub 上的 samples 仓库中,可以找到一个完整的示例,其中修改了内置上下文菜单操作,文件名为 modified_action_page.dart

此函数以类似于 contextMenuBuilder 的方式生成上下文菜单小部件,但需要更多设置才能使用。在此更改之前,您可能像这样将 buildToolbar 作为 TextSelectionControls 的一部分进行重写:

dart
// Deprecated.
class _MyMaterialTextSelectionControls extends MaterialTextSelectionControls {
  @override
  Widget buildToolbar(
    BuildContext context,
    Rect globalEditableRegion,
    double textLineHeight,
    Offset selectionMidpoint,
    List<TextSelectionPoint> endpoints,
    TextSelectionDelegate delegate,
    ClipboardStatusNotifier clipboardStatus,
    Offset lastSecondaryTapDownPosition,
  ) {
    return _MyCustomToolbar();
  },
}

现在,您可以直接将 contextMenuBuilder 用作 TextField(和其他小部件)的参数。传递给 buildToolbar 的参数信息可以从传递给 contextMenuBuilderEditableTextState 中获取。

以下示例展示了如何从头开始构建一个完全自定义的工具栏,同时仍然使用默认按钮。

dart
class _MyContextMenu extends StatelessWidget {
  const _MyContextMenu({
    required this.anchor,
    required this.children,
  });

  final Offset anchor;
  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          top: anchor.dy,
          left: anchor.dx,
          child: Container(
            width: 200,
            height: 200,
            color: Colors.amberAccent,
            child: Column(
              children: children,
            ),
          ),
        ),
      ],
    );
  }
}

class _MyTextField extends StatelessWidget {
  const _MyTextField();

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _controller,
      maxLines: 4,
      minLines: 2,
      contextMenuBuilder: (context, editableTextState) {
        return _MyContextMenu(
          anchor: editableTextState.contextMenuAnchors.primaryAnchor,
          children: AdaptiveTextSelectionToolbar.getAdaptiveButtons(
            context,
            editableTextState.contextMenuButtonItems,
          ).toList(),
        );
      },
    );
  }
}

在 GitHub 上的 samples 仓库中,可以找到一个完整的自定义上下文菜单构建示例,文件名为 custom_menu_page.dart

时间线

#

已在版本中实现:3.6.0-0.0.pre
在稳定版中发布:3.7.0

参考资料

#

API 文档

相关问题

相关 PR