我可以在Flutter Web应用程序中更改右键单击操作吗?

9

像Google Drive一样,在Flutter Web应用程序中,我可以创建自定义菜单吗?


1
目前有一个开放的GitHub问题要添加该功能。所以我认为现在没有简单的方法。 - Lambda Fairy
1
只要运行document.onContextMenu.listen((event) => event.preventDefault());GestureDetectoronSecondaryTapUp事件就会起作用。 - nathanfranke
5个回答

19

以下是如何通过鼠标右键在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:
      }
    }
  }
}

唯一需要做的就是正确定位上下文菜单的左上角。


你能分享一个完整的例子吗?这段代码有一些错误。 - Subair K
但是使用这种解决方案,当浏览器屏幕大小改变时,我们肯定会遇到问题。菜单将不会处于正确的位置。我们该如何避免这种情况呢? - Mayur Agarwal
@MayurAgarwal,你测试了吗?我用Container(align: Alignment.bottomRight)替换了Center,看起来很好。 - BambinoUA

4

阻止默认右键菜单

web/index.html文件中,为<html>标签添加一个oncontextmenu属性:

<!DOCTYPE html>
<html oncontextmenu="event.preventDefault();">
<head>
...

请参阅:https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes#event_handler_attributes 这与https://dev59.com/orvoa4cB1Zd3GeqP4Hfj#64779321具有相同的效果(window.document只是<html>标签),但不会触发“避免在Flutter Web插件包之外使用仅限Web的库。”警告或使用universal_html软件包。
注意:热重载对于这种更改无效,但您可以简单地刷新(F5)浏览器。
添加自定义上下文菜单 https://github.com/flutter/flutter/pull/74286对于您的用例效果不佳

这应该默认显示在桌面上,但仅在右键单击基于EditableText的窗口小部件时才出现。暂时不支持其他位置的右键单击。

目前此功能还没有可自定义或可重用的选项。这是一个临时解决方案,我们计划进行扩展。

通常,您可以使用GestureDetector.onSecondaryTap来检测用户的右键单击。

3

在该未解决问题得到解决之前,您可以在main()中执行以下操作:

import 'dart:html';

void main() {
  window.document.onContextMenu.listen((evt) => evt.preventDefault());
  // ...
}

1
谢谢。现在,我可以禁用右键单击了。如何在右键单击时自定义菜单? - Subair K

2

您可以使用此类:https://api.flutter.dev/flutter/gestures/PointerSignalEvent-class.html并使用此小部件Listener,在onPointerSignal回调中进行操作:https://api.flutter.dev/flutter/widgets/Listener-class.html - Rody Davis
谢谢提供信息,您能否发送一个小例子,在该示例中,如果用户右键单击,则会打印某些内容? - Maadhav Sharma

0

感谢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: 

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接