I want to build a context menu for each entry in a list. I try this with an overlay, but now most of the Buttons inside of the Overlay don't work anymore. The overlay is a stack with the options and a close button in the second Layer of the Stack (I kind of need it this way for the layout). I tried to remove the stack, but that didn't help.
The close button works fine, but the menu-options can't be hit. The hit-event passes though them and would hit anything blow it.
Edit:
The green box with the buttons "User", "Add" and "close" open after a click on a Button "options". The Button "close" is clickable and closes the overly, just as wanted. The other two buttons ("User" and "Add") and not clickable and not even response to the mouse hover.
I need these two buttons to be usable when visible. For now they only print something into the console but this will change later to call a new Page
Can you tell me, what I am missing or what is wrong with my code?
This is a simplified Version of the code with the same Problem. The class MyDelegate extends SingleChildLayoutDelegate is for positioning the overlay
import 'package:flutter/material.dart';
class OverlayMenu extends StatefulWidget {
const OverlayMenu({Key? key}) : super(key: key);
@override
State<OverlayMenu> createState() => _OverlayMenuState();
}
class _OverlayMenuState extends State<OverlayMenu> {
OverlayEntry? entry;
@override
void initState() {
super.initState();
}
@override
dispose() {
_hideOverLay();
super.dispose();
}
_hideOverLay() {
entry?.remove();
entry = null;
}
_showContextMenu(BuildContext context, LayerLink layerLink) {
_hideOverLay();
final overlay = Overlay.of(context)!;
RenderBox renderBox = context.findRenderObject() as RenderBox;
var anchorSize = renderBox.size;
entry = OverlayEntry(
builder: (context) => CompositedTransformFollower(
showWhenUnlinked: false,
link: layerLink,
child: CustomSingleChildLayout(
// # Added Line 1 #
delegate: MyDelegate(anchorSize), // # Added Line 2 #
child: ContextMenu(
onClose: () => _hideOverLay(),
),
),
),
);
overlay.insert(entry!);
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: List.generate(10, (index) => ListEntry(
onPressed: (context, link) => _showContextMenu(context, link),
)),
),
);
}
}
class ListEntry extends StatelessWidget {
final Function(BuildContext, LayerLink) onPressed;
ListEntry({Key? key, required this.onPressed}) : super(key: key);
final layerLink = LayerLink();
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(vertical: 5),
child: Container(
color: Colors.grey,
child: Row(
children: [
Expanded(child: Text("Text")),
Builder(
builder: (context) {
return CompositedTransformTarget(
link: layerLink,
child: ElevatedButton(
onPressed: () => onPressed.call(context, layerLink),
child: SizedBox(width: 50, child: Text("options")),
),
);
}
)
],
),
)
);
}
}
class MyDelegate extends SingleChildLayoutDelegate {
final Size anchorSize;
MyDelegate(this.anchorSize);
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
// allow the child to be smaller than parent's constraint
return constraints.loosen();
}
@override
Offset getPositionForChild(Size size, Size childSize) {
return Offset(anchorSize.width - childSize.width,
anchorSize.height - childSize.height);
}
@override
bool shouldRelayout(_) => true;
}
class ContextMenu extends StatelessWidget {
final Function() onClose;
const ContextMenu({Key? key, required this.onClose})
: super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
width: 240,
height: 150,
child: Stack(
children: [
Padding(
padding: const EdgeInsets.only(right: 18, bottom: 10),
child: Container(
color: Colors.greenAccent,
padding: const EdgeInsets.fromLTRB(10, 10, 10, 30),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildButton(
onPressed: () => print("User"),
icon: Icon(Icons.account_circle),
child: "User",
),
SizedBox(height: 5,),
_buildButton(
onPressed: () => print("Add"),
icon: Icon(Icons.add),
child: "Add",
),
],
),
),
),
Align(
alignment: Alignment.bottomRight,
child: ElevatedButton(
onPressed: onClose,
child: SizedBox(width: 50, child: Text("close")),
),
)
],
),
);
}
_buildButton({
required Widget icon,
required String child,
required Function() onPressed,
}) {
return ElevatedButton(
onPressed: onClose,
child: Row(
children: [
icon,
Text(child),
],
),
);
}
}
CodePudding user response:
I think that your Offset
of MyDelegate
is creating problems, you can see it if you set the Offset
to zero (MyDelegate
code):
@override
Offset getPositionForChild(Size size, Size childSize) {
return Offset.zero;
}
Now your buttons inside overlay should work, but your overlay is not positioned correctly. To fix that we will define overlay width and height, and then set the Offset
to the CompositedTransformFollower
like this:
_showContextMenu(BuildContext context, LayerLink layerLink) {
_hideOverLay();
final overlay = Overlay.of(context)!;
RenderBox renderBox = context.findRenderObject() as RenderBox;
var anchorSize = renderBox.size;
// assign overlay height and width here
double overlayHeight = 160; // was previously on SizedBox height of your ContextMenu Widget
double overlayWidth = 240; // was previously assigned on SizedBox width of your ContextMenu Widget
entry = OverlayEntry(
builder: (context) => Positioned(
width: overlayWidth,
height: overlayHeight,
child: CompositedTransformFollower(
showWhenUnlinked: false,
offset: Offset(
anchorSize.width - overlayWidth,
(anchorSize.height - overlayHeight),
),
link: layerLink,
child: CustomSingleChildLayout(
// # Added Line 1 #
delegate: MyDelegate(anchorSize), // # Added Line 2 #
child: ContextMenu(
onClose: () => _hideOverLay(),
),
),
),
),
);
overlay.insert(entry!);
}
Since we are already using the Positioned
widget, we can get rid of the CompositedTransformFollower
and its offset
, as well as the CustomSingleChildLayout
and MyDelegate
, so the simplest solution would be:
_showContextMenu(BuildContext context, LayerLink layerLink) {
_hideOverLay();
final overlay = Overlay.of(context)!;
// assign overlay height and width here
double overlayHeight = 160; // was previously on SizedBox height of your ContextMenu Widget
double overlayWidth = 240; // was previously assigned on SizedBox width of your ContextMenu Widget
RenderBox renderBox = context.findRenderObject() as RenderBox;
Offset offset = renderBox.localToGlobal(Offset.zero);
final xPosition = offset.dx;
final yPosition = offset.dy;
entry = OverlayEntry(
builder: (context) {
return Positioned(
height: overlayHeight,
width: overlayWidth,
left: xPosition / 2,
// show it on top of the clicked item
top: yPosition - overlayHeight,
// you don't need CustomSingleChildLayout with MyDelegate anymore
child: ContextMenu(
onClose: () => _hideOverLay(),
),
);
},
);
overlay.insert(entry!);
}
Now you can remove the SizedBox
height and width in your ContextMenu
widget since we are assigning it to the overlay now.