像Google Drive一样,在Flutter Web应用程序中,我可以创建自定义菜单吗?
以下是如何通过鼠标右键在Flutter Web应用程序中实现工作上下文菜单的说明:
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:universal_html/html.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
void initState() {
super.initState();
// Prevent default event handler
document.onContextMenu.listen((event) => event.preventDefault());
}
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
body: Center(
child: Listener(
child: Icon(
Icons.ac_unit,
size: 48.0,
),
onPointerDown: _onPointerDown,
),
),
);
}
/// Callback when mouse clicked on `Listener` wrapped widget.
Future<void> _onPointerDown(PointerDownEvent event) async {
// Check if right mouse button clicked
if (event.kind == PointerDeviceKind.mouse &&
event.buttons == kSecondaryMouseButton) {
final overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
final menuItem = await showMenu<int>(
context: context,
items: [
PopupMenuItem(child: Text('Copy'), value: 1),
PopupMenuItem(child: Text('Cut'), value: 2),
],
position: RelativeRect.fromSize(
event.position & Size(48.0, 48.0), overlay.size));
// Check if menu item clicked
switch (menuItem) {
case 1:
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Copy clicked'),
behavior: SnackBarBehavior.floating,
));
break;
case 2:
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Cut clicked'),
behavior: SnackBarBehavior.floating));
break;
default:
}
}
}
}
唯一需要做的就是正确定位上下文菜单的左上角。
Container(align: Alignment.bottomRight)
替换了Center
,看起来很好。 - BambinoUA在web/index.html
文件中,为<html>
标签添加一个oncontextmenu
属性:
<!DOCTYPE html>
<html oncontextmenu="event.preventDefault();">
<head>
...
window.document
只是<html>
标签),但不会触发“避免在Flutter Web插件包之外使用仅限Web的库。”警告或使用universal_html
软件包。这应该默认显示在桌面上,但仅在右键单击基于EditableText的窗口小部件时才出现。暂时不支持其他位置的右键单击。
通常,您可以使用目前此功能还没有可自定义或可重用的选项。这是一个临时解决方案,我们计划进行扩展。
GestureDetector.onSecondaryTap
来检测用户的右键单击。在该未解决问题得到解决之前,您可以在main()
中执行以下操作:
import 'dart:html';
void main() {
window.document.onContextMenu.listen((evt) => evt.preventDefault());
// ...
}
感谢BambinoUA的启发。我决定为此制作自己的跨平台类。
适用于iOS/Android/Web/Windows/Mac和Linux。已经测试过。
import 'package:bap/components/splash_effect.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:universal_html/html.dart' as html;
class CrossPlatformClick extends StatefulWidget {
final Widget child;
/**
* Normal touch, tap, right click for platforms.
*/
final Function()? onNormalTap;
/**
* A list of menu items for right click or long press.
*/
final List<PopupMenuEntry<String>>? menuItems;
final Function(String? itemValue)? onMenuItemTapped;
const CrossPlatformClick({Key? key, required this.child, this.menuItems, this.onNormalTap, this.onMenuItemTapped}) : super(key: key);
@override
State<CrossPlatformClick> createState() => _CrossPlatformClickState();
}
class _CrossPlatformClickState extends State<CrossPlatformClick> {
/**
* We record this so that we can use long-press and location.
*/
PointerDownEvent? _lastEvent;
@override
Widget build(BuildContext context) {
final listener = Listener(
child: widget.child,
onPointerDown: (event) => _onPointerDown(context, event),
);
return SplashEffect(
isDisabled: widget.onNormalTap == null,
borderRadius: BorderRadius.zero,
onTap: widget.onNormalTap!,
child: listener,
onLongPress: () {
if (_lastEvent != null) {
_openMenu(context, _lastEvent!);
return;
}
if (kDebugMode) {
print("Last event was null, cannot open menu");
}
},
);
}
@override
void initState() {
super.initState();
html.document.onContextMenu.listen((event) => event.preventDefault());
}
/// Callback when mouse clicked on `Listener` wrapped widget.
Future<void> _onPointerDown(BuildContext context, PointerDownEvent event) async {
_lastEvent = event;
if (widget.menuItems == null) {
return;
}
// Check if right mouse button clicked
if (event.kind == PointerDeviceKind.mouse && event.buttons == kSecondaryMouseButton) {
return await _openMenu(context, event);
}
}
_openMenu(BuildContext context, PointerDownEvent event) async {
final overlay = Overlay.of(context)!.context.findRenderObject() as RenderBox;
final menuItem = await showMenu<String>(
context: context,
items: widget.menuItems ?? [],
position: RelativeRect.fromSize(event.position & Size(48.0, 48.0), overlay.size),
);
widget.onMenuItemTapped!(menuItem);
}
}
标准闪屏效果触摸的类
import 'package:flutter/material.dart';
class SplashEffect extends StatelessWidget {
final Widget child;
final Function() onTap;
final Function()? onLongPress;
final BorderRadius? borderRadius;
final bool isDisabled;
const SplashEffect({
Key? key,
required this.child,
required this.onTap,
this.isDisabled = false,
this.onLongPress,
this.borderRadius = const BorderRadius.all(Radius.circular(6)),
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (isDisabled) {
return child;
}
return Material(
type: MaterialType.transparency,
child: InkWell(
borderRadius: borderRadius,
child: child,
onTap: onTap,
onLongPress: onLongPress,
),
);
}
}
如何使用它:
return CrossPlatformClick(
onNormalTap: onTapped,
menuItems: [
PopupMenuItem(child: Text('Copy Name', style: TextStyle(fontSize: 16)), value: "copied"),
],
onMenuItemTapped: (item) {
print("item tapped: " + (item ?? "-no-item"));
},
child:
document.onContextMenu.listen((event) => event.preventDefault());
,GestureDetector
的onSecondaryTapUp
事件就会起作用。 - nathanfranke