https://stackoverflow.com/questions/54300081/flutter-popupmenu-on-long-presshtml
import 'package:flutter/material.dart'; void main() => runApp(MaterialApp( home: MyApp(), )); class MyApp extends StatefulWidget { @override State<StatefulWidget> createState() { return MyAppState(); } } class MyAppState extends State<MyApp> { _showMenu(BuildContext context){ showMenu(context: context, position: RelativeRect.fromLTRB(10, 50,50,50), items: [ PopupMenuItem(child: Text('option1'), value: 'btn1',), PopupMenuItem(child: Text('option2'), value: 'btn2',), PopupMenuItem(child: Text('option3'), value: 'btn3',), ]).then((value){ print(value); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('TEST'),), body: Container( width: double.infinity,height: double.infinity, child: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ SizedBox(height: 50.0,), InkWell( onLongPress: (){ _showMenu(context); }, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Container(width: 30, child: Text('BTN'),), Container(width: 30, child: Text('BTN'),), Container(width: 30, child: Text('BTN'),), Container(width: 30, child: Text('BTN'),), ], ),) ], ), ), ), ); } }
The OP and the First Answerer bypassed the original problem using PopupMenuButton
, which worked fine in their case. But I think the more general question of how to position one's own menu and how to receive the user's response without using PopupMenuButton
is worth answering, because sometimes we want a popup menu on a custom widget, and we want it to appear on some gestures other than a simple tap (e.g. the OP's original intention was to long-press).git
I set out to make a simple app demonstrating the following:github
GestureDetector
to capture long-pressshowMenu()
to display a popup menu, and position it near the finger's touchPopupMenuEntry
that represents multiple values (the oft-used PopupMenuItem
can only represent a single value)The result is, when you long-press on a big yellow area, a popup menu appears on which you can select +1
or -1
, and the big number would increment or decrement accordingly:app
Skip to the end for the entire body of code. Comments are sprinkled in there to explain what I am doing. Here are a few things to note:less
showMenu()
's position
parameter takes some effort to understand. It's a RelativeRect
, which represents how a smaller rect is positioned inside a bigger rect. In our case, the bigger rect is the entire screen, the smaller rect is the area of touch. Flutter positions the popup menu according to these rules (in plain English):async
if the smaller rect leans toward the left half of the bigger rect, the popup menu would align with the smaller rect's left edgeide
if the smaller rect leans toward the right half of the bigger rect, the popup menu would align with the smaller rect's right edgeui
if the smaller rect is in the middle, which edge wins depends on the language's text direction. Left edge wins if using English and other left-to-right languages, right edge wins otherwise.this
It's always useful to reference PopupMenuButton
's official implementation to see how it uses showMenu()
to display the menu.spa
showMenu()
returns a Future
. Use Future.then()
to register a callback to handle user selection. Another option is to use await
.
Remember that PopupMenuEntry
is a (subclass of) StatefulWidget
. You can layout any number of sub-widgets inside it. This is how you represent multiple values in a PopupMenuEntry
. If you want it to represent two values, just make it contain two buttons, however you want to lay them out.
To close the popup menu, use Navigator.pop()
. Flutter treats popup menus like a smaller "page". When we display a popup menu, we are actually pushing a "page" to the navigator's stack. To close a popup menu, we pop it from the stack, thus completing the aforementioned Future
.
Here is the full code:
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Popup Menu Usage', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: 'Popup Menu Usage'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { var _count = 0; var _tapPosition; void _showCustomMenu() { final RenderBox overlay = Overlay.of(context).context.findRenderObject(); showMenu( context: context, items: <PopupMenuEntry<int>>[PlusMinusEntry()], position: RelativeRect.fromRect( _tapPosition & Size(40, 40), // smaller rect, the touch area Offset.zero & overlay.size // Bigger rect, the entire screen ) ) // This is how you handle user selection .then<void>((int delta) { // delta would be null if user taps on outside the popup menu // (causing it to close without making selection) if (delta == null) return; setState(() { _count = _count + delta; }); }); // Another option: // // final delta = await showMenu(...); // // Then process `delta` however you want. // Remember to make the surrounding function `async`, that is: // // void _showCustomMenu() async { ... } } void _storePosition(TapDownDetails details) { _tapPosition = details.globalPosition; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ GestureDetector( // This does not give the tap position ... onLongPress: _showCustomMenu, // Have to remember it on tap-down. onTapDown: _storePosition, child: Container( color: Colors.amberAccent, padding: const EdgeInsets.all(100.0), child: Text( '$_count', style: const TextStyle( fontSize: 100, fontWeight: FontWeight.bold), ), ), ), ], ), ), ); } } class PlusMinusEntry extends PopupMenuEntry<int> { @override double height = 100; // height doesn't matter, as long as we are not giving // initialValue to showMenu(). @override bool represents(int n) => n == 1 || n == -1; @override PlusMinusEntryState createState() => PlusMinusEntryState(); } class PlusMinusEntryState extends State<PlusMinusEntry> { void _plus1() { // This is how you close the popup menu and return user selection. Navigator.pop<int>(context, 1); } void _minus1() { Navigator.pop<int>(context, -1); } @override Widget build(BuildContext context) { return Row( children: <Widget>[ Expanded(child: FlatButton(onPressed: _plus1, child: Text('+1'))), Expanded(child: FlatButton(onPressed: _minus1, child: Text('-1'))), ], ); } }