18

Is it possible to dynamically create a popup menu (PopupMenuButton) by pressing the action button and display this menu in the middle of the screen? For example, how modify for standard flutter application to implement this scenario:

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

I managed to advance somewhat in solving the problem, but still there are questions. Here is the text of main.dart. By clicking on the canvas, the _showPopupMenu3 (context) function is called from _handleTapDown (...). The menu does appear, I can catch option, but after selecting the menu isn't closed. To close menu need to press the BACK button or click on the canvas. This probably corresponds to the CANCEL situation. So the questions are: 1) How to close the menu after selecting of item (maybe it's just some parameter of property of menu)? 2) The purpose and meaning of the coordinates that should be passed to the position parameter is not quite clear. How do I raise the menu next to the click coordinate?

Sources:

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),
//        ),
      ),
    );
  }
}
Michael Kanzieper
  • 709
  • 1
  • 10
  • 20

6 Answers6

34

Yes it is possible

    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);
       });
    }

There will be times when you would want to display _showPopupMenu at the location where you pressed on the button Use GestureDetector for that

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

and then _showPopupMenu will be like

_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,
  );
}
sreeramu
  • 1,213
  • 12
  • 19
Vishal Singh
  • 1,341
  • 14
  • 13
  • Where did you face problem in the second part ? – Vishal Singh Jan 29 '20 at 05:51
  • 2
    @Vishual I had a problem when I tap the gestureDetector widget, no popup menu show. I dont know if the RaiseDButton widget I used as a child of the GestureDetector widget was the cause – CanCoder Jan 29 '20 at 05:53
  • 1
    right and bottom are set to 0 which will cause the menu to always appear at the bottom right corner – ibrahim Feb 22 '20 at 18:05
10

@Vishal Singh's answer needs two improvements:

  1. If you use 0 for right, then the menu is aligned to the right because

Horizontally, the menu is positioned so that it grows in the direction that has the most room. For example, if the position describes a rectangle on the left edge of the screen, then the left edge of the menu is aligned with the left edge of the position, and the menu grows to the right.

  1. If you use 0 for bottom, this works fine with a popup menu without initialValue but moves the menu far down if initialValue is set. This is because

If initialValue is specified then the first item with a matching value will be highlighted and the value of position gives the rectangle whose vertical center will be aligned with the vertical center of the highlighted item (when possible).

If initialValue is not specified then the top of the menu will be aligned with the top of the position rectangle.

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

So for a more universal solution calculate the right and the bottom correctly:

  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: [
      // ...
    ],
  );
Alexey Inkin
  • 1,853
  • 1
  • 12
  • 32
4

@Vishal Singh answer was really helpful. However, I had the problem that the menu was always on the right. Giving the right value a really high value fixed it, Example:

_showPopupMenu(Offset position) async {
    await showMenu(
        context: context,
        position: RelativeRect.fromLTRB(position.dx, position.dy, 100000, 0),
        ...
Joe
  • 43
  • 4
1

PopMenu on Custom click

PopupMenu

Use async method and showMenu() widget

 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);
    });
  }

The widget code where from you called popup

InkWell(
                                          onTap: () {
                                            showMemberMenu();
                                          },
                                          child: Text('Switch Profile',
                                              style: TextStyle(
                                                  color: Colors.blue,
                                                  fontFamily: 'Roboto1',
                                                  fontSize: 10.sp)),
                                        ),
Hossein Azem
  • 331
  • 3
  • 8
Nisha Jain
  • 637
  • 6
  • 14
1

In addition to Vishal Singh's answer, if you don't want your menu to appear in a position related to the offset where you tapped, but rather in relation to the widget (the one you use as the menu opener) returned by your overridden build function, use your context to find its render object and then pass its position to your showMenu function position property.

  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),
      ),
    );
  }
Iván Yoed
  • 3,878
  • 31
  • 44
-1

Close the menu is quite simple: need to add a line: Navigator.pop (context);

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

Remains question with the coordinates.

Michael Kanzieper
  • 709
  • 1
  • 10
  • 20