'Flutter: How to get the size of a child widget in an overlay before it is rendered to use it in its parent's layout?

TL;DR

I have a parent that depends on the size of a child and that child is in the overlay and is not rendered until the parent is pressed, how can I get the child's size before rendering to layout the parent correctly?

Details

I edited flutter's PopupMenuButton and created a custom PopupButton that takes a child, content, and an offsetBuilder.

It can show the content on an Overlay when child is pressed. The content's position on the overlay is determined according to the offset returned from offsetBuilder as here:

             PopupButton(
              child: Container(
                child: const Text('MMMMMMMMMMM\nMMMMMMMMMMM\nMMMMMMMMMMM',
                    style: TextStyle(fontSize: 60), maxLines: 10,),
                decoration:
                    BoxDecoration(border: Border.all(color: Colors.red)),
              ),
              content: popupContent,
              offsetBuilder: (size) {
                // size is the size of the child above
                return Offset((size.width - 500) / 2, 0.0);
              },
            )

the offset returned above translates content starting from the top left of the child as here:

enter image description here

I need to know the size of the content (the blue row) in order to place it above the child (the Text widget above) like here (where the offset is hardcoded): enter image description here

The problem is that content is not actually rendered until I press on the Text so using GlobalKey in the code above will not work because when the code above executes the conten is still not actually rendered and Size returned by the global key will be zero or null.

Also, using this solution may solve the problem but will render the widgets wrong for 1 frame and then everything will be correct (it is using WidgetsBinding.instance.addPostFrameCallback).

How can I get the size of content to use it in offsetBuilder?



Solution 1:[1]

I don't know if this is the most CLEAN and OPTIMAL way to do it, but I could solve it using CustomSingleChildLayout widget. This widget takes a child and delegates its layout to some SingleChildLayoutDelegate as per the documentation. So all I had to do in this case is send this SingleChildLayoutDelegate the Size and Offset of the popup button and then do the calculations in the getPositionForChild method of the delegate. However this forced me to refactor the PopupButton's code to accept in its constructor a ContentBuilder which is the function to be called inside the PopupButton and that will pass the button Size and Offset and get the CustomSingleChildLayout widget.

This is the CustomSingleChildLayout class:

    class PopupContent extends StatelessWidget {
  final Size popupButtonSize;
  final Offset popupButtonOffset;

  const PopupContent({
    Key? key,
    required this.popupButtonSize,
    required this.popupButtonOffset,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final moc = MediaQuery.of(context);
    return CustomSingleChildLayout(
      delegate: PopupContentSingleChildLayoutDelegate(
        popupButtonSize: popupButtonSize,
        popupButtonOffset: popupButtonOffset,
        padding: moc.padding,
      ),
      child: ... the blue row,
    );
  }
}

class PopupContentSingleChildLayoutDelegate extends SingleChildLayoutDelegate {
  final Size popupButtonSize;
  final Offset popupButtonOffset;
  final EdgeInsets padding;

  PopupContentSingleChildLayoutDelegate({
    required this.popupButtonSize,
    required this.popupButtonOffset,
    required this.padding,
  });

  @override
  bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate) {
    return this != oldDelegate;
  }

  @override
  Offset getPositionForChild(Size size, Size childSize) {
    Offset offset = Offset((popupButtonSize.width - childSize.width) / 2, 0);
    offset = popupButtonOffset
        .translate(offset.dx, offset.dy)
        .translate(0, -childSize.height);

    // Find the ideal vertical position.
    double y = offset.dy;

    // Find the ideal horizontal position.
    double x = offset.dx;

  ...some other calculations

    return Offset(x, y);
  }
}

and this is how I call it now

PopupButton(
              child: Container(
                decoration:
                    BoxDecoration(border: Border.all(color: Colors.red)),
                child: Text(text,
                    maxLines: 3, style: const TextStyle(fontSize: 60)),
              ),
              contentBuilder: (size, offset) => PopupContent(
                popupButtonOffset: offset,
                popupButtonSize: size,
              ),
            )

now it works, but maybe its time the code be abstracted more and cleaned up.

enter image description here

Solution 2:[2]

You can use LayoutBuilder to get size datas of the parent widget. Here is the example:

Container(
   width: _mainWidth,
   height: _mainHeight,
   color: Colors.blue,
   child: Center(
       child: LayoutBuilder(
           builder: (BuildContext context, BoxConstraints constraints) {
               return Container(
                 width: constraints.maxWidth / 2,
                 height: constraints.maxHeight / 2,
                 color: Colors.yellow,
               );},
           ),
       ),
   ),

constraints can give you width and height info. In the example above, child widget's size is half of it's parent.

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 Haidar
Solution 2 aedemirsen