如何动态地创建和显示一个弹出菜单在Flutter?

zbq4xfa0  于 2023-05-29  发布在  Flutter
关注(0)|答案(6)|浏览(144)

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

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

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

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),
//        ),
      ),
    );
  }
}
ax6ht2ek

ax6ht2ek1#

是的有可能

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 , value is the value given in PopupMenuItem
      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,
  );
}
xggvc2p6

xggvc2p62#

@Vishal Singh的回答需要两个改进:
1.如果使用0作为right,则菜单将向右对齐,因为
在水平方向上,菜单的位置使其在具有最大空间的方向上增长。例如,如果位置描述了屏幕左边缘上的矩形,则菜单的左边缘与位置的左边缘对齐,并且菜单向右增长。
1.如果您对bottom使用0,则在没有initialValue的弹出菜单中可以正常工作,但如果设置了initialValue,则会将菜单向下移动。这是因为
如果指定了initialValue,那么第一个具有匹配值的项目将被突出显示,并且position的值给出了一个矩形,其垂直中心将与突出显示的项目的垂直中心对齐(如果可能)。
如果没有指定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: [
      // ...
    ],
  );
jv4diomz

jv4diomz3#

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

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

x4shl7ld4#

自定义单击时的弹出菜单

使用async方法和showMenu()小部件

void showMemberMenu() async {
    await showMenu(
      context: context,
      position: RelativeRect.fromLTRB(200, 150, 100, 100),
      items: [
        PopupMenuItem(
          value: 1,
          child: Text("ROHIT",
            style: TextStyle(
              fontSize: 15.sp,
              fontWeight: FontWeight.bold,
              fontFamily: 'Roboto',
              color: Colors.green),),
        ),
        PopupMenuItem(
          value: 2,
          child: Text("REKHA", style: TextStyle(
              fontSize: 15.sp,
              fontWeight: FontWeight.bold,
              fontFamily: 'Roboto',
              color: Colors.green),),
        ),
        PopupMenuItem(
          value: 3,
          child: Text("DHRUV", style: TextStyle(
              fontSize: 15.sp,
              fontWeight: FontWeight.bold,
              fontFamily: 'Roboto',
              color: Colors.green),),
        ),
      ],
      elevation: 8.0,
    ).then((value) {
      if (value != null) print(value);
    });
  }

你调用popup的小部件代码

InkWell(
                                          onTap: () {
                                            showMemberMenu();
                                          },
                                          child: Text('Switch Profile',
                                              style: TextStyle(
                                                  color: Colors.blue,
                                                  fontFamily: 'Roboto1',
                                                  fontSize: 10.sp)),
                                        ),
h43kikqp

h43kikqp5#

除了Vishal Singh的答案之外,如果你不想让菜单出现在与你点击的偏移量相关的位置,而是与你覆盖的构建函数返回的小部件(你用作菜单打开器的那个)相关,使用你的上下文来找到它的渲染对象,然后将它的位置传递给你的showMenu函数position属性。

Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => _showPopupMenu((context.findRenderObject() as RenderBox).localToGlobal(Offset.zero)),
      child: Icon(
        Icons.more_vert,
        color: isPressed ? const Color(0xff41D3BD) : const Color(0xffC0C0C0),
      ),
    );
  }
vof42yt1

vof42yt16#

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

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

坐标仍有问题。

相关问题