自定义上下文菜单的新方法
概述
#上下文菜单(或文本选择工具栏)是指在 Flutter 中长按或右键单击文本时显示的菜单,其中包含诸如剪切、复制、粘贴和全选等选项。以前,只能通过 ToolbarOptions
和 TextSelectionControls
对其进行有限的自定义。现在,它们已通过小部件(widget)实现组合,就像 Flutter 中的其他一切一样,并且特定的配置参数已被弃用。
背景
#以前,可以通过 TextSelectionControls
禁用上下文菜单中的按钮,但除此之外的任何自定义都需要复制和编辑框架中数百行的自定义类。现在,所有这些都已被一个简单的构建器函数 contextMenuBuilder
取代,该函数允许使用任何 Flutter 小部件作为上下文菜单。
变更说明
#上下文菜单现在是从 contextMenuBuilder
参数构建的,该参数已添加到所有文本编辑和文本选择小部件中。如果未提供此参数,Flutter 会默认将其设置为一个为给定平台构建正确上下文菜单的选项。所有这些默认小部件都已公开供用户重用。自定义上下文菜单现在包括使用 contextMenuBuilder
返回您想要的任何小部件,可能包括重用内置的上下文菜单小部件。
下面是一个示例,演示了如何在选中电子邮件地址时将“发送电子邮件”按钮添加到默认上下文菜单中。完整代码可在 GitHub 上的 samples 仓库中找到,文件名为 email_button_page.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
或其他小部件:
// Deprecated.
TextField(
toolbarOptions: ToolbarOptions(
copy: true,
),
)
现在,您可以通过调整传递给 AdaptiveTextSelectionToolbar
的 buttonItems
来实现相同效果。例如,您可以确保剪切按钮永远不会出现,而其他按钮则像往常一样出现:
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,
);
},
)
或者,您可以确保剪切按钮独占且始终出现:
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
并像这样设置这些布尔值来隐藏和显示按钮:
// Deprecated.
class _MyMaterialTextSelectionControls extends MaterialTextSelectionControls {
@override
bool canCut() => false,
}
有关如何使用 contextMenuBuilder
实现类似效果,请参阅上一节关于 ToolbarOptions
的内容。
TextSelectionControls.handleCut
和其他按钮回调
#这些函数允许修改按下按钮时调用的回调。在此更改之前,您可能通过重写这些处理程序方法来修改上下文菜单按钮的回调,如下所示:
// Deprecated.
class _MyMaterialTextSelectionControls extends MaterialTextSelectionControls {
@override
bool handleCut() {
// My custom cut implementation here.
},
}
使用 contextMenuBuilder
仍然可以实现这一点,包括在自定义处理程序中调用原始按钮的操作,方法是使用诸如 AdaptiveTextSelectionToolbar.buttonItems
等工具栏小部件。
此示例演示了如何修改复制按钮,使其在执行常规复制逻辑的同时显示一个对话框。
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
的一部分进行重写:
// 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
的参数信息可以从传递给 contextMenuBuilder
的 EditableTextState
中获取。
以下示例展示了如何从头开始构建一个完全自定义的工具栏,同时仍然使用默认按钮。
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 文档
相关问题
- 简单的自定义文本选择工具栏
- 文本字段外部的右键菜单
- 桌面文本编辑 - 稳定版
- 禁用 TextField 上下文菜单的功能
- 文本选择工具栏样式缺失的 API
- 在所有小部件中启用复制工具栏
- 禁用浏览器的上下文菜单
- 自定义上下文菜单在 Flutter Web 上不显示
相关 PR