Skip to main content

一种自定义上下文菜单的新方法

摘要

#

上下文菜单,或文本选择工具栏,是在 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 上的示例存储库中提供了许多不同自定义上下文菜单的示例:in the samples repo

所有相关的已弃用功能都标有弃用警告“使用 contextMenuBuilder 代替”。

迁移指南

#

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

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

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

dart
// 已弃用。
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
// 已弃用。
class _MyMaterialTextSelectionControls extends MaterialTextSelectionControls {
  @override
  bool canCut() => false,
}

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

TextSelectionControls.handleCut 和其他按钮回调

#

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

dart
// 已弃用。
class _MyMaterialTextSelectionControls extends MaterialTextSelectionControls {
  @override
  bool handleCut() {
    // 我在这里自定义剪切实现。
  },
}

这仍然可以使用 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('已复制,但也显示了此对话框。'),
                ),
            );
          )
        },
      );
    }
    return AdaptiveTextSelectionToolbar.buttonItems(
      anchors: editableTextState.contextMenuAnchors,
      buttonItems: buttonItems,
    );
  },
)

在 GitHub 上的示例存储库中可以找到修改内置上下文菜单操作的完整示例:modified_action_page.dart

此函数类似于 contextMenuBuilder 生成上下文菜单小部件,但需要更多设置才能使用。在此更改之前,您可能已将 buildToolbar 作为 TextSelectionControls 的一部分进行覆盖,如下所示:

dart
// 已弃用。
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: