概述

#

上下文菜单,或文本选择工具栏,是当在 Flutter 中长按或右键点击文本时出现的菜单,它们会显示**剪切**、**复制**、**粘贴**和**全选**等选项。以前,只能使用 ToolbarOptionsTextSelectionControls 对它们进行有限的定制。现在,它们已像 Flutter 中的其他所有内容一样,可以使用小部件进行组合,并且已弃用特定的配置参数。

背景

#

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

变更说明

#

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

这是一个示例,展示了如何在选择电子邮件地址时向默认上下文菜单添加一个**发送电子邮件**按钮。完整代码可在 GitHub 上的示例仓库中的 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 上的示例仓库中找到。

所有相关的已弃用功能都带有弃用警告:“请使用 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 上的示例仓库中的 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 上的示例仓库中的 custom_menu_page.dart 文件中找到。

时间线

#

首次发布版本:3.6.0-0.0.pre
稳定发布版本:3.7.0

参考资料

#

API 文档

相关问题

相关 PR