Flutter Web ShowMenu Does Not Move With Screen Resize
Asked Answered
P

4

8

Tricky question, hoping someone has a nice thought.

I call showMenu(...) on my text button in flutter, which works well. However, sometimes during screen resizing, the menu gets stuck somewhere on the screen (away from its intended position). Sometimes it follows its anchored position. Very odd, and I noticed this behavior with the dropdown menu too.

Here is my sample code. I want to either move the menu always with the screen, or in worst case, hide the menu on a screen resizing event. Any thoughts on how to do either would be great!

class ZHomeMenuBar extends StatefulWidget {
  const ZHomeMenuBar({Key? key}) : super(key: key);

  @override
  State<ZHomeMenuBar> createState() => _ZHomeMenuBarState();
}

class _ZHomeMenuBarState extends State<ZHomeMenuBar> {
  final GlobalKey _mesKey = GlobalKey();
  final GlobalKey _accKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    final zuser = Provider.of<ZUser?>(context);
    return Container(
      height: 66,
      decoration: BoxDecoration(color: context.backgroundColor),
      padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: Row(
        children: [
          Text(zuser == null ? "" : zuser.displayName ?? ""),
          const Spacer(),
          ZTextButton(text: "Portfolio", onPressed: () => {}),
          context.sh,
          ZTextButton(
              key: _mesKey,
              text: "Messages",
              onPressed: () {
                _showMessage(context);
              }),
          context.sh,
          ZTextButton(key: _accKey, text: "Account", onPressed: () {}),
          context.sh,
        ],
      ),
    );
  }

  _showMessage(context) {
    final RenderBox renderBox =
        _mesKey.currentContext?.findRenderObject() as RenderBox;
    final Size size = renderBox.size;
    final Offset offset = renderBox.localToGlobal(Offset.zero);

    showMenu(
        context: context,
        position: RelativeRect.fromLTRB(offset.dx, offset.dy + size.height,
            offset.dx + size.width, offset.dy + size.height),
        items: [
          PopupMenuItem<String>(child: const Text('menu option 1'), value: '1'),
          PopupMenuItem<String>(child: const Text('menu option 2'), value: '2'),
          PopupMenuItem<String>(child: const Text('menu option 3'), value: '3'),
        ]);
  }
}
Prismatoid answered 5/12, 2021 at 17:39 Comment(0)
N
4

In order to hide the menu when you're resizing the screen, you can use dart:html's window object to add an event listener to the browser's resize and pop the context of the menu.

Here is the updated code for your ZHomeMenuBar widget:

class ZHomeMenuBar extends StatefulWidget {
  ...
}

class _ZHomeMenuBarState extends State<ZHomeMenuBar> {
  ...

  Timer? timer;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    if (kIsWeb) {

      final int debounceDuration = 100;

      html.window.addEventListener("resize", (_) {
        final modalRoute = ModalRoute.of(context);

        if (modalRoute == null) {
          return;
        }

        if (!modalRoute.isCurrent) {
          Navigator.pop(context);

          if (timer != null && timer!.isActive) {
            timer!.cancel();
          }

          timer = Timer(Duration(milliseconds: 100), () {
            _showMessage(context);
          });
        }
      });

    }

  }

  @override
  void dispose() {
    super.dispose();
    timer?.cancel();
  }

  @override
  Widget build(BuildContext context) {
    ...
  }

  _showMessage(context) {
    ...
  }
}

So what the code in the didChangeDependencies method does is:

  1. The window listens to the resize event.
  2. The ModalRoute of the context is obtained.
  3. The method returns if the ModalRoute is null
  4. The isCurrent property of the ModalRoute is checked. If it is false, it means the ModalRoute isn't the top-most route on the navigator and that means there is a dialog on the screen.
  5. So if isCurrent is false, we pop the context (this removes the dialog), and set a short timer with the debounceDuration and then show the dialog. This uses the newly recalculated dimensions and shows the dialog at the right position.
  6. If the timer is active while another timer is running, we cancel the previous timer and assign the new one to the timer variable.

You can update the debounceDuration as you need.

Nosebleed answered 9/12, 2021 at 18:2 Comment(3)
I really appreciate the effort here, and I see what you are going for. I wonder if there's a smoother way in flutter to do it, but perhaps not..Prismatoid
Also, how do we get the context here?Prismatoid
didChangeDependencies has access to the context. See api.flutter.dev/flutter/widgets/State/initState.htmlNosebleed
E
3

You can use WidgetsBindingObserver mixin, which can be used to listen to the Widget resize so that you can pop the displayed menu. This will work on all available platforms.

Example code:

import 'package:flutter/material.dart';

class TestView extends StatefulWidget {
  const TestView({Key? key}) : super(key: key);

  @override
  _TestViewState createState() => _TestViewState();
}

class _TestViewState extends State<TestView> with WidgetsBindingObserver {
  late Size _lastSize;

  @override
  void initState() {
    super.initState();
    _lastSize = WidgetsBinding.instance!.window.physicalSize;
    WidgetsBinding.instance!.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance!.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeMetrics() {
    setState(() {
      _lastSize = WidgetsBinding.instance!.window.physicalSize;
    });
    Navigator.of(context).popUntil((route) {
      return route is MaterialPageRoute;
    });
  }

  void _select(String choice) {}

  @override
  Widget build(BuildContext context) {
    List<String> choices = ["1", "2", "3"];
    return Scaffold(
      appBar: AppBar(
        title: const Text('test'),
        actions: <Widget>[
          PopupMenuButton(
            onSelected: _select,
            padding: EdgeInsets.zero,
            // initialValue: choices[_selection],
            itemBuilder: (BuildContext context) {
              return choices.map((String choice) {
                return PopupMenuItem<String>(
                  value: choice,
                  child: Text(choice),
                );
              }).toList();
            },
          )
        ],
      ),
      body: Center(
        child: Column(
          children: [
            Text('Current size: $_lastSize'),
            Padding(
              padding: const EdgeInsets.all(16),
              child: DropdownButton<ThemeMode>(
                value: null,
                onChanged: (tm) {
                  print(_lastSize);
                },
                items: const [
                  DropdownMenuItem(
                    value: ThemeMode.system,
                    child: Text('System Theme'),
                  ),
                  DropdownMenuItem(
                    value: ThemeMode.light,
                    child: Text('Light Theme'),
                  ),
                  DropdownMenuItem(
                    value: ThemeMode.dark,
                    child: Text('Dark Theme'),
                  )
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

instead of:

Navigator.of(context).popUntil((route) {
  return route is MaterialPageRoute;
});

you can also use Navigator.of(context).popUntil(ModalRoute.withName('YourPageName'));

if you are using named routes.

Engine answered 15/12, 2021 at 17:23 Comment(0)
C
2

I've only been using flutter for a few months, so my methodology could be dead wrong, but I do two things when positioning anything in flutter.

  1. I always size widgets according to a mathematical expression. So for instance, I have this helper class, which I found somewhere else on stackoverflow, that will allow me to get the screen width and height. To use it, you just put the following at the beginning of any widget's build method:
SizeConfig.init(context); 

Again I can't take credit for this helper class, I got it from here sonmewhere. If I can find it, I'll link it.

import 'package:flutter/material.dart';

class SizeConfig {
  static late MediaQueryData _mediaQueryData;
  static double screenWidth = 0;
  static double screenHeight = 0;
  static double blockSizeHorizontal = 0;
  static double blockSizeVertical = 0;
  static double _safeAreaHorizontal = 0;
  static double _safeAreaVertical = 0;
  static double safeBlockHorizontal = 0;
  static double safeBlockVertical = 0;

  void init(BuildContext context){
    _mediaQueryData = MediaQuery.of(context);
    screenWidth = _mediaQueryData.size.width;
    screenHeight = _mediaQueryData.size.height;
    blockSizeHorizontal = screenWidth/100;
    blockSizeVertical = screenHeight/100;
    _safeAreaHorizontal = _mediaQueryData.padding.left + _mediaQueryData.padding.right;
    _safeAreaVertical = _mediaQueryData.padding.top + _mediaQueryData.padding.bottom + kToolbarHeight;
    safeBlockHorizontal = (screenWidth - _safeAreaHorizontal)/100;
    safeBlockVertical = (screenHeight - _safeAreaVertical)/100;
  }
}

If you use this class and size according to a mathematical expression, the expression will be re-evaluated on every window resize. So for instance, this will set height always to 1/4 of the safely available height (safely available height accounts for a standard appbar). When the window is resized, this value is recalculated. This method is for widget size scaling.

height: SizeConfig.safeBlockVertical * 25,
  1. For positioning, I then use Containers, Rows, Columns, Padding, Center, etc. I never find myself needing to use "positioned" or offsets and that comes from my next point...

  2. I always start with a Stack widget as a base if the page is going to be busy, this provides flexibility in that by sizing everything according to mathematical expression, I can positional two widgets directly next to each other but on different "layers" of the stack, and no one will be able to tell.

  3. Bonus: If you run into an instance where you need to completely rework a widget or its size given a specific condition, although they are bad practice in a lot of cases, for simple conditional rendering, a generic widget returning function can be a really good thing. Or a generic function that accounts for your specific parameters that just returns a different height or width can be useful.

Take for instance this layout code. Depending on if you are on a desktop platform versus a mobile platform, I decorate my routes differently. I'm using the vrouter package for this...

  VNesterBase(
    widgetBuilder: (child) => getValueByPlatform(
      desktop: DesktopLayout(
        key: UniqueKey(),
        child: child,
      ),
      fallback: BaseLayout(
        key: UniqueKey(),
        child: child,
      ),
    ),
    nestedRoutes: [] //nested routes

EDIT: I tested your class by removing the buttons and replacing them with text widgets and they appear to move correctly when I resize the window.

Cresting answered 14/12, 2021 at 23:34 Comment(0)
B
1

showmenu in flutter web with responsive position

GestureDetector(
  onTapDown: (TapDownDetails details) {
    final Offset offset = details.globalPosition;
    _showPopupMenu(context, offset);
  },
  child: IconButton(
    iconSize: 28,
    onPressed: null,
    icon: const Icon(Icons.more_vert_rounded),
    splashRadius: 20,
  ),
);



void _showPopupMenu(BuildContext context, Offset offset) async {
    await showMenu(
      context: context,
      position:
          RelativeRect.fromLTRB(offset.dx, offset.dy, offset.dx, offset.dy),
      items: [
        PopupMenuItem<String>(
          value: 'edit',
          child: Text('Edit'),
        ),
        PopupMenuItem<String>(
          value: 'delete',
          child: Text('Delete'),
        ),
      ],
      elevation: 8.0,
    ).then((value) {
      if (value == 'edit') {
        // Handle edit action
      } else if (value == 'delete') {
        // Handle delete action
      }
    });
  }

OUTPUT

Blinnie answered 14/7 at 19:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.