HII
HII

Reputation: 4129

How to get the exact size of PopupMenuButton (or any widget)?

I need to get the PopupMenuButton's size because it has a property offset which controls where the dropdown menu is rendered on the screen, and I want this one to be rendered such that the top left of the drop down menu is aligned with the bottom left of the PopupMenuButton (see image below).

My approach now is this:

extension TextExtension on Text {
  /// Calculates the size of the text inside this text widget.
  /// note: this method supposes ltr direction of text, which is not always true, but it doesn't affect the size that much, so
  /// keep in mind that the size returned may be approximate in some cases.
  /// The text inside this widget must be non-null before calling this method.
  Size getSize({TextDirection? textDirection}) {
    TextPainter tp = TextPainter(
        text: TextSpan(text: data),
        textDirection: textDirection ?? TextDirection.ltr)
      ..layout();
    return tp.size;
  }
}

And then when I define the PopupMenuButton, I do this:

Widget _dropDownMenu({
    required BuildContext context,
    required String title,
    required List<PopupMenuItem> items,
  }) {
    final text = Text(
      title,
      style: Theme.of(context).textTheme.bodyMedium,
    );
    final textSize = text.size;
    return PopupMenuButton(
        child: text,
        itemBuilder: (context) => items,
        offset: Offset(0, textSize.height),
      );
  }

It works, but I don't like it. I think there must be a better way to do this.

This is how it looks like right now:

enter image description here

I tried LayoutBuilder, but it is returning infinite width constraints.

Is there a more clean way of doing this?

Upvotes: 0

Views: 730

Answers (1)

HII
HII

Reputation: 4129

It seems there is no other way except the approach I mentioned in the question, or to modify the source code of the PopupMenuButton to make it accept an OffsetBuilder as pskink mentioned. This can be done like this (and there is full working example below):

  • go to the PopupMenuButton source code and copy it all into a new file custom_popup_menu.dart (in this new file just remove all the imports and import them again as suggested by the IDE to fix them)
  • add this to anywhere top level in the file: Offset _defaultOffsetBuilder(Size size) => Offset.zero;
  • inside the PopupMenuButton class replace final Offset offset with
  /// The button size will be passed to this function to get the offset applied
  /// to the Popup Menu when it is open. The top left of the [PopupMenuButton] is considered
  /// as the origin of the coordinate system of this offset.
  ///
  /// When not set, the Popup Menu Button will be positioned directly next to
  /// the button that was used to create it.
  final Offset Function(Size) offsetBuilder;
  • inside the constructor of this class replace this.offset with this.offsetBuilder = _defaultOffsetBuilder,

  • in the showButtonMenu method of PopupMenuButtonState class, replace

      Rect.fromPoints(
        button.localToGlobal(widget.offset, ancestor: overlay),
        button.localToGlobal(
            button.size.bottomRight(Offset.zero) + widget.offset,
            ancestor: overlay),
      ),
      Offset.zero & overlay.size,
    );

with

    final offset = widget.offsetBuilder(button.size);
    final RelativeRect position = RelativeRect.fromRect(
      Rect.fromPoints(
        button.localToGlobal(offset, ancestor: overlay),
        button.localToGlobal(
            button.size.bottomRight(Offset.zero) + offset,
            ancestor: overlay),
      ),
      Offset.zero & overlay.size,
    );
  • Full Working Example:

... (imports)
import 'custom_popup_menu.dart' as pm;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) => const MaterialApp(
        debugShowCheckedModeBanner: false,
        home: HomePage2(),
      );
}

class HomePage2 extends StatelessWidget {
  const HomePage2({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) => Scaffold(
        body: Align(
          alignment: const Alignment(0, -0.8),
          child: Container(
            decoration: BoxDecoration(border: Border.all(width: 2.0)),
            child: pm.PopupMenuButton<String>(
              child: const Text(
                'Press Me',
                style: TextStyle(color: Colors.black, fontSize: 50),
              ),
              itemBuilder: (context) => [
                _buildPopupMenuItem(),
                _buildPopupMenuItem(),
                _buildPopupMenuItem(),
              ],
              color: Colors.red,
              offsetBuilder: (buttonSize) => Offset(0, buttonSize.height),
            ),
          ),
        ),
      );

  pm.PopupMenuItem<String> _buildPopupMenuItem() {
    return pm.PopupMenuItem(
      child: Text(
        'Press Me ${Random().nextInt(100)}',
        style: const TextStyle(color: Colors.black, fontSize: 50),
      ),
      onTap: () {},
    );
  }
}

enter image description here

Upvotes: 3

Related Questions