How to set Flutter showMenu starting point

Viewed 14656

I would like to know how to change the origin point of the popUpMenu, start the popup right above the bottom app bar, no matter the count of items. Aligned to the right end of the screen. Something that is like (for example)

Positioned(right: 0, bottom: bottomAppBarHeight)

Here is a screenshot of the design placement of popUpMenu I want to achieve:

Design

Here is a screenshot of the current placement of the popUpMenu (Please ignore other design differences as they are irrelevant):

Actual

The code used is as follows :

                      onPressed: () {
                        final RelativeRect position =
                            buttonMenuPosition(context);
                        showMenu(context: context, position: position, items: [
                          PopupMenuItem<int>(
                            value: 0,
                            child: Text('Working a lot harder'),
                          ),
                          PopupMenuItem<int>(
                            value: 1,
                            child: Text('Working a lot less'),
                          ),
                          PopupMenuItem<int>(
                            value: 1,
                            child: Text('Working a lot smarter'),
                          ),
                        ]);
                      },

The buttonMenuPosition function code:

RelativeRect buttonMenuPosition(BuildContext context) {
    final bool isEnglish =
        LocalizedApp.of(context).delegate.currentLocale.languageCode == 'en';
    final RenderBox bar = context.findRenderObject() as RenderBox;
    final RenderBox overlay =
        Overlay.of(context).context.findRenderObject() as RenderBox;
    const Offset offset = Offset.zero;
    final RelativeRect position = RelativeRect.fromRect(
      Rect.fromPoints(
        bar.localToGlobal(
            isEnglish
                ? bar.size.centerRight(offset)
                : bar.size.centerLeft(offset),
            ancestor: overlay),
        bar.localToGlobal(
            isEnglish
                ? bar.size.centerRight(offset)
                : bar.size.centerLeft(offset),
            ancestor: overlay),
      ),
      offset & overlay.size,
    );
    return position;
  }

Changing the offset didn't work.

4 Answers

Well, I couldn't achieve it with the showMenu function, but it was achievable by using a PopUpMenuButton and setting its offset to the height of the bottom app bar.

Here is an example code:

PopupMenuButton<int>(
    offset: const Offset(0, -380),
    itemBuilder: (context) => [
      PopupMenuItem<int>(
          value: 0,
          child: PopUpMenuTile(
            isActive: true,
            icon: Icons.fiber_manual_record,
            title:'Stop recording',
          )),
      PopupMenuItem<int>(
          value: 1,
          child: PopUpMenuTile(
            isActive: true,
            icon: Icons.pause,
            title: 'Pause recording',
          )),
      PopupMenuItem<int>(
          value: 2,
          child: PopUpMenuTile(
            icon: Icons.group,
            title: 'Members',
          )),
      PopupMenuItem<int>(
          value: 3,
          child: PopUpMenuTile(
            icon: Icons.person_add,
            title: 'Invite members',
          )),
    ],
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        Icon(Icons.more_vert,
            color: Colors.white60),
        Text(translate('more'),
            style: Theme.of(context)
                .textTheme
                .caption)
      ],
    ),
  )

The code to the custom pop up menu tile is as follows even though it is not relevant to the question:

class PopUpMenuTile extends StatelessWidget {
  const PopUpMenuTile(
      {Key key,
      @required this.icon,
      @required this.title,
      this.isActive = false})
      : super(key: key);
  final IconData icon;
  final String title;
  final bool isActive;

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.max,
      mainAxisAlignment: MainAxisAlignment.start,
      children: <Widget>[
        Icon(icon,
            color: isActive
                ? Theme.of(context).accentColor
                : Theme.of(context).primaryColor),
        const SizedBox(
          width: 8,
        ),
        Text(
          title,
          style: Theme.of(context).textTheme.headline4.copyWith(
              color: isActive
                  ? Theme.of(context).accentColor
                  : Theme.of(context).primaryColor),
        ),
      ],
    );
  }
}

I was able to get similar behavior as follows:

    class _SettingsPopupMenuState extends State<SettingsPopupMenu> {
      static const Map<String, IconData> _options = {
       'Settings' : Icons.favorite_border,
       'Share'    : Icons.bookmark_border,
       'Logout'   : Icons.share,
      };

      void _showPopup(BuildContext context) async {
        //*get the render box from the context
        final RenderBox renderBox = context.findRenderObject() as RenderBox;
        //*get the global position, from the widget local position
        final offset = renderBox.localToGlobal(Offset.zero);

        //*calculate the start point in this case, below the button
        final left = offset.dx;
        final top = offset.dy + renderBox.size.height;
        //*The right does not indicates the width
        final right = left + renderBox.size.width;

        //*show the menu
        final value = await showMenu<String>(
          // color: Colors.red,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(20.0)
          ),
          context: context, 
          position: RelativeRect.fromLTRB(left, top, right, 0.0), 
          items: _options.entries.map<PopupMenuEntry<String>>((entry) {
            return PopupMenuItem(
              value: entry.key,
              child: SizedBox(
                // width: 200, //*width of popup
                child: Row(
                  children: [
                    Icon(entry.value, color: Colors.redAccent),
                    const SizedBox(width: 10.0),
                    Text(entry.key)
                  ],
                ),
              ),
            );
          }).toList()
        );

        print(value);
      }

      @override
      Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: const Text('Popup Settings'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              //*encloses the widget from which the relative positions will be 
              //*taken in a builder, in this case a button
              Builder(
                builder: (context) {
                  return RawMaterialButton(
                    fillColor: Colors.indigo,
                    constraints: const BoxConstraints(minWidth: 200),
                    onPressed: () => _showPopup(context),
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(8.0)
                    ),
                    padding: const EdgeInsets.symmetric(vertical: 10.0),
                    child: const Text('Show Menu', style: TextStyle(color: 
    Colors.white)),
                  );
                }
              ),
            ],
          ),
        ),
      );
    }
  }

I know this is old but your code is one line from working and I couldn't see an answer that covered it.

You just need to change the following in the buttonMenuPosition function:

return position;

to

return position.shift(offset);

The one and only working code is here

Offset offs = Offset(0,0);
    final RenderBox button = context.findRenderObject()! as RenderBox;
    final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;
    final RelativeRect position = RelativeRect.fromRect(
      Rect.fromPoints(
        button.localToGlobal(offs, ancestor: overlay),
        button.localToGlobal(button.size.bottomRight(Offset.zero) + offs, ancestor: overlay),
      ),
      Offset.zero & overlay.size,
    );

This is taken from enter image description here

Related