自定义上下文菜单的新方式
概述
#上下文菜单,或文本选择工具栏,是当在 Flutter 中长按或右键点击文本时出现的菜单,它们会显示**剪切**、**复制**、**粘贴**和**全选**等选项。以前,只能使用 ToolbarOptions
和 TextSelectionControls
对它们进行有限的定制。现在,它们已像 Flutter 中的其他所有内容一样,可以使用小部件进行组合,并且已弃用特定的配置参数。
背景
#以前,可以使用 TextSelectionControls
从上下文菜单中禁用按钮,但超出此范围的任何定制都需要复制和编辑框架中数百行自定义类。现在,所有这些都已被一个简单的构建器函数 contextMenuBuilder
所取代,它允许任何 Flutter 小部件用作上下文菜单。
变更说明
#上下文菜单现在通过 contextMenuBuilder
参数构建,该参数已添加到所有文本编辑和文本选择小部件中。如果未提供此参数,则 Flutter 会将其设置为默认值,为给定平台构建正确的上下文菜单。所有这些默认小部件都向用户公开以便重复使用。现在,定制上下文菜单包括使用 contextMenuBuilder
返回您想要的任何小部件,可能包括重复使用内置的上下文菜单小部件。
这是一个示例,展示了如何在选择电子邮件地址时向默认上下文菜单添加一个**发送电子邮件**按钮。完整代码可在 GitHub 上的示例仓库中的 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 上的示例仓库中找到。
所有相关的已弃用功能都带有弃用警告:“请使用 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 上的示例仓库中的 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 上的示例仓库中的 custom_menu_page.dart
文件中找到。
时间线
#首次发布版本:3.6.0-0.0.pre
稳定发布版本:3.7.0
参考资料
#API 文档
相关问题
- 简单的自定义文本选择工具栏
- 文本字段外的右键菜单
- 桌面文本编辑 - 稳定版
- 在 TextField 上禁用上下文菜单的功能
- 缺少用于文本选择工具栏样式的 API
- 在所有小部件中启用复制工具栏
- 从浏览器禁用上下文菜单
- 自定义上下文菜单未在 Flutter Web 上显示
相关 PR