showMenu position Flutter - flutter

i have a GridView Widget that contain some GridTiles that are wrapped with GestureDetector trying to showUp a menu to delete the GridTile when i have longPress on it ,,, everything is fine except that i want that menu to be shown from the point that i have clicked into not at the top of the app
showMenu(
context: context,
position: ..........,// Here i want the solution
items: [
PopupMenuItem(
child: FlatButton.icon(
onPressed: () {
_notesProv.deleteNote(id);
Navigator.of(context).pop();
},
icon: Icon(
Icons.delete,
color: Colors.black,
),
label: Text(
'delete note',
style: TextStyle(color: Colors.black),
),
),
),
],
color: Colors.green[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
));

See comments in the code for guidance on what's going on.
Wherever there's a comment, that code was added to the example Gridview stolen from Flutter official cookbook in order to pop up the menu where you tapped.
class GridPositionPage extends StatefulWidget {
#override
_GridPositionPageState createState() => _GridPositionPageState();
}
class _GridPositionPageState extends State<GridPositionPage> {
// ↓ hold tap position, set during onTapDown, using getPosition() method
Offset tapXY;
// ↓ hold screen size, using first line in build() method
RenderBox overlay;
#override
Widget build(BuildContext context) {
// ↓ save screen size
overlay = Overlay.of(context).context.findRenderObject();
return BaseExamplePage(
title: 'Grid Position',
child: GridView.count(
crossAxisCount: 2,
children: List.generate(100, (index) {
return Center(
child: InkWell(
child: Text(
'Item $index',
style: Theme.of(context).textTheme.headline5,
),
// ↓ save screen tap position now
onTapDown: getPosition,
onLongPress: () => showMenu(
context: context,
position: relRectSize,
items: [
PopupMenuItem(
child: FlatButton.icon(
label: Text('Delete'),
icon: Icon(Icons.delete),
),
)
]
),
),
);
}),
),
);
}
// ↓ create the RelativeRect from size of screen and where you tapped
RelativeRect get relRectSize => RelativeRect.fromSize(tapXY & const Size(40,40), overlay.size);
// ↓ get the tap position Offset
void getPosition(TapDownDetails detail) {
tapXY = detail.globalPosition;
}
}

This function handles everything, you just need to use the findRenderObject method to get the RenderObject on each grid item:
void showGridMenu() async {
Rect? rect;
RenderBox? overlay = Overlay.of(context).context.findRenderObject() as RenderBox;
final renderObject = context.findRenderObject();
final translation = renderObject?.getTransformTo(null).getTranslation();
if (translation != null && renderObject?.paintBounds != null) {
final offset = Offset(translation.x, translation.y);
rect = renderObject!.paintBounds.shift(offset);
}
if (rect != null) {
var value = await showMenu<String>(
context: context,
position: RelativeRect.fromRect(
rect,
Offset.zero & overlay.size,
),
items: [
const PopupMenuItem(value: 'yes', child: Text('Yes')),
const PopupMenuItem(value: 'no', child: Text('No')),
],
);
if (value != null) {
debugPrint(value);
}
}
}

position: RelativeRect.fromSize(
Rect.fromCenter(
center: Offset.zero, width: 100, height: 100),
Size(100, 100),
),
Somthing like that

Related

flutter_bloc is not resetting its Cubit Model values when we try to push the same screen more than one time

Here is my main.dart code,
void main() {
// Bloc.observer = AppBlocObserver();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<SignInCubit>(
create: (BuildContext context) => SignInCubit(),
),
BlocProvider<SignUpCubit>(
create: (BuildContext context) => SignUpCubit(),
),
],
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return OrientationBuilder(
builder: (BuildContext context, Orientation orientation) {
//sizeConfiguration
SizeConfig().init(constraints, orientation);
return MaterialApp(
title: 'Flutter Demo',
theme: lightThemeData(context),
darkTheme: darkThemeData(context),
// themeMode: _themeMode,
scaffoldMessengerKey: Messenger.rootScaffoldMessengerKey,
navigatorKey: CustomRoutes.navigatorKey,
home: const SplashPage(),
initialRoute: SplashPage.id,
routes: <String, WidgetBuilder> {
SplashPage.id:(BuildContext context) => const SplashPage(),
SignInPage.id: (BuildContext context) => const SignInPage(),
SignUpPage.id: (BuildContext context) => const SignUpPage(),
},
);
});
}),
);
}
Here is the code on SignIn screen,
class SignInPage extends StatefulWidget {
const SignInPage({super.key});
static const String id = 'SignInPage';
#override
State<SignInPage> createState() => _SignInPageState();
}
class _SignInPageState extends State<SignInPage> {
final dynamic _formKey = GlobalKey<FormState>();
final TextEditingController _passwordController = TextEditingController();
#override
void initState() {
createFocusNodeListeners();
SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
_passwordFocus.addListener(_passwordListener);
});
super.initState();
}
#override
Widget build(BuildContext context) {
final CTTheme cTTheme = Theme.of(context).extension<CTTheme>()!;
//https://medium.com/codex/flutter-bloc-an-introduction-with-cubit-7eae1e740fd0
final SignInCubitModel state = context.watch<SignInCubit>().state;
return Scaffold(
body: CustomPaint(
painter:
LoginCustomPaint(customPaintColor: cTTheme.customPaintCircleColor1),
child: GestureDetector(
onTap: () {
FocusScope.of(context).unfocus();
},
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24), // common padding
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const SizedBox(
height: 98,
),
Text(
AppStrings.welcomeTo, // heading 3
style: cTTheme.heading3,
),
const SizedBox(
height: 16,
),
RichText(
text: TextSpan(
text: AppStrings.m,
style: cTTheme.heading1, // heading1 with pink color
children: <TextSpan>[
TextSpan(
text: AppStrings.onteFiore,
style: cTTheme.heading2, //heading2 with pink color
),
TextSpan(
text: '\n${AppStrings.c}', style: cTTheme.heading1),
TextSpan(
text: AppStrings.linical, style: cTTheme.heading2),
TextSpan(
text: ' ${AppStrings.t}', style: cTTheme.heading1),
TextSpan(
text: AppStrings.rials, style: cTTheme.heading2),
],
),
),
const SizedBox(
height: 16,
),
Text(
AppStrings.findOutClinicalTrailAndStudies,
style: cTTheme.subtitle3, // subtitle 1
),
SizedBox(
height: SizeConfig.heightMultiplier! * 19.8,
),
TextFormField(
autofocus: false,
style:cTTheme.menuItemTextStyle,
controller: _passwordController,
autovalidateMode: state.passwordValidation
? AutovalidateMode.onUserInteraction
: AutovalidateMode.disabled,
obscureText: !state.signInPasswordVisibility,
// obscuringCharacter: '*',
focusNode: _passwordFocus,
decoration: kTextFieldDecoration2(context).copyWith(
labelText: AppStrings.password,
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: state.isInvalidPassWord
? state.passWordTextFormFieldColor
: cTTheme.dividerColor!)),
prefixIcon: Padding(
padding: const EdgeInsetsDirectional.only(end: 16.0),
child: Image.asset(// constants
AppIconsPath.padLockIcon),
),
suffixIcon: InkWell(
onTap: () {
context
.read<SignInCubit>()
.changeSignINPassowrdVisibilty(
state.signInPasswordVisibility
? ChangePasswordVisibilityEnum.invisible
: ChangePasswordVisibilityEnum.visible);
},
child: state.signInPasswordVisibility
? Image.asset(AppIconsPath.eyeIcon)
: Image.asset(AppIconsPath.privacyIcon),
),
),
onTap: () => context
.read<SignInCubit>()
.changePasswordTextFormFieldColor(
color: cTTheme.dividerColor!,
isInvalidPassWord: false),
onChanged: (String? value) {
_debouncer.run(() {
checkControllerValues();
});
},
onEditingComplete: () {
if (Validators.validateSignInPassword(
_passwordController.text) !=
null) {
context
.read<SignInCubit>()
.enableValidatorForPasswordTextField();
}
},
validator: (String? value) =>
Validators.validateSignInPassword(value!),
),
errorMessage(state.isInvalidPassWord, 'Password Is Incorrect'),
const SizedBox(
height: 14,
),
Align(
alignment: Alignment.bottomRight,
child: InkWell(
onTap: () async {
final bool isConnocted = await checkConnectivity();
if (isConnocted) {
CustomRoutes.push(
screen: const ForgotPasswordPage());
return;
}
CustomRoutes.push(
screen: InternetErrorScreen(
onPressed: () {
ConnectivityManager.internetChecker()
.then((bool retry) {
if (retry) {
CustomRoutes.back();
}
});
},
),
);
},
child: Text(
AppStrings.forgotPassword,
style: cTTheme.description1,
))),
const SizedBox(
height: 48,
),
PrimaryButton(
onPressed: state.isSignInButtonDissabled
? null
: () {
FocusManager.instance.primaryFocus?.unfocus();
if (_formKey.currentState!.validate()) {
context.read<SignInCubit>().onSignInSubmit(
_emailOrPhoneController.text,
_passwordController.text);
}
},
text: AppStrings.signIn.toUpperCase(),
isLoadingVisible: state.isLoading,
),
const SizedBox(
height: 16,
),
InkWell(
onTap: () async {
final bool isConnocted = await checkConnectivity();
if (isConnocted) {
Navigator.pushNamed(context, SignUpPage.id);
// CustomRoutes.pushreplace(screen: const SignUpPage());
return;
}
CustomRoutes.push(
screen: InternetErrorScreen(onPressed: () {
ConnectivityManager.internetChecker()
.then((bool retry) {
if (retry) {
CustomRoutes.back();
}
});
}));
},
child: Center(
child: RichText(
text: TextSpan(
text: AppStrings.dontHaveAnAccount,
style: cTTheme.subtitle2,
children: <TextSpan>[
TextSpan(
text: ' ${AppStrings.signUp}',
style: cTTheme.iconButtonTextStyle),
],
),
),
),
),
const SizedBox(
height: 66,
),
Center(
child: Text(
AppStrings.byLoggingInYouAgreeToOur,
style: cTTheme.noteTextStyle1,
)),
const SizedBox(
height: 6,
),
Center(
child: InkWell(
onTap: () => CustomRoutes.push(
screen: InternetErrorScreen(onPressed: () {
ConnectivityManager.internetChecker().then((bool value) {
if (value) {
CustomRoutes.back();
}
});
})),
child: Text(
AppStrings.termsAndpolicies,
style: cTTheme.noteTextStyle2,
),
)),
const SizedBox(
height: 20,
),
],
),
),
),
),
),
));
}
I am switching(show/hide) eye icon on the password textfield to show and hide the eye image using "changeSignINPassowrdVisibilty" function from cubit file. Here is the Cubit file code,
class SignInCubit extends Cubit<SignInCubitModel> {
SignInCubit()
: super(
SignInCubitModel()); // here inside SignInCubit class, all the fields are already initialized
final CTTheme cTTheme =
Theme.of(CustomRoutes.navigatorKey.currentContext!).extension<CTTheme>()!;
void changeSignINPassowrdVisibilty(ChangePasswordVisibilityEnum newState) {
emit(state.copyWith(
signInPasswordVisibility:
newState == ChangePasswordVisibilityEnum.visible));
}
}
Here is the code from SignInCubitModel class,
class SignInCubitModel {
SignInCubitModel({
this.signInPasswordVisibility = false,
});
bool signInPasswordVisibility;
SignInCubitModel copyWith({
bool? signInPasswordVisibility,
}) {
return SignInCubitModel(
signInPasswordVisibility:
signInPasswordVisibility ?? this.signInPasswordVisibility,
);
}
}
Initially signInPasswordVisibility field value is false. When user try to switch visibility of the password , signInPasswordVisibility will change its value to true or false.
Here is my problem,
1.I am enabling signInPasswordVisibility to true on SignIn screen by clicking eye icon. Please note now signInPasswordVisibility value is true.
And
2.I am pushing to Signup screen when they click signup button from SignIn screen.
And
3.I am pushing to SignIn screen again when they click SignIn button from signup screen.
But this time signInPasswordVisibility value in SignIn screen still remains true. Its supposed to be false since I am pushing to this screen again. It has to reset the all SignIn screen state values when I push to SignIn screen. But Its not happening.
Signin screen still keeps old state values even if I push to the Signin screen multiple time.
Is there any problem with the way I implemented flutter_bloc? OR Is there any solution to reset the state values every time we push the SignIn screen?

How to remove popup when I move the cursor away from the button in Flutter?

I build this code below that makes possible to show my Popupmenubuttons Items when I put the cursor over the button without clicking the button. But there is a problem. I can show my items with this method but I can not make possible to close my items in the moment I move my cursor from the button. Please look carefully my code and tell me if you have any idea how to solve this.
openPopUpItem2() {
GestureDetector? detector;
searchForGestureDetector(BuildContext element) {
element.visitChildElements((element) {
if (element.widget != null && element.widget is GestureDetector) {
detector = element.widget as GestureDetector;
} else {
searchForGestureDetector(element);
}
});
}
searchForGestureDetector(keyList[2].currentContext!);
detector!.onTap!();
}
The code below shows how I have used the function to show my popupItems.
InkWell(
onHover: (value) {
if (value) openPopUpItem2();
},
onTap: () {},
child: PopupMenuButton(
key: keyList[2],
tooltip: '',
color: Color(0xFF262533),
position: PopupMenuPosition.under,
child: MouseRegion(
onEnter: (details) =>
setState(() => ishovering2 = true),
onExit: (details) => setState(() {
ishovering2 = false;
}),
child: Text(
'Blog',
style: TextStyle(
color: ishovering2 ? Colors.pink : Colors.white,
fontSize: 24,
fontFamily: 'Poppins',
),
),
),
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
PopupMenuItem(
child: OnHover(
builder: (isHovered) {
final color = isHovered
? Colors.pink
: const Color(0xFF262533);
return ListTile(
title: const Text(
'Archiv',
style: TextStyle(color: Colors.white),
),
enabled: true,
hoverColor: color,
onTap: () {},
);
},
),
),
]),
),
This is tricky indeed. What comes to my mind is if you would wrap your button on another InkWell and add some padding to it. And on onHover method you would dismiss your items.
Here is how I think it should look like:
InkWell(
onHiver: (){
dismissItems();
},
child:
Padding(
padding: //some padding
child: // your button
)
)

Place widget outside of bounding box

My app shows various Container() Widget()s in several columns in a certain view.
I tried to place some icons inside the Container()s to provide operations like delete, minimize etc. Unfortunately, that doesn't look good on native targets.
Therefore I'd like to keep the visual appearance as is and show an actions menu above the actual Container() once the mouse pointer moves over the Container().
This menu would be above all other widgets, be non-modal and disappear, once the pointer leaves the bound box of the Container(). Containers() shouldn't change size and location.
Using MouseRegion(), I'd make the menu appear and disappear.
May I place some Widget() outside the bounding rectangle of a Container() [or other widgets)? Ideally, I'd like to place it relative to the other bounding box.
UPDATE 2022-03-24
Created an OverlayMenu() class which renders something like this:
Usage:
OverlayMenu(
actionWidget:
const Icon(Icons.settings, color: Colors.blue, size: 20),
callbacks: [
() {
// Click on icons 1 action
},
() {
// Click on icon 2 action
}
],
icons: [
Icons.delete, Icons.tv
],
leftOffset: StoreProvider.of<EModel>(context)
.state
.columnWidth
.toDouble())
And the implementation of OverlayMenu():
import 'package:flutter/material.dart';
class OverlayMenu {
List<IconData> icons;
List<Function> callbacks;
Widget actionWidget;
double leftOffset = 0.0;
bool mayShowMenu = true;
OverlayMenu({ required this.actionWidget, required this.icons, required this.callbacks, required this.leftOffset });
Widget insert( BuildContext context ) {
return MouseRegion(
onEnter: (_) {
if (mayShowMenu) {
_showOverlay( context );
}
},
onExit: (_) {
mayShowMenu = true;
},
child: const Icon(Icons.settings, color: Colors.blue, size: 20),
);
}
///
///
///
void _showOverlay(BuildContext outerContext ) async {
final renderBox = outerContext.findRenderObject() as RenderBox;
var size = renderBox.size;
var offset = renderBox.localToGlobal(Offset.zero);
assert( icons.length == callbacks.length, 'Need to provide as many icons as callbacks' );
List<Widget> actionElements = List.empty( growable: true );
for( var n=0; n<icons.length; n++ ) {
actionElements.add( GestureDetector(
onTap: () {
callbacks[ n ]();
},
child: Icon( icons[ n ], size: 22 ))
);
}
// Declaring and Initializing OverlayState
// and OverlayEntry objects
OverlayState? overlayState = Overlay.of(outerContext);
OverlayEntry? overlayEntry;
overlayEntry = OverlayEntry(builder: (context) {
// You can return any widget you like here
// to be displayed on the Overlay
return Stack(children: [
Positioned(
top: offset.dy,
left: offset.dx +
leftOffset -
40,
child: MouseRegion(
onExit: (_) {
overlayEntry?.remove();
print(DateTime.now().toString() + ' ovl: removed');
},
cursor: SystemMouseCursors.contextMenu,
child: Material(
child:Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: Colors.grey.shade300,
width: 1,
),
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 4,
blurRadius: 4,
offset: const Offset( 4,4 ),
blurStyle: BlurStyle.normal),
]
),
child: SizedBox(
width: 60,
height: 60,
child: Wrap(spacing: 4, children: actionElements
))))),
),
]);
});
// Inserting the OverlayEntry into the Overlay
overlayState?.insert(overlayEntry);
mayShowMenu = true;
print(DateTime.now().toString() + ' ovl: inserted');
}
}
One approach you can use to achieve what you want is Overlay widget since it's non-modal and also does't require layout/size changes to have hit testable items.
Based on your question I assume this flow is what want:
Insert an overlay entry once the pointer has entered the widget and remove it once it leaves
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Stack(
children: [
MouseRegion(
onEnter: (_) {
Overlay.of(context)!.insert(this._overlayEntry);
},
onExit: (_) => clear(),
child: FloatingActionButton(
key: buttonKey,
onPressed: () {},
),
),
],
),
),
);
}
This is how we remove the entry, a check whether we can remove or not and a delay (for smoothing) :
Future<void> clear() async {
if (!keepMenuVisible) {
await Future.delayed(Duration(milliseconds: 200));
if (!keepMenuVisible) {
_overlayEntry.remove();
}
}
}
The additional delays are used to ensure that the menu doesn't despair reactively but instead we make it smoother.
keepMenuVisible is used to lock the menu and keep it visible once the menu it self has been hovered.
Finally, we create the entry and place the items relative to the main widget (FAB in this case):
#override
void initState() {
super.initState();
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
_overlayEntry = _createOverlayEntry();
});
}
OverlayEntry _createOverlayEntry() {
final renderBox = buttonKey.currentContext!.findRenderObject() as RenderBox;
var size = renderBox.size;
var offset = renderBox.localToGlobal(Offset.zero);
return OverlayEntry(
builder: (_) => Positioned(
left: offset.dx,
top: offset.dy + size.height + 5.0,
width: 200,
child: MouseRegion(
onEnter: (_) {
keepMenuVisible = true;
},
onHover: (_) {
keepMenuVisible = true;
},
onExit: (_) async {
keepMenuVisible = false;
clear();
},
child: Material(
elevation: 4.0,
child: ListView(
padding: EdgeInsets.zero,
shrinkWrap: true,
children: <Widget>[
ListTile(
onTap: () => print('tap action 1'),
title: Text('Action 1'),
),
ListTile(
onTap: () => print('tap action 2'),
title: Text('Action 2'),
)
],
),
),
),
),
);
}
check the full sample here

Flutter remove OverlayEntry if touch outside

I have a CustomDropDown, done with a OverlayEntry. The problem is that I have a StatefulWidget for that, which I place in my Screen simply like that:
CustomDropDownButton(
buttonLabel: 'Aus Vorauswahl wählen',
options: [
'1',
'2',
'3',
'4',
],
),
Now inside that CustomDropDownButton I can simply call floatingDropdown.remove(); where ever I want but how can I call that from a Parent-Widget?? I hope you understand my problem. Right now the only way to remove the overlay is by pressing the DropDownButton again, but it should be removed everytime the user taps outside the actual overlay.
I am quite lost here so happy for every help! Let me know if you need any more details!
This is the code for my CustomDropDownButton if that helps:
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../../constants/styles/colors.dart';
import '../../../constants/styles/text_styles.dart';
import '../../../services/size_service.dart';
import 'drop_down.dart';
class CustomDropDownButton extends StatefulWidget {
String buttonLabel;
final List<String> options;
CustomDropDownButton({
required this.buttonLabel,
required this.options,
});
#override
_CustomDropdownState createState() => _CustomDropdownState();
}
class _CustomDropdownState extends State<CustomDropDownButton> {
late GlobalKey actionKey;
late double height, width, xPosition, yPosition;
bool _isDropdownOpened = false;
int _selectedIndex = -1;
late OverlayEntry floatingDropdown;
#override
void initState() {
actionKey = LabeledGlobalKey(widget.buttonLabel);
super.initState();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
key: actionKey,
onTap: () {
setState(() {
if (_isDropdownOpened) {
floatingDropdown.remove();
} else {
findDropdownData();
floatingDropdown = _createFloatingDropdown();
Overlay.of(context)!.insert(floatingDropdown);
}
_isDropdownOpened = !_isDropdownOpened;
});
},
child: Container(
height: scaleWidth(50),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(width: 1.0, color: AppColors.black),
),
color: AppColors.white,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: scaleWidth(10),
),
Text(
widget.buttonLabel,
style: AppTextStyles.h5Light,
),
Spacer(),
_isDropdownOpened
? SvgPicture.asset(
'images/icons/arrow_down_primary.svg',
width: scaleWidth(21),
)
: SvgPicture.asset(
'images/icons/arrow_up_primary.svg',
width: scaleWidth(21),
),
SizedBox(
width: scaleWidth(10),
),
],
),
),
);
}
void findDropdownData() {
RenderBox renderBox =
actionKey.currentContext!.findRenderObject()! as RenderBox;
height = renderBox.size.height;
width = renderBox.size.width;
Offset? offset = renderBox.localToGlobal(Offset.zero);
xPosition = offset.dx;
yPosition = offset.dy;
}
OverlayEntry _createFloatingDropdown() {
return OverlayEntry(builder: (context) {
return Positioned(
left: xPosition,
width: width,
top: yPosition + height,
height: widget.options.length * height + scaleWidth(5),
child: DropDown(
itemHeight: height,
options: widget.options,
onOptionTap: (selectedIndex) {
setState(() {
widget.buttonLabel = widget.options[selectedIndex];
_selectedIndex = selectedIndex;
floatingDropdown.remove();
_isDropdownOpened = !_isDropdownOpened;
});
},
selectedIndex: _selectedIndex,
),
);
});
}
}
1. Return a ListView instead GestureDetector
2. Under Listview use that GestureDetector containing DropDown as one of the children.
3. Add another children(widgets) as GestureDetector and set onTap of each one as:
GestureDetector(
onTap: () {
if(isDropdownOpened){
floatingDropDown!.remove();
isDropdownOpened = false;
}
},
child: Container(
height: 200,
color: Colors.black,
),
)
In short you have to add GestureDetector to the part wherever you want the tapping should close overlay entry
** Full Code **
//This is to close overlay when you navigate to another screen
#override
void dispose() {
// TODO: implement dispose
floatingDropDown!.remove();
super.dispose();
}
Widget build(BuildContext context) {
return ListView(
children: [
Padding(
padding: EdgeInsets.all(20),
child: GestureDetector(
key: _actionKey,
onTap: () {
setState(() {
if (isDropdownOpened) {
floatingDropDown!.remove();
} else {
findDropDownData();
floatingDropDown = _createFloatingDropDown();
Overlay.of(context)!.insert(floatingDropDown!);
}
isDropdownOpened = !isDropdownOpened;
});
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), color: Colors.orangeAccent),
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
children: <Widget>[
Text(
widget.text,
style: TextStyle(color: Colors.white, fontSize: 20),
),
Spacer(),
Icon(
Icons.arrow_drop_down,
color: Colors.white,
),
],
),
),
),
),
GestureDetector(
onTap: () {
if(isDropdownOpened){
floatingDropDown!.remove();
isDropdownOpened = false;
}
},
child: Container(
height: 200,
color: Colors.black,
),
)
],
);
}
Let me know whether it helped or not
Listen to full screen onTapDown gesture and navigation event.
The screen' s gesture event:
#override
Widget build(BuildContext context) {
return RawGestureDetector(
gestures: {
PenetrableTapRecognizer: GestureRecognizerFactoryWithHandlers<PenetrableTapRecognizer>(
() => PenetrableTapRecognizer(),
(instance) {
instance.onTapDown = (_) => _handleGlobalGesture();
},
),
},
behavior: HitTestBehavior.opaque,
child: Scaffold(
),
);
}
void _handleGlobalGesture {
// insert or remove the popup menu
// a bool flag maybe helpful
}
class PenetrableTapRecognizer extends TapGestureRecognizer {
#override
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
}

How to set Flutter showMenu starting point

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:
Here is a screenshot of the current placement of the popUpMenu (Please ignore other design differences as they are irrelevant):
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.
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