PopupMenuButton menu always has a gap to the side of the screen

Viewed 5998

I want to position the menu of PopupMenuButton to the side of my app, but it always leaves a gap of about 10 logical pixels with the side:
enter image description here

I already tried to give the offset property a negative dx value, but that does not seem to do anything. As far as I know there are no other properties on PopupMenuButton that change the position of the menu.

How can the menu be positioned exactly at the side? Am I missing something obvious here?

BTW: the PopupMenuButton is located in the AppBar of a Scaffold.

This is my code for my PopupMenuButton:

PopupMenuButton(
  color: cardBackground,
  elevation: 0.0,
  shape: RoundedRectangleBorder(
    side: BorderSide(
      width: 0.5,
      color: cardText,
  )),
  padding: EdgeInsets.all(0.0),
  offset: Offset(-10.0, kToolbarHeight),
  onSelected: (value) => doPaletteAction(value),
  itemBuilder: (BuildContext context) => createPopUpItems(),
  icon: Icon(
    Icons.format_list_bulleted_rounded,
    color: appBarIconButtons,
  )
)
3 Answers

Material popup menu margin is hardcoded: PopupMenuButton > PopupMenuButtonState > showMenu > _PopupMenuRoute > _PopupMenuRouteLayout > _kMenuScreenPadding = 8.0

What you can do is

  1. Play around:
  • Open your local popup_menu.dart source file (Ctrl-click on PopupMenuButton in IDE)

  • Replace 8.0 with 0.0, do hot-reload, observe the change

  • Remember to rollback the source back then to keep consistent behaviour

  1. Make a reliable fix:
  • Copy the file to your project, do the fix

  • Also remove relative imports and add import 'package:flutter/material.dart';

  • Then use your fixed "fork" instead of original:

import 'package:myapp/popup_menu.dart' as my_popup_menu;

Scaffold(
  appBar: AppBar(
    elevation: 0,
    leading: my_popup_menu.PopupMenuButton(
      elevation: 0,
      shape: Border.all(width: 0.5),
      offset: Offset(0, kToolbarHeight),
      itemBuilder: (context) => [
        my_popup_menu.PopupMenuItem(
          child: Text('PopupMenuItem'),
        ),
      ],
    ),
  ),
)

The problem lies in popup_menu.dart:

const double _kMenuScreenPadding = 8.0;

The padding is hardcoded, you can however create an alternate CustomPopupMenuButton in your project and change the constant value like in this gist

After adding that class in your code you just switch the implementation to:

CustomPopupMenuButton(...)
 

PopupMenuButton: enter image description here CustomPopupMenuButton: enter image description here

Custom Popup Menu in Flutter The Custom Popup menu looks like this on my Pixel3. It might look different on another phone.

I don't want to waste your time so you can copy-paste the code below. There's a longer explanation further down
Tl;dr
Create an Overlay Entry and display it when the menu button is pressed. .You can add padding and everything else.

import 'package:flutter/material.dart';

class MainScreen extends StatefulWidget {
  @override
  _MainScreenState createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  GlobalKey _globalKey = new GlobalKey();
  OverlayEntry _overlayEntry;
  Size menuSize;
  Offset menuOffset;

  bool isMenuVisible = false;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.white,
        leading: GestureDetector(
            onTap: (){
              (isMenuVisible) ? CloseCustomPopUpMenu() : OpenCustomPopUpMenu();
            },
          child: Container(
            key: _globalKey,
            height: 20,
            width: 20,
            child: Icon(
            Icons.format_list_bulleted_rounded,
            color: Colors.black,
          ))
        ),
        title: Text("Pro Color Palette",
        style: TextStyle(color: Colors.black),),
      ),

      body: Text("This took long \u2639"),
    );
  }
  void OpenCustomPopUpMenu() {
    RenderBox renderBox = _globalKey.currentContext.findRenderObject();
    menuSize = renderBox.size*2;
    menuOffset = renderBox.localToGlobal(Offset(0,-55));
    _overlayEntry = _createOverlayEntry();
    Overlay.of(context).insert(_overlayEntry);
    isMenuVisible = !isMenuVisible;
  }

  void CloseCustomPopUpMenu() {
    _overlayEntry.remove();
    isMenuVisible = !isMenuVisible;
  }

  OverlayEntry _createOverlayEntry() {
    return OverlayEntry(builder: (context) {
      return Positioned(
        top: menuOffset.dy + menuSize.height,
        left: menuOffset.dx,
        width:menuSize.width *2,
        child: Material(
            color: Colors.transparent,
            child: CustomPopupMenu(),
        ),
      );
    },
    );
  }
}
class CustomPopupMenu extends StatefulWidget {
  @override
  _CustomPopupMenuState createState() => _CustomPopupMenuState();
}

final Container ImprovisedDivider = Container(
  height: 1,
  width:300,
  color: Colors.black,);
class _CustomPopupMenuState extends State<CustomPopupMenu> {
  @override
  Widget build(BuildContext context) {
    return Container(
      height: 175,
      color: Colors.white,
      child: Column(
        children:<Widget>[
          ImprovisedDivider,
          ListTile(
            leading:Icon(
              Icons.save,
              color: Colors.black,
              size:20 ,),
            contentPadding: EdgeInsets.only(left: 1.0),
           title:Text("Save palette",),
          ),
          ImprovisedDivider,
          ListTile(
            leading:Icon(
              Icons.delete,
              color: Colors.black,
              size:20 ,),
            contentPadding: EdgeInsets.only(left: 1.0),
            title:Text("Clear palette"),
          ),
          ImprovisedDivider,
          ListTile(
            leading:Icon(
              Icons.upload_file,
              color: Colors.black,
              size:20 ,),
            contentPadding: EdgeInsets.only(left: 1.0),
            title:Text("Upload a palette"),
          ),
          ImprovisedDivider,
        ],
      ),
    );
  }
}

Long explanation You need an overlay to build a custom popup menu. Overlay Docs. It lets you build widgets that float above other widgets. We'll need a GlobalKey to identify the container holding the menu button. We also need a Gesture Detector to identify screen taps.

Steps:

  1. Create menu button, declare variables for the GlobalKey, Overlay, size of the menu and offset of the menu.
class MainScreen extends StatefulWidget {
  @override
  _MainScreenState createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> 
{
    //Initialize global key
      GlobalKey _globalKey = new GlobalKey();
    //Create new overlay entry
      OverlayEntry _overlayEntry;
    //Store the size of the menu
      Size menuSize;
    //Store the menu's offset
      Offset menuOffset;
    //This is used to ooen and close the menu
      bool isMenuVisible = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.white,
        leading: GestureDetector(
            onTap: (){
              (isMenuVisible) ? CloseCustomPopUpMenu() : OpenCustomPopUpMenu();
            },
          child: Container(
            key: _globalKey,//
            height: 20,
            width: 20,
            child: Icon(
            Icons.format_list_bulleted_rounded,
            color: Colors.black,
          ))
        ),
        title: Text("Pro Color Palette $isMenuVisible",
        style: TextStyle(color: Colors.black),),
      ),

      body: Text("....."),
    );
  }
  1. Create a Custom PopupMenu widget.(I'm using a bunch of ListTiles and improvised dividers). Note, we can only change the menu's height. It inherits width from it's parent.
class CustomPopupMenu extends StatefulWidget {
  @override
  _CustomPopupMenuState createState() => _CustomPopupMenuState();
}

final Container ImprovisedDivider = Container(
  height: 1,
  width:300,
  color: Colors.black,);

class _CustomPopupMenuState extends State<CustomPopupMenu> {
  @override
  Widget build(BuildContext context) {
    return Container(
      height: 180,//Change Menu height here
      color: Colors.white,
      child: Column(
        children:<Widget>[
          ImprovisedDivider,
          ListTile(
            leading:Icon(
              Icons.save,
              color: Colors.black,
              size:20 ,),
            contentPadding: EdgeInsets.only(left: 1.0),
           title:Text("Save palette",),
          ),
          ImprovisedDivider,
          ListTile(
            leading:Icon(
              Icons.delete,
              color: Colors.black,
              size:20 ,),
            contentPadding: EdgeInsets.only(left: 1.0),
            title:Text("Clear palette"),
          ),
          ImprovisedDivider,
          ListTile(
            leading:Icon(
              Icons.upload_file,
              color: Colors.black,
              size:20 ,),
            contentPadding: EdgeInsets.only(left: 1.0),
            title:Text("Upload a palette"),
          ),
          ImprovisedDivider,
        ],
      ),
    );
  }
}
  1. Create the overlay entry int it's own function.We use a Positioned widget to put it in a specific place on the screen.
OverlayEntry _createOverlayEntry() {
    return OverlayEntry(builder: (context) {
      return Positioned(
        top: menuOffset.dy + menuSize.height,
        left: menuOffset.dx,
        width:menuSize.width *2,//Change menu width here
        child: Material(
            color: Colors.transparent,
            child: CustomPopupMenu(),//Call our Custom menu here
        ),
      );
    },
    );
  }

  1. Logic to display the menu(Also change width and offset from edge of screen).The global key is useful here. It allows us to get information about the parent container like it's position and it's size. Note
void OpenCustomPopUpMenu() {
   RenderBox renderBox = _globalKey.currentContext.findRenderObject();
   menuSize = renderBox.size*2;//Change width here
   menuOffset = renderBox.localToGlobal(Offset(0,-60));//Change offset from screen edge here
   _overlayEntry = _createOverlayEntry();
   Overlay.of(context).insert(_overlayEntry);
   isMenuVisible = !isMenuVisible;
 }
  1. Logic to close the menu
  
  void CloseCustomPopUpMenu() {
    _overlayEntry.remove();
    isMenuVisible = !isMenuVisible;
  }
Related