如何在颤动中动态创建和显示弹出菜单?

Posted

技术标签:

【中文标题】如何在颤动中动态创建和显示弹出菜单?【英文标题】:How dynamically create and show a popup menu in flutter? 【发布时间】:2018-11-18 08:47:05 【问题描述】:

是否可以通过按下操作按钮来动态创建弹出菜单(PopupMenuButton)并将该菜单显示在屏幕中间?例如,如何修改标准的flutter应用来实现这个场景:

  void _showPopupMenu()
  
  // Create and show popup menu 
     ...
  

我设法在解决问题方面取得了一些进展,但仍然存在问题。这是 main.dart 的文本。通过单击画布,从 _handleTapDown (...) 调用 _showPopupMenu3 (context) 函数。菜单确实出现了,我可以捕捉选项,但选择菜单后没有关闭。要关闭菜单需要按 BACK 按钮或单击画布。这可能对应于 CANCEL 情况。所以问题是: 1)选择项目后如何关闭菜单(可能只是菜单属性的一些参数)? 2) 应该传递给位置参数的坐标的目的和含义不是很清楚。点击坐标旁边的菜单如何升起?

来源:

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget 
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) 
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or press Run > Flutter Hot Reload in IntelliJ). Notice that the
        // counter didn't reset back to zero; the application is not restarted.
        primarySwatch: Colors.blue,
      ),
      home: new TouchTestPage(title: 'Flutter Demo Home Page'),
    );
  


class TouchTestPage extends StatefulWidget 
  TouchTestPage(Key key, this.title) : super(key: key);

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  @override
  _TouchTestPageState createState() => new _TouchTestPageState();


class _TouchTestPageState extends State<TouchTestPage> 

  @override
  Widget build(BuildContext context) 
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return new Scaffold(
      appBar: new AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: new Text(widget.title),
      ),
      body: new Container(
        decoration: new BoxDecoration(
          color: Colors.white70,
          gradient: new LinearGradient(
              colors: <Color>[Colors.lightBlue, Colors.white30]),
          border: new Border.all(
            color: Colors.blueGrey,
            width: 1.0,
          ),
        ),
        child: new Center(child: new TouchControl()),
      ),
    );
  


class TouchControl extends StatefulWidget 
  final double xPos;
  final double yPos;
  final ValueChanged<Offset> onChanged;

  const TouchControl(
    Key key,
    this.onChanged,
    this.xPos: 0.0,
    this.yPos: 0.0,
  )
      : super(key: key);

  @override
  TouchControlState createState() => new TouchControlState();


class TouchControlState extends State<TouchControl> 
  double xPos = 0.0;
  double yPos = 0.0;

  double xStart = 0.0;
  double yStart = 0.0;

  double _scale     = 1.0;
  double _prevScale = null;

  void reset()
  
    xPos  = 0.0;
    yPos  = 0.0;
  

  final List<String> popupRoutes = <String>[
    "Properties", "Delete", "Leave"
  ];
  String selectedPopupRoute = "Properties";

  void _showPopupMenu3(BuildContext context)
  
    showMenu<String>(
      context: context,
      initialValue: selectedPopupRoute,
      position: new RelativeRect.fromLTRB(40.0, 60.0, 100.0, 100.0),
      items: popupRoutes.map((String popupRoute) 
        return new PopupMenuItem<String>(
          child: new
          ListTile(
              leading: const Icon(Icons.visibility),
              title: new Text(popupRoute),
              onTap: ()
              
                setState(()
                
                  print("onTap [$popupRoute] ");
                  selectedPopupRoute = popupRoute;
                );
              
          ),
          value: popupRoute,
        );
      ).toList(),
    );
  

  void onChanged(Offset offset)
  
    final RenderBox referenceBox = context.findRenderObject();
    Offset position = referenceBox.globalToLocal(offset);
    if (widget.onChanged != null)
    
      //    print('---- onChanged.CHANGE ----');
      widget.onChanged(position);
    
    else
    
      //    print('---- onChanged.NO CHANGE ----');
    

    xPos = position.dx;
    yPos = position.dy;

  

  @override
  bool hitTestSelf(Offset position) => true;

  void _handlePanStart(DragStartDetails details) 
    print('start');
    //  _scene.clear();


    final RenderBox referenceBox = context.findRenderObject();
    Offset position = referenceBox.globalToLocal(details.globalPosition);

    onChanged(details.globalPosition);
    xStart = xPos;
    yStart = yPos;
  

  void _handlePanEnd(DragEndDetails details) 

    print("_handlePanEnd");
    print('end');

  

  void _handleTapDown(TapDownDetails details) 

    print('--- _handleTapDown ---');
    final RenderBox referenceBox = context.findRenderObject();
    Offset position = referenceBox.globalToLocal(details.globalPosition);
    onChanged(new Offset(0.0, 0.0));

     _showPopupMenu3(context); //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    print('+++ _handleTapDown [$position.dx,$position.dy] +++');
  

  void _handleTapUp(TapUpDetails details) 
    //  _scene.clear();

    print('--- _handleTapUp   ---');
    final RenderBox referenceBox = context.findRenderObject();
    Offset position = referenceBox.globalToLocal(details.globalPosition);
    onChanged(new Offset(0.0, 0.0));

    //_showPopupMenu(context);
    print('+++ _handleTapUp   [$position.dx,$position.dy] +++');
  

  void _handleDoubleTap() 
    print('_handleDoubleTap');
  

  void _handleLongPress() 
    print('_handleLongPress');
  

  void _handlePanUpdate(DragUpdateDetails details) 

    //  logger.clear("_handlePanUpdate");
    final RenderBox referenceBox = context.findRenderObject();
    Offset position = referenceBox.globalToLocal(details.globalPosition);
    onChanged(details.globalPosition);
  

  @override
  Widget build(BuildContext context) 
    return new ConstrainedBox(
      constraints: new BoxConstraints.expand(),
      child: new GestureDetector(
        behavior: HitTestBehavior.opaque,
        onPanStart:     _handlePanStart,
        onPanEnd:       _handlePanEnd,
        onPanUpdate:    _handlePanUpdate,
        onTapDown:      _handleTapDown,
        onTapUp:        _handleTapUp,
        onDoubleTap:    _handleDoubleTap,
        onLongPress:    _handleLongPress,
//        onScaleStart:   _handleScaleStart,
//        onScaleUpdate:  _handleScaleUpdate,
//        onScaleEnd:     _handleScaleEnd,
//        child: new CustomPaint(
//          size: new Size(xPos, yPos),
//          painter: new ScenePainter(editor.getScene()),
//          foregroundPainter: new TouchControlPainter(/*_scene*//*editor.getScene(),*/ xPos, yPos),
//        ),
      ),
    );
  

【问题讨论】:

你试过什么?有什么问题? “没有成功”到底是什么意思? showDialog 函数没有编译,它的语法不正确。 您应该发布导致错误的代码以及您为该代码获得的完整且准确的错误消息。 当然可以。仅指定电子邮件地址。 【参考方案1】:

@Vishal Singh 的回答需要两个改进:

    如果您将0 用于right,则菜单将向右对齐,因为

在水平方向上,菜单的位置使其向空间最大的方向增长。例如,如果位置描述了屏幕左边缘的一个矩形,那么菜单的左边缘与该位置的左边缘对齐,并且菜单向右增长。

    如果您将0 用于bottom,这适用于没有initialValue 的弹出菜单,但如果设置了initialValue,则会将菜单向下移动。这是因为

如果指定了initialValue,那么第一个具有匹配值的项目将被突出显示,并且位置值给出垂直中心将与突出显示项目的垂直中心对齐的矩形(如果可能)。

如果没有指定initialValue,那么菜单的顶部将与位置矩形的顶部对齐。

https://api.flutter.dev/flutter/material/showMenu.html

因此,对于更通用的解决方案,正确计算右下角:

  final screenSize = MediaQuery.of(context).size;
  showMenu(
    context: context,
    position: RelativeRect.fromLTRB(
      offset.dx,
      offset.dy,
      screenSize.width - offset.dx,
      screenSize.height - offset.dy,
    ),
    items: [
      // ...
    ],
  );

【讨论】:

【参考方案2】:

有可能

    void _showPopupMenu() async 
      await showMenu(
        context: context,
        position: RelativeRect.fromLTRB(100, 100, 100, 100),
        items: [
          PopupMenuItem(
            value: 1
            child: Text("View"),
          ),
          PopupMenuItem(
             value: 2
            child: Text("Edit"),
          ),
          PopupMenuItem(
            value: 3
            child: Text("Delete"),
          ),
        ],
        elevation: 8.0,
      ).then((value)

// NOTE: even you didnt select item this method will be called with null of value so you should call your call back with checking if value is not null 


      if(value!=null)
       print(value);

       );
    

有时您会希望在您按下按钮的位置显示_showPopupMenu 为此使用 GestureDetector

GestureDetector(
  onTapDown: (TapDownDetails details) 
    _showPopupMenu(details.globalPosition);
  ,
  child: Container(child: Text("Press Me")),
);

然后 _showPopupMenu 会像

_showPopupMenu(Offset offset) async 
    double left = offset.dx;
    double top = offset.dy;
    await showMenu(
    context: context,
    position: RelativeRect.fromLTRB(left, top, 0, 0),
    items: [
      ...,
    elevation: 8.0,
  );

【讨论】:

你在第二部分遇到了什么问题? @Vishual 我在点击手势检测器小部件时遇到问题,没有弹出菜单显示。我不知道我用作 GestureDetector 小部件的孩子的 RaiseDButton 小部件是否是原因 right 和 bottom 设置为 0,这将导致菜单始终出现在右下角 看一看:***.com/a/63952566/10563627【参考方案3】:

@Vishal Singh 的回答真的很有帮助。但是,我遇到的问题是菜单总是在右边。给正确的价值一个非常高的价值修复它, 示例:

_showPopupMenu(Offset position) async 
    await showMenu(
        context: context,
        position: RelativeRect.fromLTRB(position.dx, position.dy, 100000, 0),
        ...

【讨论】:

【参考方案4】:

关闭菜单很简单:需要添加一行:Navigator.pop(context);

      onTap: ()
      
        setState(()
        
          print("onTap [$popupRoute] ");
          selectedPopupRoute = popupRoute;
          Navigator.pop(context);
        );
      

仍然是坐标问题。

【讨论】:

以上是关于如何在颤动中动态创建和显示弹出菜单?的主要内容,如果未能解决你的问题,请参考以下文章

iOS - 如何在 Swift 中显示“AirPlay”弹出菜单?

Winform中动态绑定menuStript菜单数据,点击菜单弹出窗体,关联数据表,窗体显示在容器中?

Vue 过渡到弹出菜单

动态更改弹出菜单项的字体大小 Android

在自定义视图上显示弹出菜单时不要关闭键盘

如何在swiftui中的navigationitems下面创建一个弹出菜单