'Flutter Override PopupMenuButton Widget to prevent soft keyboard to close
In my flutter mobile application, I use a PopupMenuButton placed at the bottom of the view. If I click on it, the soft keyboard is dismissed because the text input looses the focus I guess. So the popup menu does not show at the right place.
I would like to override this behavior to prevent PopupMenuButton to close the keyboard. Maybe by extending the PopupMenuButton class ? But I don't really know how to do it.
Keyboard open before click on button:
Menu not in the right place:
Solution 1:[1]
Here i have found work around for these type of functionality
popup_menu.dart
import 'dart:core';
import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
import '../utils/color_utils.dart';
import 'triangle_painter.dart';
abstract class MenuItemProvider {
String get menuTitle;
Widget get menuImage;
TextStyle get menuTextStyle;
TextAlign get menuTextAlign;
}
class MenuItem extends MenuItemProvider {
Widget image;
String title;
var userInfo;
TextStyle textStyle;
TextAlign textAlign;
MenuItem(
{this.title, this.image, this.userInfo, this.textStyle, this.textAlign});
@override
Widget get menuImage => image;
@override
String get menuTitle => title;
@override
TextStyle get menuTextStyle =>
textStyle ?? TextStyle(color: Color(0xffc5c5c5), fontSize: 10.0);
@override
TextAlign get menuTextAlign => textAlign ?? TextAlign.center;
}
enum MenuType { big, oneLine }
typedef MenuClickCallback = Function(MenuItemProvider item);
typedef PopupMenuStateChanged = Function(bool isShow);
class PopupMenu {
static var itemWidth = 72.0;
static var itemHeight = 65.0;
static var arrowHeight = 10.0;
OverlayEntry _entry;
List<MenuItemProvider> items;
/// row count
int _row;
/// col count
int _col;
/// The left top point of this menu.
Offset _offset;
/// Menu will show at above or under this rect
Rect _showRect;
/// if false menu is show above of the widget, otherwise menu is show under the widget
bool _isDown = true;
/// The max column count, default is 4.
int _maxColumn;
/// callback
VoidCallback dismissCallback;
MenuClickCallback onClickMenu;
PopupMenuStateChanged stateChanged;
Size _screenSize;
/// Cannot be null
static BuildContext context;
/// style
Color _backgroundColor;
Color _highlightColor;
Color _lineColor;
/// It's showing or not.
bool _isShow = false;
bool get isShow => _isShow;
PopupMenu(
{MenuClickCallback onClickMenu,
BuildContext context,
VoidCallback onDismiss,
int maxColumn,
Color backgroundColor,
Color highlightColor,
Color lineColor,
PopupMenuStateChanged stateChanged,
List<MenuItemProvider> items}) {
this.onClickMenu = onClickMenu;
this.dismissCallback = onDismiss;
this.stateChanged = stateChanged;
this.items = items;
this._maxColumn = maxColumn ?? 4;
this._backgroundColor = backgroundColor ?? Color(0xff232323);
this._lineColor = lineColor ?? Color(0xff353535);
this._highlightColor = highlightColor ?? Color(0x55000000);
if (context != null) {
PopupMenu.context = context;
}
}
void show({Rect rect, GlobalKey widgetKey, List<MenuItemProvider> items}) {
if (rect == null && widgetKey == null) {
print("'rect' and 'key' can't be both null");
return;
}
this.items = items ?? this.items;
this._showRect = rect ?? PopupMenu.getWidgetGlobalRect(widgetKey);
this._screenSize = window.physicalSize / window.devicePixelRatio;
this.dismissCallback = dismissCallback;
_calculatePosition(PopupMenu.context);
_entry = OverlayEntry(builder: (context) {
return buildPopupMenuLayout(_offset);
});
Overlay.of(PopupMenu.context).insert(_entry);
_isShow = true;
if (this.stateChanged != null) {
this.stateChanged(true);
}
}
static Rect getWidgetGlobalRect(GlobalKey key) {
RenderBox renderBox = key.currentContext.findRenderObject();
var offset = renderBox.localToGlobal(Offset.zero);
return Rect.fromLTWH(
offset.dx, offset.dy, renderBox.size.width, renderBox.size.height);
}
void _calculatePosition(BuildContext context) {
_col = _calculateColCount();
_row = _calculateRowCount();
_offset = _calculateOffset(PopupMenu.context);
}
Offset _calculateOffset(BuildContext context) {
double dx = _showRect.left + _showRect.width / 2.0 - menuWidth() / 2.0;
if (dx < 10.0) {
dx = 10.0;
}
if (dx + menuWidth() > _screenSize.width && dx > 10.0) {
double tempDx = _screenSize.width - menuWidth() - 10;
if (tempDx > 10) dx = tempDx;
}
double dy = _showRect.top - menuHeight();
if (dy <= MediaQuery.of(context).padding.top + 10) {
// The have not enough space above, show menu under the widget.
dy = arrowHeight + _showRect.height + _showRect.top;
_isDown = false;
} else {
dy -= arrowHeight;
_isDown = true;
}
return Offset(dx, dy);
}
double menuWidth() {
itemWidth =
_textSize(items.first.menuTitle, items.first.menuTextStyle).width;
return itemWidth;
}
Size _textSize(String text, TextStyle style) {
var textPainter = TextPainter(
text: TextSpan(text: text, style: style),
maxLines: 10,
textDirection: TextDirection.ltr)
..layout(minWidth: 0, maxWidth: 210.0);
return textPainter.size;
}
// This height exclude the arrow
double menuHeight() {
itemHeight =
_textSize(items.first.menuTitle, items.first.menuTextStyle).height + 2;
return itemHeight;
}
LayoutBuilder buildPopupMenuLayout(Offset offset) {
return LayoutBuilder(builder: (context, constraints) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
dismiss();
},
onVerticalDragStart: (DragStartDetails details) {
dismiss();
},
onHorizontalDragStart: (DragStartDetails details) {
dismiss();
},
child: Container(
child: Stack(
children: <Widget>[
// triangle arrow
Positioned(
left: _showRect.left + _showRect.width / 2.0 - 7.5,
top: _isDown
? offset.dy + menuHeight()
: offset.dy - arrowHeight,
child: CustomPaint(
size: Size(15.0, arrowHeight),
painter:
TrianglePainter(isDown: _isDown, color: _backgroundColor),
),
),
// menu content
Positioned(
left: offset.dx,
top: offset.dy,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
boxShadow: [
BoxShadow(
color: shadowColor,
offset: Offset(0, 0),
blurRadius: 8,
spreadRadius: 0)
],
),
width: menuWidth(),
height: menuHeight(),
child: Column(
children: <Widget>[
ClipRRect(
borderRadius: BorderRadius.circular(10.0),
child: Container(
width: menuWidth(),
height: menuHeight(),
decoration: BoxDecoration(
color: _backgroundColor,
borderRadius: BorderRadius.circular(10.0)),
child: Column(
children: _createRows(),
),
)),
],
),
),
)
],
),
),
);
});
}
List<Widget> _createRows() {
List<Widget> rows = [];
for (int i = 0; i < _row; i++) {
Color color =
(i < _row - 1 && _row != 1) ? _lineColor : Colors.transparent;
Widget rowWidget = Container(
decoration:
BoxDecoration(border: Border(bottom: BorderSide(color: color))),
height: itemHeight,
child: Row(
children: _createRowItems(i),
),
);
rows.add(rowWidget);
}
return rows;
}
List<Widget> _createRowItems(int row) {
List<MenuItemProvider> subItems =
items.sublist(row * _col, min(row * _col + _col, items.length));
List<Widget> itemWidgets = [];
int i = 0;
for (var item in subItems) {
itemWidgets.add(_createMenuItem(
item,
i < (_col - 1),
));
i++;
}
return itemWidgets;
}
// calculate row count
int _calculateRowCount() {
if (items == null || items.length == 0) {
debugPrint('error menu items can not be null');
return 0;
}
int itemCount = items.length;
if (_calculateColCount() == 1) {
return itemCount;
}
int row = (itemCount - 1) ~/ _calculateColCount() + 1;
return row;
}
// calculate col count
int _calculateColCount() {
if (items == null || items.length == 0) {
debugPrint('error menu items can not be null');
return 0;
}
int itemCount = items.length;
if (_maxColumn != 4 && _maxColumn > 0) {
return _maxColumn;
}
if (itemCount == 4) {
return 2;
}
if (itemCount <= _maxColumn) {
return itemCount;
}
if (itemCount == 5) {
return 3;
}
if (itemCount == 6) {
return 3;
}
return _maxColumn;
}
double get screenWidth {
double width = window.physicalSize.width;
double ratio = window.devicePixelRatio;
return width / ratio;
}
Widget _createMenuItem(MenuItemProvider item, bool showLine) {
return _MenuItemWidget(
item: item,
showLine: showLine,
clickCallback: itemClicked,
lineColor: _lineColor,
backgroundColor: _backgroundColor,
highlightColor: _highlightColor,
);
}
void itemClicked(MenuItemProvider item) {
if (onClickMenu != null) {
onClickMenu(item);
}
dismiss();
}
void dismiss() {
if (!_isShow) {
// Remove method should only be called once
return;
}
_entry.remove();
_isShow = false;
if (dismissCallback != null) {
dismissCallback();
}
if (this.stateChanged != null) {
this.stateChanged(false);
}
}
}
class _MenuItemWidget extends StatefulWidget {
final MenuItemProvider item;
final bool showLine;
final Color lineColor;
final Color backgroundColor;
final Color highlightColor;
final Function(MenuItemProvider item) clickCallback;
_MenuItemWidget(
{this.item,
this.showLine = false,
this.clickCallback,
this.lineColor,
this.backgroundColor,
this.highlightColor});
@override
State<StatefulWidget> createState() {
return _MenuItemWidgetState();
}
}
class _MenuItemWidgetState extends State<_MenuItemWidget> {
var highlightColor = Color(0x55000000);
var color = Color(0xff232323);
@override
void initState() {
color = widget.backgroundColor;
highlightColor = widget.highlightColor;
super.initState();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (details) {
color = highlightColor;
setState(() {});
},
onTapUp: (details) {
color = widget.backgroundColor;
setState(() {});
},
onLongPressEnd: (details) {
color = widget.backgroundColor;
setState(() {});
},
onTap: () {
if (widget.clickCallback != null) {
widget.clickCallback(widget.item);
}
},
child: Container(
padding: EdgeInsets.all(2.0),
width: PopupMenu.itemWidth,
height: PopupMenu.itemHeight,
decoration: BoxDecoration(
color: color,
border: Border(
right: BorderSide(
color: widget.showLine
? widget.lineColor
: Colors.transparent))),
child: _createContent()),
);
}
Widget _createContent() {
if (widget.item.menuImage != null) {
// image and text
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 30.0,
height: 30.0,
child: widget.item.menuImage,
),
Container(
height: 22.0,
child: Material(
color: Colors.transparent,
child: Text(
widget.item.menuTitle,
style: widget.item.menuTextStyle,
),
),
)
],
);
} else {
// only text
return Container(
child: Center(
child: Material(
color: Colors.transparent,
child: Text(
widget.item.menuTitle,
style: widget.item.menuTextStyle,
textAlign: widget.item.menuTextAlign,
),
),
),
);
}
}
}
triangle_painter.dart
import 'package:flutter/rendering.dart';
class TrianglePainter extends CustomPainter {
bool isDown;
Color color;
TrianglePainter({this.isDown = true, this.color});
@override
void paint(Canvas canvas, Size size) {
var _paint = Paint();
_paint.strokeWidth = 2.0;
_paint.color = color;
_paint.style = PaintingStyle.fill;
var path = Path();
if (isDown) {
path.moveTo(0.0, -1.0);
path.lineTo(size.width, -1.0);
path.lineTo(size.width / 2.0, size.height);
} else {
path.moveTo(size.width / 2.0, 0.0);
path.lineTo(0.0, size.height + 1);
path.lineTo(size.width, size.height + 1);
}
canvas.drawPath(path, _paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
Example :-
class PopUpDemoScreen extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _PopUpDemoScreenState();
}
}
class _PopUpDemoScreenState extends State<PopUpDemoScreen> {
PopupMenu menu;
GlobalKey btnKey = GlobalKey();
BuildContext context;
@override
Widget build(BuildContext context) {
this.context = context;
return Scaffold(
body: Container(
child: Center(
child: FlatButton(
key: btnKey,
color: Colors.blue,
textColor: Colors.white,
disabledColor: Colors.grey,
disabledTextColor: Colors.black,
padding: EdgeInsets.all(8.0),
splashColor: Colors.blueAccent,
onPressed: showPopup,
child: Text(
"Show Popup",
style: TextStyle(fontSize: 20.0),
),
),
),
),
);
}
void showPopup() {
menu = PopupMenu(
context: context,
backgroundColor: Colors.white,
items: [
MenuItem(
title:
'Pellentesque nec interdum ipsum. Sed tempus ante nec augue aliquam, ullamcorper'
,textStyle: TextStyle(
color: Color(0xff333333),
fontSize: 12,
fontWeight: FontWeight.w300,
fontStyle: FontStyle.normal,
))
],
);
menu.show(widgetKey: btnKey);
}
Widget _buildButtonWidget(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(),
);
}
}
OutPut:-
Solution 2:[2]
Seems to be the same issue as flutter/issues/24843 & flutter/issues/50567.
A potential solution would be using the keep_keyboard_popup_menu package, which was uploaded a couple of days ago by PegasisForever.
Solution 3:[3]
I have used showMenu method instead of PopupMenuButton, and before call showMenu method I did this:
if (FocusScope.of(context).hasFocus) {
Future.delayed(Duration(milliseconds: 50)).whenComplete(() => FocusScope.of(context).requestFocus());
}
showMyPopupMenu(context);
and it's work fine.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Ankit Mahadik |
Solution 2 | Jon |
Solution 3 |