Flutter Web ShowMenu 不随屏幕调整大小而移动

Posted

技术标签:

【中文标题】Flutter Web ShowMenu 不随屏幕调整大小而移动【英文标题】:Flutter Web ShowMenu Does Not Move With Screen Resize 【发布时间】:2022-01-11 03:42:01 【问题描述】:

棘手的问题,希望有人有好的想法。

我在颤动的文本按钮上调用showMenu(...),效果很好。 但是,有时在屏幕调整大小期间,菜单会卡在屏幕上的某个位置(远离其预期位置)。有时它会跟随其锚定位置。很奇怪,我也注意到下拉菜单的这种行为。

这是我的示例代码。我想始终随屏幕移动菜单,或者在最坏的情况下,在屏幕调整大小事件中隐藏菜单。任何关于如何做的想法都会很棒!

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'),
        ]);
  

【问题讨论】:

【参考方案1】:

为了在调整屏幕大小时隐藏菜单,可以使用dart:html 的window 对象将事件监听器添加到浏览器的resize 并弹出菜单的上下文。

这是您的 ZHomeMenuBar 小部件的更新代码:

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) 
    ...
  

那么didChangeDependencies 方法中的代码的作用是:

    窗口监听调整大小事件。 获取上下文的ModalRoute。 如果ModalRoute 为空,则该方法返回 检查ModalRouteisCurrent 属性。如果为 false,则意味着 ModalRoute 不是导航器上最顶层的路线,这意味着屏幕上有一个对话框。 所以如果isCurrent 为假,我们会弹出上下文(这会删除对话框),并使用debounceDuration 设置一个短计时器,然后显示对话框。这将使用新重新计算的尺寸并在正确的位置显示对话框。 如果计时器在另一个计时器运行时处于活动状态,我们将取消前一个计时器并将新计时器分配给timer 变量。

您可以根据需要更新debounceDuration

【讨论】:

我真的很感谢这里的努力,我明白你想要什么。我想知道在颤振中是否有更平滑的方式来做到这一点,但也许没有.. 另外,我们如何在这里获取上下文? didChangeDependencies 可以访问上下文。见api.flutter.dev/flutter/widgets/State/initState.html【参考方案2】:

我只用了几个月的 Flutter,所以我的方法可能是完全错误的,但是在 Flutter 中定位任何东西时我会做两件事。

    我总是根据数学表达式调整小部件的大小。例如,我有这个帮助类,我在 *** 的其他地方找到了它,它可以让我获得屏幕的宽度和高度。要使用它,您只需将以下内容放在任何小部件的构建方法的开头:
SizeConfig.init(context); 

再一次,我不能把这个助手类归功于我,我是从这里得到的。如果我能找到它,我会链接它。

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;
  

如果您根据数学表达式使用此类和大小,则每次调整窗口大小时都会重新评估该表达式。因此,例如,这将始终将高度设置为安全可用高度的 1/4(安全可用高度占标准应用栏)。调整窗口大小时,会重新计算此值。此方法适用于小部件尺寸缩放

height: SizeConfig.safeBlockVertical * 25,

    对于定位,然后我使用容器、行、列、填充、中心等。我从来没有发现自己需要使用“定位”或偏移,这来自我的下一点......

    如果页面会很忙,我总是以 Stack 小部件作为基础,这提供了灵活性,因为通过根据数学表达式调整所有内容的大小,我可以将两个小部件直接相邻放置,但位于不同的“层” "的堆栈,没有人能说出来。

    奖励:如果您遇到需要在特定条件下完全修改小部件或其大小的实例,尽管在很多情况下这是不好的做法,但对于简单的条件渲染,可以使用通用小部件返回函数真是一件好事。或者,考虑您的特定参数并返回不同高度或宽度的通用函数可能很有用。

以这个布局代码为例。根据你是在桌面平台还是在移动平台上,我会以不同的方式装饰我的路线。我正在为此使用 vrouter 包...

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

编辑:我通过删除按钮并用文本小部件替换它们来测试你的类,当我调整窗口大小时它们似乎可以正确移动。

【讨论】:

【参考方案3】:

你可以使用WidgetsBindingObserver mixin,它可以用来监听Widget的resize,这样你就可以pop显示的菜单了。这适用于所有可用平台。

示例代码:

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'),
                  )
                ],
              ),
            ),
          ],
        ),
      ),
    );
  


代替:

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

你也可以使用Navigator.of(context).popUntil(ModalRoute.withName('YourPageName'));

如果您使用命名路由。

【讨论】:

以上是关于Flutter Web ShowMenu 不随屏幕调整大小而移动的主要内容,如果未能解决你的问题,请参考以下文章

Android 智能应用横幅不随屏幕缩放

UITabBar 宽度不随屏幕尺寸增加

KivyMD DatePicker 不随屏幕大小调整大小

Flutter web 获取屏幕大小

Flutter web:'webdev build'时出现空白屏幕

android布局如何实现显示填满整个手机屏幕,EditText如何设置能使其样式不随输入内容太多而发生变化。